1#!/usr/bin/env python 2# Copyright (c) 2018 The Chromium Authors. All rights reserved. 3# Use of this source code is governed by a BSD-style license that can be 4# found in the LICENSE file. 5 6"""Script that triggers and waits for tasks on android-compile.skia.org""" 7 8import base64 9import hashlib 10import json 11import optparse 12import requests 13import sys 14import time 15 16 17ANDROID_COMPILE_HOST = "https://android-compile.skia.org" 18ANDROID_COMPILE_REGISTER_POST_URI = ANDROID_COMPILE_HOST + "/_/register" 19ANDROID_COMPILE_TASK_STATUS_URI = ANDROID_COMPILE_HOST + "/get_task_status" 20GCE_WEBHOOK_SALT_METADATA_URI = ( 21 "http://metadata/computeMetadata/v1/project/attributes/" 22 "ac_webhook_request_salt") 23 24 25POLLING_FREQUENCY_SECS = 60 # 1 minute. 26DEADLINE_SECS = 30 * 60 # 30 minutes. 27 28 29class AndroidCompileException(Exception): 30 pass 31 32 33def _CreateTaskJSON(options): 34 """Creates a JSON representation of the requested task.""" 35 params = {} 36 params["issue"] = options.issue 37 params["patchset"] = options.patchset 38 params["hash"] = options.hash 39 return json.dumps(params) 40 41 42def _GetWebhookSaltFromMetadata(): 43 """Gets webhook_request_salt from GCE's metadata server.""" 44 headers = {"Metadata-Flavor": "Google"} 45 resp = requests.get(GCE_WEBHOOK_SALT_METADATA_URI, headers=headers) 46 if resp.status_code != 200: 47 raise AndroidCompileException( 48 'Return code from %s was %s' % (GCE_WEBHOOK_SALT_METADATA_URI, 49 resp.status_code)) 50 return base64.standard_b64decode(resp.text) 51 52 53def _GetAuthHeaders(data, options): 54 m = hashlib.sha512() 55 if data: 56 m.update(data) 57 m.update('notverysecret' if options.local else _GetWebhookSaltFromMetadata()) 58 encoded = base64.standard_b64encode(m.digest()) 59 return { 60 "Content-type": "application/x-www-form-urlencoded", 61 "Accept": "application/json", 62 "X-Webhook-Auth-Hash": encoded} 63 64 65def _TriggerTask(options): 66 """Triggers the task on Android Compile and returns the new task's ID.""" 67 task = _CreateTaskJSON(options) 68 headers = _GetAuthHeaders(task, options) 69 resp = requests.post(ANDROID_COMPILE_REGISTER_POST_URI, task, headers=headers) 70 71 if resp.status_code != 200: 72 raise AndroidCompileException( 73 'Return code from %s was %s' % (ANDROID_COMPILE_REGISTER_POST_URI, 74 resp.status_code)) 75 try: 76 ret = json.loads(resp.text) 77 except ValueError, e: 78 raise AndroidCompileException( 79 'Did not get a JSON response from %s: %s' % ( 80 ANDROID_COMPILE_REGISTER_POST_URI, e)) 81 return ret["taskID"] 82 83 84def TriggerAndWait(options): 85 task_id = _TriggerTask(options) 86 task_str = '[id: %d, issue: %d, patchset: %d, hash: %s]' % ( 87 task_id, options.issue, options.patchset, options.hash) 88 89 print 90 print 'Task %s has been successfully scheduled on %s.' % ( 91 task_str, ANDROID_COMPILE_HOST) 92 print 93 print 'The server will be polled every %d seconds.' % POLLING_FREQUENCY_SECS 94 print 95 96 headers = _GetAuthHeaders('', options) 97 # Now poll the server till the task completes or till deadline is hit. 98 time_started_polling = time.time() 99 while True: 100 if (time.time() - time_started_polling) > DEADLINE_SECS: 101 raise AndroidCompileException( 102 'Task did not complete in the deadline of %s seconds.' % ( 103 DEADLINE_SECS)) 104 105 # Get the status of the task the trybot added. 106 get_url = '%s?task=%s' % (ANDROID_COMPILE_TASK_STATUS_URI, task_id) 107 resp = requests.get(get_url, headers=headers) 108 if resp.status_code != 200: 109 raise AndroidCompileException( 110 'Return code from %s was %s' % (ANDROID_COMPILE_TASK_STATUS_URI, 111 resp.status_code)) 112 try: 113 ret = json.loads(resp.text) 114 except ValueError, e: 115 raise AndroidCompileException( 116 'Did not get a JSON response from %s: %s' % (get_url, e)) 117 118 if ret["infra_failure"]: 119 raise AndroidCompileException( 120 'Your run failed due to infra failures. It could be due to needing ' 121 'to rebase or something else.') 122 123 if ret["done"]: 124 print 125 print 126 if not ret.get("is_master_branch", True): 127 print 'The Android Framework Compile bot only works for patches and' 128 print 'hashes from the master branch.' 129 print 130 return 0 131 elif ret["withpatch_success"]: 132 print 'Your run was successfully completed.' 133 print 134 print 'With patch logs are here: %s' % ret["withpatch_log"] 135 print 136 return 0 137 elif ret["nopatch_success"]: 138 raise AndroidCompileException('The build with the patch failed and the ' 139 'build without the patch succeeded. This means that the patch ' 140 'causes Android to fail compilation.\n\n' 141 'With patch logs are here: %s\n\n' 142 'No patch logs are here: %s\n\n' % ( 143 ret["withpatch_log"], ret["nopatch_log"])) 144 else: 145 print ('Both with patch and no patch builds failed. This means that the' 146 ' Android tree is currently broken. Marking this bot as ' 147 'successful') 148 print 149 print 'With patch logs are here: %s' % ret["withpatch_log"] 150 print 'No patch logs are here: %s' % ret["nopatch_log"] 151 return 0 152 153 print '.' 154 time.sleep(POLLING_FREQUENCY_SECS) 155 156 157def main(): 158 option_parser = optparse.OptionParser() 159 option_parser.add_option( 160 '', '--issue', type=int, default=0, 161 help='The Gerrit change number to get the patch from.') 162 option_parser.add_option( 163 '', '--patchset', type=int, default=0, 164 help='The Gerrit change patchset to use.') 165 option_parser.add_option( 166 '', '--hash', type=str, default='', 167 help='The Skia repo hash to compile against.') 168 option_parser.add_option( 169 '', '--local', default=False, action='store_true', 170 help='Uses a dummy metadata salt if this flag is true else it tries to ' 171 'get the salt from GCE metadata.') 172 options, _ = option_parser.parse_args() 173 sys.exit(TriggerAndWait(options)) 174 175 176if __name__ == '__main__': 177 main() 178