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 """Post a try job request via HTTP to the Tryserver to produce build.""" | |
6 | |
7 import getpass | |
8 import json | |
9 import optparse | |
10 import os | |
11 import sys | |
12 import urllib | |
13 import urllib2 | |
14 | |
15 # Link to get JSON data of builds | |
16 BUILDER_JSON_URL = ('%(server_url)s/json/builders/%(bot_name)s/builds/' | |
17 '%(build_num)s?as_text=1&filter=0') | |
18 | |
19 # Link to display build steps | |
20 BUILDER_HTML_URL = ('%(server_url)s/builders/%(bot_name)s/builds/%(build_num)s') | |
21 | |
22 # Tryserver buildbots status page | |
23 TRY_SERVER_URL = 'http://build.chromium.org/p/tryserver.chromium.perf' | |
24 | |
25 # Hostname of the tryserver where perf bisect builders are hosted. This is used | |
26 # for posting build request to tryserver. | |
27 BISECT_BUILDER_HOST = 'master4.golo.chromium.org' | |
28 # 'try_job_port' on tryserver to post build request. | |
29 BISECT_BUILDER_PORT = 8341 | |
30 | |
31 | |
32 # From buildbot.status.builder. | |
33 # See: http://docs.buildbot.net/current/developer/results.html | |
34 SUCCESS, WARNINGS, FAILURE, SKIPPED, EXCEPTION, RETRY, TRYPENDING = range(7) | |
35 | |
36 # Status codes that can be returned by the GetBuildStatus method. | |
37 OK = (SUCCESS, WARNINGS) | |
38 # Indicates build failure. | |
39 FAILED = (FAILURE, EXCEPTION, SKIPPED) | |
40 # Inidcates build in progress or in pending queue. | |
41 PENDING = (RETRY, TRYPENDING) | |
42 | |
43 | |
44 class ServerAccessError(Exception): | |
45 | |
46 def __str__(self): | |
47 return '%s\nSorry, cannot connect to server.' % self.args[0] | |
48 | |
49 | |
50 def PostTryJob(url_params): | |
51 """Sends a build request to the server using the HTTP protocol. | |
52 | |
53 Args: | |
54 url_params: A dictionary of query parameters to be sent in the request. | |
55 In order to post build request to try server, this dictionary | |
56 should contain information for following keys: | |
57 'host': Hostname of the try server. | |
58 'port': Port of the try server. | |
59 'revision': SVN Revision to build. | |
60 'bot': Name of builder bot which would be used. | |
61 Returns: | |
62 True if the request is posted successfully. Otherwise throws an exception. | |
63 """ | |
64 # Parse url parameters to be sent to Try server. | |
65 if not url_params.get('host'): | |
66 raise ValueError('Hostname of server to connect is missing.') | |
67 if not url_params.get('port'): | |
68 raise ValueError('Port of server to connect is missing.') | |
69 if not url_params.get('revision'): | |
70 raise ValueError('Missing revision details. Please specify revision' | |
71 ' information.') | |
72 if not url_params.get('bot'): | |
73 raise ValueError('Missing bot details. Please specify bot information.') | |
74 | |
75 # Pop 'host' and 'port' to avoid passing them as query params. | |
76 url = 'http://%s:%s/send_try_patch' % (url_params.pop('host'), | |
77 url_params.pop('port')) | |
78 | |
79 print 'Sending by HTTP' | |
80 query_params = '&'.join('%s=%s' % (k, v) for k, v in url_params.iteritems()) | |
81 print 'url: %s?%s' % (url, query_params) | |
82 | |
83 connection = None | |
84 try: | |
85 print 'Opening connection...' | |
86 connection = urllib2.urlopen(url, urllib.urlencode(url_params)) | |
87 print 'Done, request sent to server to produce build.' | |
88 except IOError, e: | |
89 raise ServerAccessError('%s is unaccessible. Reason: %s' % (url, e)) | |
90 if not connection: | |
91 raise ServerAccessError('%s is unaccessible.' % url) | |
92 response = connection.read() | |
93 print 'Received %s from server' % response | |
94 if response != 'OK': | |
95 raise ServerAccessError('%s is unaccessible. Got:\n%s' % (url, response)) | |
96 return True | |
97 | |
98 | |
99 def _IsBuildRunning(build_data): | |
100 """Checks whether the build is in progress on buildbot. | |
101 | |
102 Presence of currentStep element in build JSON indicates build is in progress. | |
103 | |
104 Args: | |
105 build_data: A dictionary with build data, loaded from buildbot JSON API. | |
106 | |
107 Returns: | |
108 True if build is in progress, otherwise False. | |
109 """ | |
110 current_step = build_data.get('currentStep') | |
111 if (current_step and current_step.get('isStarted') and | |
112 current_step.get('results') is None): | |
113 return True | |
114 return False | |
115 | |
116 | |
117 def _IsBuildFailed(build_data): | |
118 """Checks whether the build failed on buildbot. | |
119 | |
120 Sometime build status is marked as failed even though compile and packaging | |
121 steps are successful. This may happen due to some intermediate steps of less | |
122 importance such as gclient revert, generate_telemetry_profile are failed. | |
123 Therefore we do an addition check to confirm if build was successful by | |
124 calling _IsBuildSuccessful. | |
125 | |
126 Args: | |
127 build_data: A dictionary with build data, loaded from buildbot JSON API. | |
128 | |
129 Returns: | |
130 True if revision is failed build, otherwise False. | |
131 """ | |
132 if (build_data.get('results') in FAILED and | |
133 not _IsBuildSuccessful(build_data)): | |
134 return True | |
135 return False | |
136 | |
137 | |
138 def _IsBuildSuccessful(build_data): | |
139 """Checks whether the build succeeded on buildbot. | |
140 | |
141 We treat build as successful if the package_build step is completed without | |
142 any error i.e., when results attribute of the this step has value 0 or 1 | |
143 in its first element. | |
144 | |
145 Args: | |
146 build_data: A dictionary with build data, loaded from buildbot JSON API. | |
147 | |
148 Returns: | |
149 True if revision is successfully build, otherwise False. | |
150 """ | |
151 if build_data.get('steps'): | |
152 for item in build_data.get('steps'): | |
153 # The 'results' attribute of each step consists of two elements, | |
154 # results[0]: This represents the status of build step. | |
155 # See: http://docs.buildbot.net/current/developer/results.html | |
156 # results[1]: List of items, contains text if step fails, otherwise empty. | |
157 if (item.get('name') == 'package_build' and | |
158 item.get('isFinished') and | |
159 item.get('results')[0] in OK): | |
160 return True | |
161 return False | |
162 | |
163 | |
164 def _FetchBuilderData(builder_url): | |
165 """Fetches JSON data for the all the builds from the tryserver. | |
166 | |
167 Args: | |
168 builder_url: A tryserver URL to fetch builds information. | |
169 | |
170 Returns: | |
171 A dictionary with information of all build on the tryserver. | |
172 """ | |
173 data = None | |
174 try: | |
175 url = urllib2.urlopen(builder_url) | |
176 except urllib2.URLError, e: | |
177 print ('urllib2.urlopen error %s, waterfall status page down.[%s]' % ( | |
178 builder_url, str(e))) | |
179 return None | |
180 if url is not None: | |
181 try: | |
182 data = url.read() | |
183 except IOError, e: | |
184 print 'urllib2 file object read error %s, [%s].' % (builder_url, str(e)) | |
185 return data | |
186 | |
187 | |
188 def _GetBuildData(buildbot_url): | |
189 """Gets build information for the given build id from the tryserver. | |
190 | |
191 Args: | |
192 buildbot_url: A tryserver URL to fetch build information. | |
193 | |
194 Returns: | |
195 A dictionary with build information if build exists, otherwise None. | |
196 """ | |
197 builds_json = _FetchBuilderData(buildbot_url) | |
198 if builds_json: | |
199 return json.loads(builds_json) | |
200 return None | |
201 | |
202 | |
203 def _GetBuildBotUrl(builder_host, builder_port): | |
204 """Gets build bot URL based on the host and port of the builders. | |
205 | |
206 Note: All bisect builder bots are hosted on tryserver.chromium i.e., | |
207 on master4:8328, since we cannot access tryserver using host and port | |
208 number directly, we use tryserver URL. | |
209 | |
210 Args: | |
211 builder_host: Hostname of the server where the builder is hosted. | |
212 builder_port: Port number of ther server where the builder is hosted. | |
213 | |
214 Returns: | |
215 URL of the buildbot as a string. | |
216 """ | |
217 if (builder_host == BISECT_BUILDER_HOST and | |
218 builder_port == BISECT_BUILDER_PORT): | |
219 return TRY_SERVER_URL | |
220 else: | |
221 return 'http://%s:%s' % (builder_host, builder_port) | |
222 | |
223 | |
224 def GetBuildStatus(build_num, bot_name, builder_host, builder_port): | |
225 """Gets build status from the buildbot status page for a given build number. | |
226 | |
227 Args: | |
228 build_num: A build number on tryserver to determine its status. | |
229 bot_name: Name of the bot where the build information is scanned. | |
230 builder_host: Hostname of the server where the builder is hosted. | |
231 builder_port: Port number of ther server where the builder is hosted. | |
232 | |
233 Returns: | |
234 A tuple consists of build status (SUCCESS, FAILED or PENDING) and a link | |
235 to build status page on the waterfall. | |
236 """ | |
237 results_url = None | |
238 if build_num: | |
239 # Gets the buildbot url for the given host and port. | |
240 server_url = _GetBuildBotUrl(builder_host, builder_port) | |
241 buildbot_url = BUILDER_JSON_URL % {'server_url': server_url, | |
242 'bot_name': bot_name, | |
243 'build_num': build_num | |
244 } | |
245 build_data = _GetBuildData(buildbot_url) | |
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 _IsBuildSuccessful(build_data): | |
256 return (OK, results_url) | |
257 return (PENDING, results_url) | |
258 | |
259 | |
260 def GetBuildNumFromBuilder(build_reason, bot_name, builder_host, builder_port): | |
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. | |
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 | |
293 def _GetQueryParams(options): | |
294 """Parses common query parameters which will be passed to PostTryJob. | |
295 | |
296 Args: | |
297 options: The options object parsed from the command line. | |
298 | |
299 Returns: | |
300 A dictionary consists of query parameters. | |
301 """ | |
302 values = {'host': options.host, | |
303 'port': options.port, | |
304 'user': options.user, | |
305 'name': options.name | |
306 } | |
307 if options.email: | |
308 values['email'] = options.email | |
309 if options.revision: | |
310 values['revision'] = options.revision | |
311 if options.root: | |
312 values['root'] = options.root | |
313 if options.bot: | |
314 values['bot'] = options.bot | |
315 if options.patch: | |
316 values['patch'] = options.patch | |
317 return values | |
318 | |
319 | |
320 def _GenParser(): | |
321 """Parses the command line for posting build request.""" | |
322 usage = ('%prog [options]\n' | |
323 'Post a build request to the try server for the given revision.\n') | |
324 parser = optparse.OptionParser(usage=usage) | |
325 parser.add_option('-H', '--host', | |
326 help='Host address of the try server.') | |
327 parser.add_option('-P', '--port', type='int', | |
328 help='HTTP port of the try server.') | |
329 parser.add_option('-u', '--user', default=getpass.getuser(), | |
330 dest='user', | |
331 help='Owner user name [default: %default]') | |
332 parser.add_option('-e', '--email', | |
333 default=os.environ.get('TRYBOT_RESULTS_EMAIL_ADDRESS', | |
334 os.environ.get('EMAIL_ADDRESS')), | |
335 help=('Email address where to send the results. Use either ' | |
336 'the TRYBOT_RESULTS_EMAIL_ADDRESS environment ' | |
337 'variable or EMAIL_ADDRESS to set the email address ' | |
338 'the try bots report results to [default: %default]')) | |
339 parser.add_option('-n', '--name', | |
340 default='try_job_http', | |
341 help='Descriptive name of the try job') | |
342 parser.add_option('-b', '--bot', | |
343 help=('IMPORTANT: specify ONE builder per run is supported.' | |
344 'Run script for each builders separately.')) | |
345 parser.add_option('-r', '--revision', | |
346 help=('Revision to use for the try job; default: the ' | |
347 'revision will be determined by the try server; see ' | |
348 'its waterfall 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(argv): | |
358 parser = _GenParser() | |
359 options, _ = parser.parse_args() | |
360 if not options.host: | |
361 raise ServerAccessError('Please use the --host option to specify the try ' | |
362 'server host to connect to.') | |
363 if not options.port: | |
364 raise ServerAccessError('Please use the --port option to specify the try ' | |
365 'server port to connect to.') | |
366 params = _GetQueryParams(options) | |
367 PostTryJob(params) | |
368 | |
369 | |
370 if __name__ == '__main__': | |
371 sys.exit(Main(sys.argv)) | |
372 | |
OLD | NEW |