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 7import getpass 8import json 9import optparse 10import os 11import sys 12import urllib 13import urllib2 14 15# Link to get JSON data of builds 16BUILDER_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 20BUILDER_HTML_URL = ('%(server_url)s/builders/%(bot_name)s/builds/%(build_num)s') 21 22# Tryserver buildbots status page 23TRY_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. 27BISECT_BUILDER_HOST = 'master4.golo.chromium.org' 28# 'try_job_port' on tryserver to post build request. 29BISECT_BUILDER_PORT = 8341 30 31 32# From buildbot.status.builder. 33# See: http://docs.buildbot.net/current/developer/results.html 34SUCCESS, WARNINGS, FAILURE, SKIPPED, EXCEPTION, RETRY, TRYPENDING = range(7) 35 36# Status codes that can be returned by the GetBuildStatus method. 37OK = (SUCCESS, WARNINGS) 38# Indicates build failure. 39FAILED = (FAILURE, EXCEPTION, SKIPPED) 40# Inidcates build in progress or in pending queue. 41PENDING = (RETRY, TRYPENDING) 42 43 44class ServerAccessError(Exception): 45 46 def __str__(self): 47 return '%s\nSorry, cannot connect to server.' % self.args[0] 48 49 50def 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 99def _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 117def _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 138def _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 164def _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 188def _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 203def _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 224def 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 260def 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 293def _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 320def _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 357def 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 370if __name__ == '__main__': 371 sys.exit(Main(sys.argv)) 372 373