# Copyright 2014 The Chromium Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. """This module contains functionality for starting build try jobs via HTTP. This includes both sending a request to start a job, and also related code for querying the status of the job. This module can be either run as a stand-alone script to send a request to a builder, or imported and used by calling the public functions below. """ import getpass import json import optparse import os import sys import urllib import urllib2 # URL template for fetching JSON data about builds. BUILDER_JSON_URL = ('%(server_url)s/json/builders/%(bot_name)s/builds/' '%(build_num)s?as_text=1&filter=0') # URL template for displaying build steps. BUILDER_HTML_URL = ('%(server_url)s/builders/%(bot_name)s/builds/%(build_num)s') # Try server status page for the perf try server. PERF_TRY_SERVER_URL = 'http://build.chromium.org/p/tryserver.chromium.perf' # Hostname of the tryserver where perf bisect builders are hosted. # This is used for posting build requests to the tryserver. PERF_BISECT_BUILDER_HOST = 'master4.golo.chromium.org' # The default 'try_job_port' on tryserver to post build request. PERF_BISECT_BUILDER_PORT = 8341 # Status codes that can be returned by the GetBuildStatus method. # From buildbot.status.builder. # See: http://docs.buildbot.net/current/developer/results.html SUCCESS, WARNINGS, FAILURE, SKIPPED, EXCEPTION, RETRY, TRYPENDING = range(7) OK = (SUCCESS, WARNINGS) # These indicate build is complete. FAILED = (FAILURE, EXCEPTION, SKIPPED) # These indicate build failure. PENDING = (RETRY, TRYPENDING) # These indicate in progress or in pending queue. class ServerAccessError(Exception): def __str__(self): return '%s\nSorry, cannot connect to server.' % self.args[0] def PostTryJob(host, port, params): """Sends a build request to the server using the HTTP protocol. The required parameters are: 'revision': "src@rev", where rev is a git hash or SVN revision. 'bot': Name of builder bot to use, e.g. "win_perf_bisect_builder". Args: host: Hostname of the try server. port: Port of the try server. params: A dictionary of parameters to be sent in the POST request. Returns: True if the request is posted successfully. Raises: ServerAccessError: Failed to make a request to the try server. ValueError: Not all of the expected inputs were given. """ if not params.get('revision'): raise ValueError('Missing revision number.') if not params.get('bot'): raise ValueError('Missing bot name.') base_url = 'http://%s:%s/send_try_patch' % (host, port) print 'Posting build request by HTTP.' print 'URL: %s, params: %s' % (base_url, params) connection = None try: print 'Opening connection...' connection = urllib2.urlopen(base_url, urllib.urlencode(params)) print 'Done, request sent to server to produce build.' except IOError as e: raise ServerAccessError('%s is unaccessible. Reason: %s' % (base_url, e)) if not connection: raise ServerAccessError('%s is unaccessible.' % base_url) response = connection.read() print 'Received response from server: %s' % response if response != 'OK': raise ServerAccessError('Response was not "OK".') return True def _IsBuildRunning(build_data): """Checks whether the build is in progress on buildbot. Presence of currentStep element in build JSON indicates build is in progress. Args: build_data: A dictionary with build data, loaded from buildbot JSON API. Returns: True if build is in progress, otherwise False. """ current_step = build_data.get('currentStep') if (current_step and current_step.get('isStarted') and current_step.get('results') is None): return True return False def _IsBuildFailed(build_data): """Checks whether the build failed on buildbot. Sometime build status is marked as failed even though compile and packaging steps are successful. This may happen due to some intermediate steps of less importance such as gclient revert, generate_telemetry_profile are failed. Therefore we do an addition check to confirm if build was successful by calling _IsBuildSuccessful. Args: build_data: A dictionary with build data, loaded from buildbot JSON API. Returns: True if revision is failed build, otherwise False. """ if (build_data.get('results') in FAILED and not _IsBuildSuccessful(build_data)): return True return False def _IsBuildSuccessful(build_data): """Checks whether the build succeeded on buildbot. We treat build as successful if the package_build step is completed without any error i.e., when results attribute of the this step has value 0 or 1 in its first element. Args: build_data: A dictionary with build data, loaded from buildbot JSON API. Returns: True if revision is successfully build, otherwise False. """ if build_data.get('steps'): for item in build_data.get('steps'): # The 'results' attribute of each step consists of two elements, # results[0]: This represents the status of build step. # See: http://docs.buildbot.net/current/developer/results.html # results[1]: List of items, contains text if step fails, otherwise empty. if (item.get('name') == 'package_build' and item.get('isFinished') and item.get('results')[0] in OK): return True return False def _FetchBuilderData(builder_url): """Fetches JSON data for the all the builds from the tryserver. Args: builder_url: A tryserver URL to fetch builds information. Returns: A dictionary with information of all build on the tryserver. """ data = None try: url = urllib2.urlopen(builder_url) except urllib2.URLError as e: print ('urllib2.urlopen error %s, waterfall status page down.[%s]' % ( builder_url, str(e))) return None if url is not None: try: data = url.read() except IOError as e: print 'urllib2 file object read error %s, [%s].' % (builder_url, str(e)) return data def _GetBuildData(buildbot_url): """Gets build information for the given build id from the tryserver. Args: buildbot_url: A tryserver URL to fetch build information. Returns: A dictionary with build information if build exists, otherwise None. """ builds_json = _FetchBuilderData(buildbot_url) if builds_json: return json.loads(builds_json) return None def _GetBuildBotUrl(builder_host, builder_port): """Gets build bot URL for fetching build info. Bisect builder bots are hosted on tryserver.chromium.perf, though we cannot access this tryserver using host and port number directly, so we use another tryserver URL for the perf tryserver. Args: builder_host: Hostname of the server where the builder is hosted. builder_port: Port number of ther server where the builder is hosted. Returns: URL of the buildbot as a string. """ if (builder_host == PERF_BISECT_BUILDER_HOST and builder_port == PERF_BISECT_BUILDER_PORT): return PERF_TRY_SERVER_URL else: return 'http://%s:%s' % (builder_host, builder_port) def GetBuildStatus(build_num, bot_name, builder_host, builder_port): """Gets build status from the buildbot status page for a given build number. Args: build_num: A build number on tryserver to determine its status. bot_name: Name of the bot where the build information is scanned. builder_host: Hostname of the server where the builder is hosted. builder_port: Port number of the server where the builder is hosted. Returns: A pair which consists of build status (SUCCESS, FAILED or PENDING) and a link to build status page on the waterfall. """ results_url = None if build_num: # Get the URL for requesting JSON data with status information. server_url = _GetBuildBotUrl(builder_host, builder_port) buildbot_url = BUILDER_JSON_URL % { 'server_url': server_url, 'bot_name': bot_name, 'build_num': build_num, } build_data = _GetBuildData(buildbot_url) if build_data: # Link to build on the buildbot showing status of build steps. results_url = BUILDER_HTML_URL % { 'server_url': server_url, 'bot_name': bot_name, 'build_num': build_num, } if _IsBuildFailed(build_data): return (FAILED, results_url) elif _IsBuildSuccessful(build_data): return (OK, results_url) return (PENDING, results_url) def GetBuildNumFromBuilder(build_reason, bot_name, builder_host, builder_port): """Gets build number on build status page for a given 'build reason'. This function parses the JSON data from buildbot page and collects basic information about the all the builds, and then uniquely identifies the build based on the 'reason' attribute in builds's JSON data. The 'reason' attribute set is when a build request is posted, and it is used to identify the build on status page. Args: build_reason: A unique build name set to build on tryserver. bot_name: Name of the bot where the build information is scanned. builder_host: Hostname of the server where the builder is hosted. builder_port: Port number of ther server where the builder is hosted. Returns: A build number as a string if found, otherwise None. """ # Gets the buildbot url for the given host and port. server_url = _GetBuildBotUrl(builder_host, builder_port) buildbot_url = BUILDER_JSON_URL % { 'server_url': server_url, 'bot_name': bot_name, 'build_num': '_all', } builds_json = _FetchBuilderData(buildbot_url) if builds_json: builds_data = json.loads(builds_json) for current_build in builds_data: if builds_data[current_build].get('reason') == build_reason: return builds_data[current_build].get('number') return None def _GetRequestParams(options): """Extracts request parameters which will be passed to PostTryJob. Args: options: The options object parsed from the command line. Returns: A dictionary with parameters to pass to PostTryJob. """ params = { 'user': options.user, 'name': options.name, } # Add other parameters if they're available in the options object. for key in ['email', 'revision', 'root', 'bot', 'patch']: option = getattr(options, key) if option: params[key] = option return params def _GenParser(): """Returns a parser for getting command line arguments.""" usage = ('%prog [options]\n' 'Post a build request to the try server for the given revision.') parser = optparse.OptionParser(usage=usage) parser.add_option('-H', '--host', help='Host address of the try server (required).') parser.add_option('-P', '--port', type='int', help='HTTP port of the try server (required).') parser.add_option('-u', '--user', default=getpass.getuser(), dest='user', help='Owner user name [default: %default]') parser.add_option('-e', '--email', default=os.environ.get('TRYBOT_RESULTS_EMAIL_ADDRESS', os.environ.get('EMAIL_ADDRESS')), help=('Email address where to send the results. Use either ' 'the TRYBOT_RESULTS_EMAIL_ADDRESS environment ' 'variable or EMAIL_ADDRESS to set the email address ' 'the try bots report results to [default: %default]')) parser.add_option('-n', '--name', default='try_job_http', help='Descriptive name of the try job') parser.add_option('-b', '--bot', help=('Only one builder per run may be specified; to post ' 'jobs on on multiple builders, run script for each ' 'builder separately.')) parser.add_option('-r', '--revision', help=('Revision to use for the try job. The revision may ' 'be determined by the try server; see its waterfall ' 'for more info.')) parser.add_option('--root', help=('Root to use for the patch; base subdirectory for ' 'patch created in a subdirectory.')) parser.add_option('--patch', help='Patch information.') return parser def Main(_): """Posts a try job based on command line parameters.""" parser = _GenParser() options, _ = parser.parse_args() if not options.host or not options.port: parser.print_help() return 1 params = _GetRequestParams(options) PostTryJob(options.host, options.port, params) if __name__ == '__main__': sys.exit(Main(sys.argv))