OLD | NEW |
---|---|
1 # Copyright 2014 The Chromium Authors. All rights reserved. | 1 # Copyright 2014 The Chromium Authors. All rights reserved. |
2 # Use of this source code is governed by a BSD-style license that can be | 2 # Use of this source code is governed by a BSD-style license that can be |
3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
4 | 4 |
5 """Post a try job to the Try server to produce build. It communicates | 5 """Post a try job request via HTTP to the Tryserver to produce build.""" |
6 to server by directly connecting via HTTP. | |
7 """ | |
8 | 6 |
9 import getpass | 7 import getpass |
8 import json | |
10 import optparse | 9 import optparse |
11 import os | 10 import os |
12 import sys | 11 import sys |
13 import urllib | 12 import urllib |
14 import urllib2 | 13 import urllib2 |
15 | 14 |
16 | 15 |
qyearsley
2014/05/15 19:40:06
Technically, I think there's no need for two blank
prasadv
2014/05/15 20:09:42
Done.
| |
16 # Link to get JSON data of builds | |
17 BUILDER_JSON_URL = ('%(server_url)s/json/builders/%(bot_name)s/builds/' | |
18 '%(build_num)s?as_text=1&filter=0') | |
19 | |
20 # Link to display build steps | |
21 BUILDER_HTML_URL = ('%(server_url)s/builders/%(bot_name)s/builds/%(build_num)s') | |
22 | |
23 # Tryserver buildbots status page | |
24 TRY_SERVER_URL = 'http://build.chromium.org/p/tryserver.chromium' | |
25 | |
26 # Hostname of the tryserver where perf bisect builders are hosted. This is used | |
27 # for posting build request to tryserver. | |
28 BISECT_BUILDER_HOST = 'master4.golo.chromium.org' | |
29 # 'try_job_port' on tryserver to post build request. | |
30 BISECT_BUILDER_PORT = '8328' | |
31 | |
32 | |
33 # From buildbot.status.builder. | |
34 # See: http://docs.buildbot.net/current/developer/results.html | |
35 SUCCESS, WARNINGS, FAILURE, SKIPPED, EXCEPTION, RETRY, TRYPENDING = range(7) | |
36 | |
37 # Status codes that can be returned by the GetBuildStatus method. | |
38 OK = (SUCCESS, WARNINGS, SKIPPED) | |
39 # Indicates build failure. | |
40 FAILED = (FAILURE, EXCEPTION) | |
41 # Inidcates build in progress or in pending queue. | |
42 PENDING = (RETRY, TRYPENDING) | |
43 | |
44 | |
17 class ServerAccessError(Exception): | 45 class ServerAccessError(Exception): |
46 | |
18 def __str__(self): | 47 def __str__(self): |
19 return '%s\nSorry, cannot connect to server.' % self.args[0] | 48 return '%s\nSorry, cannot connect to server.' % self.args[0] |
20 | 49 |
21 | 50 |
22 def PostTryJob(url_params): | 51 def PostTryJob(url_params): |
23 """Sends a build request to the server using the HTTP protocol. | 52 """Sends a build request to the server using the HTTP protocol. |
24 | 53 |
25 Args: | 54 Args: |
26 url_params: A dictionary of query parameters to be sent in the request. | 55 url_params: A dictionary of query parameters to be sent in the request. |
27 In order to post build request to try server, this dictionary | 56 In order to post build request to try server, this dictionary |
(...skipping 33 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
61 raise ServerAccessError('%s is unaccessible. Reason: %s' % (url, e)) | 90 raise ServerAccessError('%s is unaccessible. Reason: %s' % (url, e)) |
62 if not connection: | 91 if not connection: |
63 raise ServerAccessError('%s is unaccessible.' % url) | 92 raise ServerAccessError('%s is unaccessible.' % url) |
64 response = connection.read() | 93 response = connection.read() |
65 print 'Received %s from server' % response | 94 print 'Received %s from server' % response |
66 if response != 'OK': | 95 if response != 'OK': |
67 raise ServerAccessError('%s is unaccessible. Got:\n%s' % (url, response)) | 96 raise ServerAccessError('%s is unaccessible. Got:\n%s' % (url, response)) |
68 return True | 97 return True |
69 | 98 |
70 | 99 |
100 def _IsBuildRunning(build_data): | |
101 """Checks whether the build is in progress on buildbot. | |
102 | |
103 Presence of currentStep element in build JSON indicates build is in progress. | |
104 | |
105 Args: | |
106 build_data: JSON object with build data, loaded from buildbot JSON API. | |
qyearsley
2014/05/15 19:40:06
It could be slightly confusing to say that the exp
prasadv
2014/05/15 20:09:42
Done.
| |
107 | |
108 Returns: | |
109 Returns True if build is in progress, otherwise False. | |
qyearsley
2014/05/15 19:40:06
The word "Returns" is redundant here. Same applies
prasadv
2014/05/15 20:09:42
Done.
| |
110 """ | |
111 current_step = build_data.get('currentStep') | |
112 if (current_step and current_step.get('isStarted') and | |
113 current_step.get('results') is None): | |
114 return True | |
115 return False | |
116 | |
117 | |
118 def _IsBuildFailed(build_data): | |
119 """Checks whether the build failed on buildbot. | |
120 | |
121 Sometime build status is marked as failed even though compile and packaging | |
122 steps are successful. This may happen due to some intermediate steps of less | |
123 importance such as gclient revert, generate_telemetry_profile are failed. | |
124 Therefore we do an addition check to confirm if build was successful by | |
125 calling _IsBuildSucceed. | |
126 | |
127 Args: | |
128 build_data: JSON object with build data, loaded from buildbot JSON API. | |
129 | |
130 Returns: | |
131 Returns True if revision is failed build, otherwise False. | |
132 """ | |
133 if (build_data.get('results') in FAILED and | |
134 not _IsBuildSucceed(build_data)): | |
135 return True | |
136 return False | |
137 | |
138 | |
139 def _IsBuildSucceed(build_data): | |
qyearsley
2014/05/15 19:40:06
Should rename to _IsBuildSuccessful. (Also update
prasadv
2014/05/15 20:09:42
Done.
| |
140 """Checks whether the build succeeded on buildbot. | |
141 | |
142 We treat build as successful if the package_build step is completed without | |
143 any error i.e., when results attribute of the this step has value 0, 1 or 3 | |
144 in its first element. | |
145 | |
146 Args: | |
147 build_data: JSON object with build data, loaded from buildbot JSON API. | |
148 | |
149 Returns: | |
150 Returns True if revision is successfully build, otherwise False. | |
151 """ | |
152 if build_data.get('steps'): | |
153 for item in build_data.get('steps'): | |
154 # The 'results' attribute of each step consists of two elements, | |
155 # results[0]: This represents the status of build step. | |
156 # See: http://docs.buildbot.net/current/developer/results.html | |
157 # results[1]: List of items, contains text if step fails, otherwise empty. | |
158 if (item.get('name') == 'package_build' and | |
159 item.get('isFinished') and | |
160 item.get('results')[0] in OK): | |
ghost stip (do not use)
2014/05/15 19:27:55
OK includes SKIPPED, did you want to include that
prasadv
2014/05/15 20:09:42
I'm really not sure why a build could be skipped,
| |
161 return True | |
162 return False | |
163 | |
164 | |
165 def _FetchBuilderData(builder_url): | |
166 """Fetches JSON data for the all the builds from the tryserver. | |
167 | |
168 Args: | |
169 builder_url: A tryserver URL to fetch builds information. | |
170 | |
171 Returns: | |
172 A dictionary with information of all build on the tryserver. | |
173 """ | |
174 data = None | |
175 try: | |
176 url = urllib2.urlopen(builder_url) | |
177 except urllib2.URLError, e: | |
178 print ('urllib2.urlopen error %s, waterfall status page down.[%s]' % ( | |
179 builder_url, str(e))) | |
180 return None | |
181 if url is not None: | |
182 try: | |
183 data = url.read() | |
184 except IOError, e: | |
185 print 'urllib2 file object read error %s, [%s].' % (builder_url, str(e)) | |
186 return data | |
187 | |
188 | |
189 def _GetBuildData(buildbot_url): | |
190 """Gets build information for the given build id from the tryserver. | |
191 | |
192 Args: | |
193 buildbot_url: A tryserver URL to fetch build information. | |
194 | |
195 Returns: | |
196 A JSON object with build information if build exists, otherwise None. | |
197 """ | |
198 builds_json = _FetchBuilderData(buildbot_url) | |
199 if builds_json: | |
200 return json.loads(builds_json) | |
201 return None | |
202 | |
203 | |
204 def _GetBuildBotUrl(builder_host, builder_port): | |
205 """Gets build bot URL based on the host and port of the builders. | |
206 | |
207 Note: All bisect builder bots are hosted on tryserver.chromium i.e., | |
208 on master4:8328, since we cannot access tryserver using host and port | |
209 number directly, we use tryserver URL. | |
210 | |
211 Args: | |
212 builder_host: Hostname of the server where the builder is hosted. | |
213 builder_port: Port number of ther server where the builder is hosted. | |
214 | |
215 Returns: | |
216 URL of the buildbot as a string. | |
217 """ | |
218 if (builder_host == BISECT_BUILDER_HOST and | |
219 builder_port == BISECT_BUILDER_PORT): | |
220 return TRY_SERVER_URL | |
221 else: | |
222 return 'http://%s:%s' % (builder_host, builder_port) | |
223 | |
224 | |
225 def GetBuildStatus(build_num, bot_name, builder_host, builder_port): | |
226 """Gets build status from the buildbot status page for a given build number. | |
227 | |
228 Args: | |
229 build_num: A build number on tryserver to determine its status. | |
230 bot_name: Name of the bot where the build information is scanned. | |
231 builder_host: Hostname of the server where the builder is hosted. | |
232 builder_port: Port number of ther server where the builder is hosted. | |
233 | |
234 Returns: | |
235 A tuple consists of build status (SUCCESS, FAILED or PENDING ) and a link | |
ghost stip (do not use)
2014/05/15 19:27:55
nit: remove space
prasadv
2014/05/15 20:09:42
Done.
| |
236 to build status page on the waterfall. | |
237 """ | |
238 # Gets the buildbot url for the given host and port. | |
239 server_url = _GetBuildBotUrl(builder_host, builder_port) | |
240 buildbot_url = BUILDER_JSON_URL % {'server_url': server_url, | |
241 'bot_name': bot_name, | |
242 'build_num': build_num | |
243 } | |
244 build_data = _GetBuildData(buildbot_url) | |
245 results_url = None | |
246 if build_data: | |
247 # Link to build on the buildbot showing status of build steps. | |
248 results_url = BUILDER_HTML_URL % {'server_url': server_url, | |
249 'bot_name': bot_name, | |
250 'build_num': build_num | |
251 } | |
252 if _IsBuildFailed(build_data): | |
253 return (FAILED, results_url) | |
254 | |
255 elif _IsBuildSucceed(build_data): | |
256 return (OK, results_url) | |
257 return (PENDING, results_url) | |
qyearsley
2014/05/15 19:40:06
The function _IsBuildRunning isn't used anywhere c
prasadv
2014/05/15 20:09:42
Added this to check if build is in pending queue o
| |
258 | |
259 | |
260 def GetBuildNumFromBuilder(build_reason, bot_name, builder_host, builder_port): | |
ghost stip (do not use)
2014/05/15 19:27:55
just checking, these methods aren't used yet right
prasadv
2014/05/15 20:09:42
Yes, these aren't used anywhere yet.
On 2014/05/15
| |
261 """Gets build number on build status page for a given build reason. | |
262 | |
263 It parses the JSON data from buildbot page and collect basic information | |
264 about the all the builds and then this uniquely identifies the build based | |
265 on the 'reason' attribute in builds's JSON data. | |
266 The 'reason' attribute set while a build request is posted, and same is used | |
267 to identify the build on status page. | |
268 | |
269 Args: | |
270 build_reason: A unique build name set to build on tryserver. | |
271 bot_name: Name of the bot where the build information is scanned. | |
272 builder_host: Hostname of the server where the builder is hosted. | |
273 builder_port: Port number of ther server where the builder is hosted. | |
274 | |
275 Returns: | |
276 A build number as a string if found, otherwise None | |
qyearsley
2014/05/15 19:40:06
Could add a period on this line.
prasadv
2014/05/15 20:09:42
Done.
| |
277 """ | |
278 # Gets the buildbot url for the given host and port. | |
279 server_url = _GetBuildBotUrl(builder_host, builder_port) | |
280 buildbot_url = BUILDER_JSON_URL % {'server_url': server_url, | |
281 'bot_name': bot_name, | |
282 'build_num': '_all' | |
283 } | |
284 builds_json = _FetchBuilderData(buildbot_url) | |
285 if builds_json: | |
286 builds_data = json.loads(builds_json) | |
287 for current_build in builds_data: | |
288 if builds_data[current_build].get('reason') == build_reason: | |
289 return builds_data[current_build].get('number') | |
290 return None | |
291 | |
292 | |
71 def _GetQueryParams(options): | 293 def _GetQueryParams(options): |
72 """Parses common query parameters which will be passed to PostTryJob. | 294 """Parses common query parameters which will be passed to PostTryJob. |
73 | 295 |
74 Args: | 296 Args: |
75 options: The options object parsed from the command line. | 297 options: The options object parsed from the command line. |
76 | 298 |
77 Returns: | 299 Returns: |
78 A dictionary consists of query parameters. | 300 A dictionary consists of query parameters. |
79 """ | 301 """ |
80 values = {'host': options.host, | 302 values = {'host': options.host, |
(...skipping 13 matching lines...) Expand all Loading... | |
94 values['patch'] = options.patch | 316 values['patch'] = options.patch |
95 return values | 317 return values |
96 | 318 |
97 | 319 |
98 def _GenParser(): | 320 def _GenParser(): |
99 """Parses the command line for posting build request.""" | 321 """Parses the command line for posting build request.""" |
100 usage = ('%prog [options]\n' | 322 usage = ('%prog [options]\n' |
101 'Post a build request to the try server for the given revision.\n') | 323 'Post a build request to the try server for the given revision.\n') |
102 parser = optparse.OptionParser(usage=usage) | 324 parser = optparse.OptionParser(usage=usage) |
103 parser.add_option('-H', '--host', | 325 parser.add_option('-H', '--host', |
104 help='Host address of the try server.') | 326 help='Host address of the try server.') |
105 parser.add_option('-P', '--port', type='int', | 327 parser.add_option('-P', '--port', type='int', |
106 help='HTTP port of the try server.') | 328 help='HTTP port of the try server.') |
107 parser.add_option('-u', '--user', default=getpass.getuser(), | 329 parser.add_option('-u', '--user', default=getpass.getuser(), |
108 dest='user', | 330 dest='user', |
109 help='Owner user name [default: %default]') | 331 help='Owner user name [default: %default]') |
110 parser.add_option('-e', '--email', | 332 parser.add_option('-e', '--email', |
111 default=os.environ.get('TRYBOT_RESULTS_EMAIL_ADDRESS', | 333 default=os.environ.get('TRYBOT_RESULTS_EMAIL_ADDRESS', |
112 os.environ.get('EMAIL_ADDRESS')), | 334 os.environ.get('EMAIL_ADDRESS')), |
113 help='Email address where to send the results. Use either ' | 335 help=('Email address where to send the results. Use either ' |
114 'the TRYBOT_RESULTS_EMAIL_ADDRESS environment ' | 336 'the TRYBOT_RESULTS_EMAIL_ADDRESS environment ' |
115 'variable or EMAIL_ADDRESS to set the email address ' | 337 'variable or EMAIL_ADDRESS to set the email address ' |
116 'the try bots report results to [default: %default]') | 338 'the try bots report results to [default: %default]')) |
117 parser.add_option('-n', '--name', | 339 parser.add_option('-n', '--name', |
118 default= 'try_job_http', | 340 default='try_job_http', |
119 help='Descriptive name of the try job') | 341 help='Descriptive name of the try job') |
120 parser.add_option('-b', '--bot', | 342 parser.add_option('-b', '--bot', |
121 help=('IMPORTANT: specify ONE builder per run is supported.' | 343 help=('IMPORTANT: specify ONE builder per run is supported.' |
122 'Run script for each builders separately.')) | 344 'Run script for each builders separately.')) |
123 parser.add_option('-r', '--revision', | 345 parser.add_option('-r', '--revision', |
124 help='Revision to use for the try job; default: the ' | 346 help=('Revision to use for the try job; default: the ' |
125 'revision will be determined by the try server; see ' | 347 'revision will be determined by the try server; see ' |
126 'its waterfall for more info') | 348 'its waterfall for more info')) |
127 parser.add_option('--root', | 349 parser.add_option('--root', |
128 help='Root to use for the patch; base subdirectory for ' | 350 help=('Root to use for the patch; base subdirectory for ' |
129 'patch created in a subdirectory') | 351 'patch created in a subdirectory')) |
130 parser.add_option('--patch', | 352 parser.add_option('--patch', |
131 help='Patch information.') | 353 help='Patch information.') |
132 return parser | 354 return parser |
133 | 355 |
134 | 356 |
135 def Main(argv): | 357 def Main(argv): |
136 parser = _GenParser() | 358 parser = _GenParser() |
137 options, args = parser.parse_args() | 359 options, _ = parser.parse_args() |
138 if not options.host: | 360 if not options.host: |
139 raise ServerAccessError('Please use the --host option to specify the try ' | 361 raise ServerAccessError('Please use the --host option to specify the try ' |
140 'server host to connect to.') | 362 'server host to connect to.') |
141 if not options.port: | 363 if not options.port: |
142 raise ServerAccessError('Please use the --port option to specify the try ' | 364 raise ServerAccessError('Please use the --port option to specify the try ' |
143 'server port to connect to.') | 365 'server port to connect to.') |
144 params = _GetQueryParams(options) | 366 params = _GetQueryParams(options) |
145 PostTryJob(params) | 367 PostTryJob(params) |
146 | 368 |
147 | 369 |
148 if __name__ == '__main__': | 370 if __name__ == '__main__': |
149 sys.exit(Main(sys.argv)) | 371 sys.exit(Main(sys.argv)) |
372 | |
OLD | NEW |