• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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