2012-02-01 11:59:23 +01:00
|
|
|
#!/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)'
|
|
|
|
|
2012-02-06 11:55:12 +01:00
|
|
|
import datetime
|
2012-02-27 16:42:25 +01:00
|
|
|
import logging
|
2012-02-06 11:55:12 +01:00
|
|
|
|
2012-02-01 11:59:23 +01:00
|
|
|
from google.appengine.ext import db
|
|
|
|
|
|
|
|
import oauth_post_request_handler
|
|
|
|
|
2012-02-27 16:42:25 +01:00
|
|
|
VALID_STATUSES = ['OK', 'failed', 'building', 'warnings']
|
2012-02-01 11:59:23 +01:00
|
|
|
|
2012-02-01 18:08:03 +01:00
|
|
|
|
|
|
|
class OrphanedBuildStatusesExistException(Exception):
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
class BuildStatusRoot(db.Model):
|
2012-02-06 11:55:12 +01:00
|
|
|
"""Exists solely to be the root parent for all build status data and to keep
|
|
|
|
track of when the last update was made.
|
2012-02-01 18:08:03 +01:00
|
|
|
|
|
|
|
Since all build status data will refer to this as their parent,
|
|
|
|
we can run transactions on the build status data as a whole.
|
|
|
|
"""
|
2012-02-06 11:55:12 +01:00
|
|
|
last_updated_at = db.DateTimeProperty()
|
2012-02-01 11:59:23 +01:00
|
|
|
|
|
|
|
|
|
|
|
class BuildStatusData(db.Model):
|
|
|
|
"""This represents one build status report from the build bot."""
|
|
|
|
bot_name = db.StringProperty(required=True)
|
2012-02-01 18:08:03 +01:00
|
|
|
revision = db.IntegerProperty(required=True)
|
2012-02-01 11:59:23 +01:00
|
|
|
build_number = db.IntegerProperty(required=True)
|
2012-02-01 18:08:03 +01:00
|
|
|
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
|
2012-02-01 11:59:23 +01:00
|
|
|
|
|
|
|
|
|
|
|
def _filter_oauth_parameters(post_keys):
|
|
|
|
return filter(lambda post_key: not post_key.startswith('oauth_'),
|
|
|
|
post_keys)
|
|
|
|
|
|
|
|
|
2012-02-01 18:08:03 +01:00
|
|
|
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]
|
2012-02-27 16:42:25 +01:00
|
|
|
if '\n' in bot_name:
|
|
|
|
raise ValueError('Bot name %s can not contain newlines.' % bot_name)
|
2012-02-01 18:08:03 +01:00
|
|
|
|
|
|
|
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()
|
|
|
|
|
|
|
|
|
2012-02-01 11:59:23 +01:00
|
|
|
class AddBuildStatusData(oauth_post_request_handler.OAuthPostRequestHandler):
|
2012-02-01 18:08:03 +01:00
|
|
|
"""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.
|
|
|
|
"""
|
2012-02-01 11:59:23 +01:00
|
|
|
|
2012-02-06 11:55:12 +01:00
|
|
|
def _parse_and_store_data(self):
|
2012-02-01 18:08:03 +01:00
|
|
|
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):
|
2012-02-06 11:55:12 +01:00
|
|
|
|
2012-02-01 18:08:03 +01:00
|
|
|
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:
|
2012-02-27 16:42:25 +01:00
|
|
|
logger.warn('Invalid parameter in request: %s.' % error)
|
|
|
|
self.response.set_status(400)
|
|
|
|
return
|
2012-02-01 18:08:03 +01:00
|
|
|
|
|
|
|
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)
|
2012-02-01 11:59:23 +01:00
|
|
|
item.put()
|
2012-02-06 11:55:12 +01:00
|
|
|
|
|
|
|
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()
|
2012-02-27 16:42:25 +01:00
|
|
|
|