Index: dashboard/dashboard/post_bisect_results.py |
diff --git a/dashboard/dashboard/post_bisect_results.py b/dashboard/dashboard/post_bisect_results.py |
new file mode 100644 |
index 0000000000000000000000000000000000000000..48207d6632a719b704353ac6f3c1ee7a938e6c81 |
--- /dev/null |
+++ b/dashboard/dashboard/post_bisect_results.py |
@@ -0,0 +1,137 @@ |
+# Copyright 2016 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. |
+ |
+"""URL endpoint to allow bisect bots to post results to the dashboard.""" |
+ |
+import json |
+import logging |
+ |
+from google.appengine.api import app_identity |
+from google.appengine.ext import ndb |
+ |
+from dashboard import bisect_report |
+from dashboard import datastore_hooks |
+from dashboard import post_data_handler |
+from dashboard import quick_logger |
+from dashboard import rietveld_service |
+from dashboard import update_bug_with_results |
+from dashboard.models import try_job |
+ |
+_EXPECTED_RESULT_PROPERTIES = { |
+ 'try_job_id': int, |
+ 'status': ['completed', 'failed', 'pending', 'aborted'], |
+} |
+ |
+ |
+class BadRequestError(Exception): |
+ """An error indicating that a 400 response status should be returned.""" |
+ pass |
+ |
+ |
+class PostBisectResultsHandler(post_data_handler.PostDataHandler): |
+ |
+ def post(self): |
+ """Validates data parameter and saves to TryJob entity. |
+ |
+ Bisect results come from a "data" parameter, which is a JSON encoding of a |
+ dictionary. |
+ |
+ The required fields are "master", "bot", "test". |
+ |
+ Request parameters: |
+ data: JSON encoding of a dictionary. |
+ |
+ Outputs: |
+ Empty 200 response with if successful, |
+ 200 response with warning message if optional data is invalid, |
+ 403 response with error message if sender IP is not white-listed, |
+ 400 response with error message if required data is invalid. |
+ 500 with error message otherwise. |
+ """ |
+ datastore_hooks.SetPrivilegedRequest() |
+ if not self._CheckIpAgainstWhitelist(): |
+ return |
+ |
+ data = self.request.get('data') |
+ if not data: |
+ self.ReportError('Missing "data" parameter.', status=400) |
+ return |
+ |
+ try: |
+ data = json.loads(self.request.get('data')) |
+ except ValueError: |
+ self.ReportError('Invalid JSON string.', status=400) |
+ return |
+ |
+ logging.info('Received data: %s', data) |
+ |
+ try: |
+ _ValidateResultsData(data) |
+ job = _UpdateTryJob(data) |
+ update_bug_with_results.UpdateQuickLog(job) |
+ except BadRequestError as error: |
+ self.ReportError(error.message, status=400) |
+ |
+ |
+def _ValidateResultsData(results_data): |
+ _Validate(_EXPECTED_RESULT_PROPERTIES, results_data) |
+ # TODO(chrisphan): Validate other values. |
+ |
+ |
+def _Validate(expected, actual): |
+ """Generic validator for expected keys, values, and types. |
+ |
+ See post_bisect_results_test.py for examples. |
+ |
+ Args: |
+ expected: Either a list of expected values or a dictionary of expected |
+ keys and type. A dictionary can contain a list of expected values. |
+ actual: A value. |
+ """ |
+ if not expected: |
+ return |
+ expected_type = type(expected) |
+ actual_type = type(actual) |
+ if expected_type is list: |
+ if actual not in expected: |
+ raise BadRequestError('Invalid value. Expected one of the following ' |
+ '%s. Actual %s.' % (','.join(expected), actual)) |
+ elif expected_type is dict: |
+ if actual_type is not dict: |
+ raise BadRequestError('Invalid type. Expected %s, actual %s.' |
+ % (expected_type, actual_type)) |
+ missing = set(expected.keys()) - set(actual.keys()) |
+ if missing: |
+ raise BadRequestError('Missing the following properties: %s' |
+ % ','.join(missing)) |
+ for key in expected: |
+ _Validate(expected[key], actual[key]) |
+ elif type(expected) is type and actual_type is not expected: |
+ raise BadRequestError('Invalid type. Expected %s, actual %s.' % |
+ (expected, actual_type)) |
+ |
+ |
+def _UpdateTryJob(results_data): |
+ try_job_id = results_data.get('try_job_id') |
+ job = ndb.Key(try_job.TryJob, try_job_id).get() |
+ if not job.results_data: |
+ job.results_data = {} |
+ job.results_data.update(results_data) |
+ job.results_data['issue_url'] = (job.results_data.get('issue_url') or |
+ _IssueURL(job)) |
+ job.put() |
+ return job |
+ |
+ |
+def _IssueURL(job): |
+ """Returns a URL for information about a bisect try job.""" |
+ if job.use_buildbucket: |
+ hostname = app_identity.get_default_version_hostname() |
+ job_id = job.buildbucket_job_id |
+ return 'https://%s/buildbucket_job_status/%s' % (hostname, job_id) |
+ else: |
+ config = rietveld_service.GetDefaultRietveldConfig() |
+ host = (config.internal_server_url if job.internal_only else |
+ config.server_url) |
+ return '%s/%d' % (host, job.rietveld_issue_id) |