#!/usr/bin/env python #-*- coding: utf-8 -*- # Copyright (c) 2012 The WebRTC project authors. All Rights Reserved. # # Use of this source code is governed by a BSD-style license # that can be found in the LICENSE file in the root of the source # tree. An additional intellectual property rights grant can be found # in the file PATENTS. All contributing project authors may # be found in the AUTHORS file in the root of the source tree. """Implements a handler for adding build status data.""" import datetime import logging from google.appengine.ext import db import oauth_post_request_handler VALID_STATUSES = ['OK', 'failed', 'building', 'warnings'] class OrphanedBuildStatusesExistException(Exception): pass class BuildStatusRoot(db.Model): """Exists solely to be the root parent for all build status data and to keep track of when the last update was made. Since all build status data will refer to this as their parent, we can run transactions on the build status data as a whole. """ last_updated_at = db.DateTimeProperty() class BuildStatusData(db.Model): """This represents one build status report from the build bot.""" bot_name = db.StringProperty(required=True) revision = db.IntegerProperty(required=True) build_number = db.IntegerProperty(required=True) status = db.StringProperty(required=True) def _ensure_build_status_root_exists(): root = db.GqlQuery('SELECT * FROM BuildStatusRoot').get() if not root: # Create a new root, but ensure we don't have any orphaned build statuses # (in that case, we would not have a single entity group as we desire). orphans = db.GqlQuery('SELECT * FROM BuildStatusData').get() if orphans: raise OrphanedBuildStatusesExistException('Parent is gone and there are ' 'orphaned build statuses in ' 'the database!') root = BuildStatusRoot() root.put() return root def _filter_oauth_parameters(post_keys): return filter(lambda post_key: not post_key.startswith('oauth_'), post_keys) def _parse_status(build_number_and_status): parsed_status = build_number_and_status.split('--') if len(parsed_status) != 2: raise ValueError('Malformed status string %s.' % build_number_and_status) parsed_build_number = int(parsed_status[0]) status = parsed_status[1] if status not in VALID_STATUSES: raise ValueError('Invalid status in %s.' % build_number_and_status) return (parsed_build_number, status) def _parse_name(revision_and_bot_name): parsed_name = revision_and_bot_name.split('--') if len(parsed_name) != 2: raise ValueError('Malformed name string %s.' % revision_and_bot_name) revision = parsed_name[0] bot_name = parsed_name[1] if '\n' in bot_name: raise ValueError('Bot name %s can not contain newlines.' % bot_name) return (int(revision), bot_name) def _delete_all_with_revision(revision, build_status_root): query_result = db.GqlQuery('SELECT * FROM BuildStatusData ' 'WHERE revision = :1 AND ANCESTOR IS :2', revision, build_status_root) for entry in query_result: entry.delete() class AddBuildStatusData(oauth_post_request_handler.OAuthPostRequestHandler): """Used to report build status data. Build status data is reported as a POST request. The POST request, aside from the required oauth_* parameters should contain name-value entries that abide by the following rules: 1) The name should be on the form --, for instance 1568--Win32Release. 2) The value should be on the form --, for instance 553--OK, 554--building. The status is permitted to be failed, OK or building. Data is keyed by revision. This handler will delete all data from a revision if data with that revision is present in the current update, since we assume that more recent data is always better data. We also assume that an update always has complete information on a revision (e.g. the status for all the bots are reported in each update). In particular the revision arrangement solves the problem when the latest revision reports 'building' for a bot. Had we not deleted the old revision we would first store a 'building' status for that bot and revision, and later store a 'OK' or 'failed' status for that bot and revision. This is undesirable since we don't want multiple statuses for one bot-revision combination. Now we will effectively update the bot's status instead. """ def _parse_and_store_data(self): build_status_root = _ensure_build_status_root_exists() build_status_data = _filter_oauth_parameters(self.request.arguments()) db.run_in_transaction(self._parse_and_store_data_in_transaction, build_status_root, build_status_data) def _parse_and_store_data_in_transaction(self, build_status_root, build_status_data): encountered_revisions = set() for revision_and_bot_name in build_status_data: build_number_and_status = self.request.get(revision_and_bot_name) try: (build_number, status) = _parse_status(build_number_and_status) (revision, bot_name) = _parse_name(revision_and_bot_name) except ValueError as error: logging.warn('Invalid parameter in request: %s.' % error) self.response.set_status(400) return if revision not in encountered_revisions: # There's new data on this revision in this update, so clear all status # entries with that revision. Only do this once when we first encounter # the revision. _delete_all_with_revision(revision, build_status_root) encountered_revisions.add(revision) # Finally, write the item. item = BuildStatusData(parent=build_status_root, bot_name=bot_name, revision=revision, build_number=build_number, status=status) item.put() request_posix_timestamp = float(self.request.get('oauth_timestamp')) request_datetime = datetime.datetime.fromtimestamp(request_posix_timestamp) build_status_root.last_updated_at = request_datetime build_status_root.put()