| #!/usr/bin/env python |
| # Copyright (c) 2018 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. |
| |
| """Script that triggers and waits for tasks on android-compile.skia.org""" |
| |
| import base64 |
| import hashlib |
| import json |
| import optparse |
| import requests |
| import sys |
| import time |
| |
| |
| ANDROID_COMPILE_HOST = "https://android-compile.skia.org" |
| ANDROID_COMPILE_REGISTER_POST_URI = ANDROID_COMPILE_HOST + "/_/register" |
| ANDROID_COMPILE_TASK_STATUS_URI = ANDROID_COMPILE_HOST + "/get_task_status" |
| GCE_WEBHOOK_SALT_METADATA_URI = ( |
| "http://metadata/computeMetadata/v1/project/attributes/" |
| "ac_webhook_request_salt") |
| |
| |
| POLLING_FREQUENCY_SECS = 60 # 1 minute. |
| DEADLINE_SECS = 60 * 60 # 60 minutes. |
| |
| INFRA_FAILURE_ERROR_MSG = ( |
| '\n\n' |
| 'Your run failed due to infra failures. ' |
| 'It could be due to any of the following:\n\n' |
| '* Need to rebase\n\n' |
| '* Failure when running "python -c from gn import gn_to_bp"\n\n' |
| '* Problem with syncing Android repository.\n\n' |
| 'See go/skia-android-framework-compile-bot-cloud-logs-errors for the ' |
| 'compile server\'s logs.' |
| ) |
| |
| |
| class AndroidCompileException(Exception): |
| pass |
| |
| |
| def _CreateTaskJSON(options): |
| """Creates a JSON representation of the requested task.""" |
| params = {} |
| params["issue"] = options.issue |
| params["patchset"] = options.patchset |
| params["hash"] = options.hash |
| return json.dumps(params) |
| |
| |
| def _GetWebhookSaltFromMetadata(): |
| """Gets webhook_request_salt from GCE's metadata server.""" |
| headers = {"Metadata-Flavor": "Google"} |
| resp = requests.get(GCE_WEBHOOK_SALT_METADATA_URI, headers=headers) |
| if resp.status_code != 200: |
| raise AndroidCompileException( |
| 'Return code from %s was %s' % (GCE_WEBHOOK_SALT_METADATA_URI, |
| resp.status_code)) |
| return base64.standard_b64decode(resp.text) |
| |
| |
| def _GetAuthHeaders(data, options): |
| m = hashlib.sha512() |
| if data: |
| m.update(data) |
| m.update('notverysecret' if options.local else _GetWebhookSaltFromMetadata()) |
| encoded = base64.standard_b64encode(m.digest()) |
| return { |
| "Content-type": "application/x-www-form-urlencoded", |
| "Accept": "application/json", |
| "X-Webhook-Auth-Hash": encoded} |
| |
| |
| def _TriggerTask(options): |
| """Triggers the task on Android Compile and returns the new task's ID.""" |
| task = _CreateTaskJSON(options) |
| headers = _GetAuthHeaders(task, options) |
| resp = requests.post(ANDROID_COMPILE_REGISTER_POST_URI, task, headers=headers) |
| |
| if resp.status_code != 200: |
| raise AndroidCompileException( |
| 'Return code from %s was %s' % (ANDROID_COMPILE_REGISTER_POST_URI, |
| resp.status_code)) |
| try: |
| ret = json.loads(resp.text) |
| except ValueError, e: |
| raise AndroidCompileException( |
| 'Did not get a JSON response from %s: %s' % ( |
| ANDROID_COMPILE_REGISTER_POST_URI, e)) |
| return ret["taskID"] |
| |
| |
| def TriggerAndWait(options): |
| task_id = _TriggerTask(options) |
| task_str = '[id: %d, issue: %d, patchset: %d, hash: %s]' % ( |
| task_id, options.issue, options.patchset, options.hash) |
| |
| print |
| print 'Task %s has been successfully scheduled on %s.' % ( |
| task_str, ANDROID_COMPILE_HOST) |
| print |
| print 'The server will be polled every %d seconds.' % POLLING_FREQUENCY_SECS |
| print |
| |
| headers = _GetAuthHeaders('', options) |
| # Now poll the server till the task completes or till deadline is hit. |
| time_started_polling = time.time() |
| while True: |
| if (time.time() - time_started_polling) > DEADLINE_SECS: |
| raise AndroidCompileException( |
| 'Task did not complete in the deadline of %s seconds.' % ( |
| DEADLINE_SECS)) |
| |
| # Get the status of the task the trybot added. |
| get_url = '%s?task=%s' % (ANDROID_COMPILE_TASK_STATUS_URI, task_id) |
| resp = requests.get(get_url, headers=headers) |
| if resp.status_code != 200: |
| raise AndroidCompileException( |
| 'Return code from %s was %s' % (ANDROID_COMPILE_TASK_STATUS_URI, |
| resp.status_code)) |
| try: |
| ret = json.loads(resp.text) |
| except ValueError, e: |
| raise AndroidCompileException( |
| 'Did not get a JSON response from %s: %s' % (get_url, e)) |
| |
| if ret["infra_failure"]: |
| raise AndroidCompileException(INFRA_FAILURE_ERROR_MSG) |
| |
| if ret["done"]: |
| print |
| print |
| if not ret.get("is_master_branch", True): |
| print 'The Android Framework Compile bot only works for patches and' |
| print 'hashes from the master branch.' |
| print |
| return 0 |
| elif ret["withpatch_success"]: |
| print 'Your run was successfully completed.' |
| print |
| print 'With patch logs are here: %s' % ret["withpatch_log"] |
| print |
| return 0 |
| elif ret["nopatch_success"]: |
| raise AndroidCompileException('The build with the patch failed and the ' |
| 'build without the patch succeeded. This means that the patch ' |
| 'causes Android to fail compilation.\n\n' |
| 'With patch logs are here: %s\n\n' |
| 'No patch logs are here: %s\n\n' % ( |
| ret["withpatch_log"], ret["nopatch_log"])) |
| else: |
| print ('Both with patch and no patch builds failed. This means that the' |
| ' Android tree is currently broken. Marking this bot as ' |
| 'successful') |
| print |
| print 'With patch logs are here: %s' % ret["withpatch_log"] |
| print 'No patch logs are here: %s' % ret["nopatch_log"] |
| return 0 |
| |
| print '.' |
| time.sleep(POLLING_FREQUENCY_SECS) |
| |
| |
| def main(): |
| option_parser = optparse.OptionParser() |
| option_parser.add_option( |
| '', '--issue', type=int, default=0, |
| help='The Gerrit change number to get the patch from.') |
| option_parser.add_option( |
| '', '--patchset', type=int, default=0, |
| help='The Gerrit change patchset to use.') |
| option_parser.add_option( |
| '', '--hash', type=str, default='', |
| help='The Skia repo hash to compile against.') |
| option_parser.add_option( |
| '', '--local', default=False, action='store_true', |
| help='Uses a dummy metadata salt if this flag is true else it tries to ' |
| 'get the salt from GCE metadata.') |
| options, _ = option_parser.parse_args() |
| sys.exit(TriggerAndWait(options)) |
| |
| |
| if __name__ == '__main__': |
| main() |