OLD | NEW |
| (Empty) |
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 | |
3 # found in the LICENSE file. | |
4 | |
5 """This module contains functionality for starting build try jobs via HTTP. | |
6 | |
7 This includes both sending a request to start a job, and also related code | |
8 for querying the status of the job. | |
9 | |
10 This module can be either run as a stand-alone script to send a request to a | |
11 builder, or imported and used by calling the public functions below. | |
12 """ | |
13 | |
14 import getpass | |
15 import json | |
16 import optparse | |
17 import os | |
18 import sys | |
19 import urllib | |
20 import urllib2 | |
21 | |
22 # URL template for fetching JSON data about builds. | |
23 BUILDER_JSON_URL = ('%(server_url)s/json/builders/%(bot_name)s/builds/' | |
24 '%(build_num)s?as_text=1&filter=0') | |
25 | |
26 # URL template for displaying build steps. | |
27 BUILDER_HTML_URL = ('%(server_url)s/builders/%(bot_name)s/builds/%(build_num)s') | |
28 | |
29 # Try server status page for the perf try server. | |
30 PERF_TRY_SERVER_URL = 'http://build.chromium.org/p/tryserver.chromium.perf' | |
31 | |
32 # Hostname of the tryserver where perf bisect builders are hosted. | |
33 # This is used for posting build requests to the tryserver. | |
34 PERF_BISECT_BUILDER_HOST = 'master4.golo.chromium.org' | |
35 | |
36 # The default 'try_job_port' on tryserver to post build request. | |
37 PERF_BISECT_BUILDER_PORT = 8341 | |
38 | |
39 # Status codes that can be returned by the GetBuildStatus method. | |
40 # From buildbot.status.builder. | |
41 # See: http://docs.buildbot.net/current/developer/results.html | |
42 SUCCESS, WARNINGS, FAILURE, SKIPPED, EXCEPTION, RETRY, TRYPENDING = range(7) | |
43 | |
44 OK = (SUCCESS, WARNINGS) # These indicate build is complete. | |
45 FAILED = (FAILURE, EXCEPTION, SKIPPED) # These indicate build failure. | |
46 PENDING = (RETRY, TRYPENDING) # These indicate in progress or in pending queue. | |
47 | |
48 | |
49 class ServerAccessError(Exception): | |
50 | |
51 def __str__(self): | |
52 return '%s\nSorry, cannot connect to server.' % self.args[0] | |
53 | |
54 | |
55 def PostTryJob(host, port, params): | |
56 """Sends a build request to the server using the HTTP protocol. | |
57 | |
58 The required parameters are: | |
59 'revision': "src@rev", where rev is an SVN Revision to build. | |
60 'bot': Name of builder bot to use, e.g. "win_perf_bisect_builder". | |
61 | |
62 Args: | |
63 host: Hostname of the try server. | |
64 port: Port of the try server. | |
65 params: A dictionary of parameters to be sent in the POST request. | |
66 | |
67 Returns: | |
68 True if the request is posted successfully. | |
69 | |
70 Raises: | |
71 ServerAccessError: Failed to make a request to the try server. | |
72 ValueError: Not all of the expected inputs were given. | |
73 """ | |
74 if not params.get('revision'): | |
75 raise ValueError('Missing revision number.') | |
76 if not params.get('bot'): | |
77 raise ValueError('Missing bot name.') | |
78 | |
79 base_url = 'http://%s:%s/send_try_patch' % (host, port) | |
80 print 'Posting build request by HTTP.' | |
81 print 'URL: %s, params: %s' % (base_url, params) | |
82 | |
83 connection = None | |
84 try: | |
85 print 'Opening connection...' | |
86 connection = urllib2.urlopen(base_url, urllib.urlencode(params)) | |
87 print 'Done, request sent to server to produce build.' | |
88 except IOError as e: | |
89 raise ServerAccessError('%s is unaccessible. Reason: %s' % (base_url, e)) | |
90 if not connection: | |
91 raise ServerAccessError('%s is unaccessible.' % base_url) | |
92 | |
93 response = connection.read() | |
94 print 'Received response from server: %s' % response | |
95 if response != 'OK': | |
96 raise ServerAccessError('Response was not "OK".') | |
97 return True | |
98 | |
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: A dictionary with build data, loaded from buildbot JSON API. | |
107 | |
108 Returns: | |
109 True if build is in progress, otherwise False. | |
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 _IsBuildSuccessful. | |
126 | |
127 Args: | |
128 build_data: A dictionary with build data, loaded from buildbot JSON API. | |
129 | |
130 Returns: | |
131 True if revision is failed build, otherwise False. | |
132 """ | |
133 if (build_data.get('results') in FAILED and | |
134 not _IsBuildSuccessful(build_data)): | |
135 return True | |
136 return False | |
137 | |
138 | |
139 def _IsBuildSuccessful(build_data): | |
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 or 1 | |
144 in its first element. | |
145 | |
146 Args: | |
147 build_data: A dictionary with build data, loaded from buildbot JSON API. | |
148 | |
149 Returns: | |
150 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): | |
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 as 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 as 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 dictionary 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 for fetching build info. | |
206 | |
207 Bisect builder bots are hosted on tryserver.chromium.perf, though we cannot | |
208 access this tryserver using host and port number directly, so we use another | |
209 tryserver URL for the perf tryserver. | |
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 == PERF_BISECT_BUILDER_HOST and | |
219 builder_port == PERF_BISECT_BUILDER_PORT): | |
220 return PERF_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 the server where the builder is hosted. | |
233 | |
234 Returns: | |
235 A pair which consists of build status (SUCCESS, FAILED or PENDING) and a | |
236 link to build status page on the waterfall. | |
237 """ | |
238 results_url = None | |
239 if build_num: | |
240 # Get the URL for requesting JSON data with status information. | |
241 server_url = _GetBuildBotUrl(builder_host, builder_port) | |
242 buildbot_url = BUILDER_JSON_URL % { | |
243 'server_url': server_url, | |
244 'bot_name': bot_name, | |
245 'build_num': build_num, | |
246 } | |
247 build_data = _GetBuildData(buildbot_url) | |
248 if build_data: | |
249 # Link to build on the buildbot showing status of build steps. | |
250 results_url = BUILDER_HTML_URL % { | |
251 'server_url': server_url, | |
252 'bot_name': bot_name, | |
253 'build_num': build_num, | |
254 } | |
255 if _IsBuildFailed(build_data): | |
256 return (FAILED, results_url) | |
257 | |
258 elif _IsBuildSuccessful(build_data): | |
259 return (OK, results_url) | |
260 return (PENDING, results_url) | |
261 | |
262 | |
263 def GetBuildNumFromBuilder(build_reason, bot_name, builder_host, builder_port): | |
264 """Gets build number on build status page for a given 'build reason'. | |
265 | |
266 This function parses the JSON data from buildbot page and collects basic | |
267 information about the all the builds, and then uniquely identifies the build | |
268 based on the 'reason' attribute in builds's JSON data. | |
269 | |
270 The 'reason' attribute set is when a build request is posted, and it is used | |
271 to identify the build on status page. | |
272 | |
273 Args: | |
274 build_reason: A unique build name set to build on tryserver. | |
275 bot_name: Name of the bot where the build information is scanned. | |
276 builder_host: Hostname of the server where the builder is hosted. | |
277 builder_port: Port number of ther server where the builder is hosted. | |
278 | |
279 Returns: | |
280 A build number as a string if found, otherwise None. | |
281 """ | |
282 # Gets the buildbot url for the given host and port. | |
283 server_url = _GetBuildBotUrl(builder_host, builder_port) | |
284 buildbot_url = BUILDER_JSON_URL % { | |
285 'server_url': server_url, | |
286 'bot_name': bot_name, | |
287 'build_num': '_all', | |
288 } | |
289 builds_json = _FetchBuilderData(buildbot_url) | |
290 if builds_json: | |
291 builds_data = json.loads(builds_json) | |
292 for current_build in builds_data: | |
293 if builds_data[current_build].get('reason') == build_reason: | |
294 return builds_data[current_build].get('number') | |
295 return None | |
296 | |
297 | |
298 def _GetRequestParams(options): | |
299 """Extracts request parameters which will be passed to PostTryJob. | |
300 | |
301 Args: | |
302 options: The options object parsed from the command line. | |
303 | |
304 Returns: | |
305 A dictionary with parameters to pass to PostTryJob. | |
306 """ | |
307 params = { | |
308 'user': options.user, | |
309 'name': options.name, | |
310 } | |
311 # Add other parameters if they're available in the options object. | |
312 for key in ['email', 'revision', 'root', 'bot', 'patch']: | |
313 option = getattr(options, key) | |
314 if option: | |
315 params[key] = option | |
316 return params | |
317 | |
318 | |
319 def _GenParser(): | |
320 """Returns a parser for getting command line arguments.""" | |
321 usage = ('%prog [options]\n' | |
322 'Post a build request to the try server for the given revision.') | |
323 parser = optparse.OptionParser(usage=usage) | |
324 parser.add_option('-H', '--host', | |
325 help='Host address of the try server (required).') | |
326 parser.add_option('-P', '--port', type='int', | |
327 help='HTTP port of the try server (required).') | |
328 parser.add_option('-u', '--user', default=getpass.getuser(), | |
329 dest='user', | |
330 help='Owner user name [default: %default]') | |
331 parser.add_option('-e', '--email', | |
332 default=os.environ.get('TRYBOT_RESULTS_EMAIL_ADDRESS', | |
333 os.environ.get('EMAIL_ADDRESS')), | |
334 help=('Email address where to send the results. Use either ' | |
335 'the TRYBOT_RESULTS_EMAIL_ADDRESS environment ' | |
336 'variable or EMAIL_ADDRESS to set the email address ' | |
337 'the try bots report results to [default: %default]')) | |
338 parser.add_option('-n', '--name', | |
339 default='try_job_http', | |
340 help='Descriptive name of the try job') | |
341 parser.add_option('-b', '--bot', | |
342 help=('Only one builder per run may be specified; to post ' | |
343 'jobs on on multiple builders, run script for each ' | |
344 'builder separately.')) | |
345 parser.add_option('-r', '--revision', | |
346 help=('Revision to use for the try job. The revision may ' | |
347 'be determined by the try server; see its waterfall ' | |
348 'for more info.')) | |
349 parser.add_option('--root', | |
350 help=('Root to use for the patch; base subdirectory for ' | |
351 'patch created in a subdirectory.')) | |
352 parser.add_option('--patch', | |
353 help='Patch information.') | |
354 return parser | |
355 | |
356 | |
357 def Main(_): | |
358 """Posts a try job based on command line parameters.""" | |
359 parser = _GenParser() | |
360 options, _ = parser.parse_args() | |
361 if not options.host or not options.port: | |
362 parser.print_help() | |
363 return 1 | |
364 params = _GetRequestParams(options) | |
365 PostTryJob(options.host, options.port, params) | |
366 | |
367 | |
368 if __name__ == '__main__': | |
369 sys.exit(Main(sys.argv)) | |
OLD | NEW |