| 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 |