Add script that triggers a task on CTFE and then waits for its completion or times out

This script will be called from a new CT trybot recipe.

BUG=skia:4341
NOTRY=true

Review URL: https://codereview.chromium.org/1370523002
diff --git a/ct/py/trigger_wait_ct_task.py b/ct/py/trigger_wait_ct_task.py
new file mode 100644
index 0000000..fec4e22
--- /dev/null
+++ b/ct/py/trigger_wait_ct_task.py
@@ -0,0 +1,173 @@
+#!/usr/bin/env python
+# Copyright (c) 2015 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.
+
+"""Python utility that triggers and waits for tasks to complete on CTFE."""
+
+import base64
+import hashlib
+import json
+import optparse
+import requests
+import sys
+import time
+
+
+CTFE_HOST = "http://ct.skia.org"
+CTFE_QUEUE = CTFE_HOST + '/queue/'
+CHROMIUM_PERF_TASK_POST_URI = CTFE_HOST + "/_/webhook_add_chromium_perf_task"
+GET_CHROMIUM_PERF_RUN_STATUS_URI = CTFE_HOST + "/get_chromium_perf_run_status"
+CHROMIUM_PERF_RUNS_HISTORY = CTFE_HOST + "/chromium_perf_runs/"
+GCE_WEBHOOK_SALT_METADATA_URI = (
+    "http://metadata/computeMetadata/v1/project/attributes/"
+    "webhook_request_salt")
+
+
+POLLING_FREQUENCY_SECS = 60  # 1 minute.
+TRYBOT_DEADLINE_SECS = 24 * 60 * 60  # 24 hours.
+
+
+class CtTrybotException(Exception):
+  pass
+
+
+def _CreateTaskJSON(options):
+  """Creates a JSON representation of the requested task."""
+  task_params = {}
+  task_params["username"] = options.requester
+  task_params["benchmark"] = options.benchmark
+  task_params["platform"] = "Linux"
+  task_params["page_sets"] = "10k"
+  task_params["repeat_runs"] = "3"
+  task_params["benchmark_args"] = "--output-format=csv-pivot-table"
+  task_params["browser_args_nopatch"] = (
+      "--disable-setuid-sandbox --enable-threaded-compositing "
+      "--enable-impl-side-painting")
+  task_params["browser_args_withpatch"] = (
+      "--disable-setuid-sandbox --enable-threaded-compositing "
+      "--enable-impl-side-painting")
+
+  trybot_params = {}
+  trybot_params["issue"] = options.issue
+  trybot_params["patchset"] = options.patchset
+  trybot_params["task"] = task_params
+  return json.dumps(trybot_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 CtTrybotException(
+          'Return code from %s was %s' % (GCE_WEBHOOK_SALT_METADATA_URI,
+                                          resp.status_code))
+  return resp.text
+
+
+def _TriggerTask(options):
+  """Triggers the requested task on CTFE and returns the new task's ID."""
+  task = _CreateTaskJSON(options)
+  m = hashlib.sha512()
+  m.update(task)
+  m.update('notverysecret' if options.local else _GetWebhookSaltFromMetadata())
+  encoded = base64.standard_b64encode(m.digest())
+
+  headers = {
+      "Content-type": "application/x-www-form-urlencoded",
+      "Accept": "application/json",
+      "X-Webhook-Auth-Hash": encoded}
+  resp = requests.post(CHROMIUM_PERF_TASK_POST_URI, task, headers=headers)
+
+  if resp.status_code != 200:
+    raise CtTrybotException(
+        'Return code from %s was %s' % (CHROMIUM_PERF_TASK_POST_URI,
+                                        resp.status_code))
+  try:
+    ret = json.loads(resp.text)
+  except ValueError, e:
+    raise CtTrybotException(
+        'Did not get a JSON response from %s: %s' % (
+            CHROMIUM_PERF_TASK_POST_URI, e))
+  return ret["taskID"]
+
+
+def TriggerAndWait(options):
+  task_id = _TriggerTask(options)
+
+  print
+  print 'Task %s has been successfull scheduled on CTFE (%s).' % (
+      task_id, CHROMIUM_PERF_RUNS_HISTORY)
+  print 'You will get an email once the task has been picked up by the server.'
+  print
+  print
+
+  # Now poll CTFE till the task completes or till deadline is hit.
+  time_started_polling = time.time()
+  while True:
+    if (time.time() - time_started_polling) > TRYBOT_DEADLINE_SECS:
+      raise CtTrybotException(
+          'Task did not complete in the deadline of %s seconds.' % (
+              TRYBOT_DEADLINE_SECS))
+
+    # Get the status of the task the trybot added.
+    get_url = '%s?task_id=%s' % (GET_CHROMIUM_PERF_RUN_STATUS_URI, task_id)
+    resp = requests.get(get_url)
+    if resp.status_code != 200:
+      raise CtTrybotException(
+          'Return code from %s was %s' % (GET_CHROMIUM_PERF_RUN_STATUS_URI,
+                                          resp.status_code))
+    try:
+      ret = json.loads(resp.text)
+    except ValueError, e:
+      raise CtTrybotException(
+          'Did not get a JSON response from %s: %s' % (get_url, e))
+    # Assert that the status is for the task we asked for.
+    assert int(ret["taskID"]) == int(task_id)
+
+    status = ret["status"]
+    if status == "Completed":
+      print
+      print ('Your run was successfully completed. Please check your email for '
+             'results of the run.')
+      print
+      return 0
+    elif status == "Completed with failures":
+      print
+      raise CtTrybotException(
+          'Your run was completed with failures. Please check your email for '
+          'links to logs of the run.')
+
+    print ('The current status of the task %s is "%s". You can view the size '
+           'of the queue here: %s' % (task_id, status, CTFE_QUEUE))
+    print 'Checking again after %s seconds' % POLLING_FREQUENCY_SECS
+    print
+    time.sleep(POLLING_FREQUENCY_SECS)
+
+
+if '__main__' == __name__:
+  option_parser = optparse.OptionParser()
+  option_parser.add_option(
+      '', '--issue',
+      help='The Rietveld CL number to get the patch from.')
+  option_parser.add_option(
+      '', '--patchset',
+      help='The Rietveld CL patchset to use.')
+  option_parser.add_option(
+      '', '--requester',
+      help='Email address of the user who requested this run.')
+  option_parser.add_option(
+      '', '--benchmark',
+      help='The CT benchmark to run on the patch.')
+  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, unused_args = option_parser.parse_args()
+  if (not options.issue or not options.patchset or not options.requester
+      or not options.benchmark):
+    option_parser.error('Must specify issue, patchset, requester and benchmark')
+
+  sys.exit(TriggerAndWait(options))
+