blob: d4f8137c3c97d95486f5dc3fa96623bf7608ef0e [file] [log] [blame]
#!/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()