blob: 106d1781d4a8540f2b8090722e8847703845c03c [file] [log] [blame]
#!/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."""
__author__ = 'phoglund@webrtc.org (Patrik Höglund)'
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 <revision>--<bot name>, for instance
1568--Win32Release.
2) The value should be on the form <build number>--<status>, 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:
logger.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()