 0f1a96a360
			
		
	
	0f1a96a360
	
	
	
		
			
			BUG= TEST= Review URL: https://webrtc-codereview.appspot.com/421003 git-svn-id: http://webrtc.googlecode.com/svn/trunk@1810 4adac7df-926f-26a2-2b94-8c16560cd09d
		
			
				
	
	
		
			172 lines
		
	
	
		
			6.3 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			172 lines
		
	
	
		
			6.3 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| #!/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:
 | |
|         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()
 | |
| 
 |