Chromium Code Reviews| Index: Tools/AutoSheriff/buildbot.py |
| diff --git a/Tools/AutoSheriff/buildbot.py b/Tools/AutoSheriff/buildbot.py |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..852ae990d63049561bd80bfe72ec317545040bba |
| --- /dev/null |
| +++ b/Tools/AutoSheriff/buildbot.py |
| @@ -0,0 +1,178 @@ |
| +# Copyright 2014 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. |
| + |
| +import collections |
| +import json |
| +import logging |
| +import operator |
| +import os |
| +import requests |
| +import urlparse |
| +import string_helpers |
| + |
| + |
| +# Python logging is stupidly verbose to configure. |
|
ojan
2014/07/22 02:01:24
Not really a useful comment.
|
| +def setup_logging(): |
| + logger = logging.getLogger(__name__) |
| + logger.setLevel(logging.DEBUG) |
| + handler = logging.StreamHandler() |
| + handler.setLevel(logging.DEBUG) |
| + formatter = logging.Formatter('%(levelname)s: %(message)s') |
| + handler.setFormatter(formatter) |
| + logger.addHandler(handler) |
| + return logger, handler |
| + |
| + |
| +log, logging_handler = setup_logging() |
| + |
| + |
| +CBE_BASE = 'https://chrome-build-extract.appspot.com' |
| + |
| +# Unclear if this should be specific to builds. |
| +class BuildCache(object): |
| + def __init__(self, root_path): |
| + self.root_path = root_path |
| + |
| + # Could be in operator. |
| + def has(self, key): |
| + path = os.path.join(self.root_path, key) |
| + return os.path.exists(path) |
| + |
| + # Could be attr getter. |
| + def get(self, key): |
| + path = os.path.join(self.root_path, key) |
| + if not self.has(path): |
| + return None |
| + with open(path) as cached: |
| + return json.load(cached) |
| + |
| + # Could be attr setter. |
| + def set(self, key, json_object): |
| + path = os.path.join(self.root_path, key) |
| + cache_dir = os.path.dirname(path) |
| + if not os.path.exists(cache_dir): |
| + os.makedirs(cache_dir) |
| + with open(path, 'w') as cached: |
| + cached.write(json.dumps(json_object)) |
| + |
| + |
| +def master_name_from_url(master_url): |
| + return urlparse.urlparse(master_url).path.split('/')[-1] |
| + |
| + |
| +def cache_key_for_build(master_url, builder_name, build_number): |
| + master_name = master_name_from_url(master_url) |
| + return os.path.join(master_name, builder_name, "%s.json" % build_number) |
| + |
| + |
| +def fetch_master_json(master_url): |
| + master_name = master_name_from_url(master_url) |
| + url = '%s/get_master/%s' % (CBE_BASE, master_name) |
| + return requests.get(url).json() |
| + |
| + |
| +def prefill_builds_cache(cache, master_url, builder_name): |
| + master_name = master_name_from_url(master_url) |
| + builds_url = '%s/get_builds' % CBE_BASE |
| + params = { 'master': master_name, 'builder': builder_name } |
| + response = requests.get(builds_url, params=params) |
| + builds = response.json()['builds'] |
| + for build in builds: |
| + if not build.get('number'): |
| + index = builds.index(build) |
| + log.error('build at index %s in %s missing number?' % (index, response.url)) |
| + continue |
| + build_number = build['number'] |
| + key = cache_key_for_build(master_url, builder_name, build_number) |
| + cache.set(key, build) |
| + build_numbers = map(operator.itemgetter('number'), builds) |
| + log.debug('Prefilled (%.1fs) %s for %s %s' % |
| + (response.elapsed.total_seconds(), |
| + string_helpers.re_range(build_numbers), |
| + master_name, builder_name)) |
| + return build_numbers |
| + |
| + |
| +def fetch_and_cache_build(cache, url, cache_key): |
| + log.debug('Fetching %s.' % url) |
| + try: |
| + build = requests.get(url).json() |
| + # Don't cache builds which are just errors? |
| + if build.get('number'): |
| + if build.get('eta') is None: |
| + cache.set(cache_key, build) |
| + else: |
| + log.debug('Not caching in-progress build from %s.') |
| + return build |
| + except ValueError, e: |
| + log.error('Not caching invalid json: %s: %s' % (url, e)) |
| + |
| + |
| +def fetch_build_json(cache, master_url, builder_name, build_number): |
| + cache_key = cache_key_for_build(master_url, builder_name, build_number) |
| + build = cache.get(cache_key) |
| + # I accidentally stored some error builds and incomplete builds before. |
| + if build and (not build.get('number') or build.get('eta')): |
| + log.warn('Refetching %s %s %s' % (master_url, builder_name, build_number)) |
| + build = None |
| + |
| + master_name = master_name_from_url(master_url) |
| + |
| + cbe_url = "https://chrome-build-extract.appspot.com/p/%s/builders/%s/builds/%s?json=1" % ( |
| + master_name, builder_name, build_number) |
| + if not build: |
| + build = fetch_and_cache_build(cache, cbe_url, cache_key) |
| + |
| + if not build: |
| + log.warn("CBE failed, failover to buildbot %s" % cbe_url) |
| + buildbot_url = "https://build.chromium.org/p/%s/json/builders/%s/builds/%s" % ( |
| + master_name, builder_name, build_number) |
| + build = fetch_and_cache_build(cache, buildbot_url, cache_key) |
| + |
| + return build |
| + |
| + |
| +# This effectively extracts the 'configuration' of the build |
| +# we could extend this beyond repo versions in the future. |
| +def revisions_from_build(build_json): |
| + def _property_value(build_json, property_name): |
| + for prop_tuple in build_json['properties']: |
| + if prop_tuple[0] == property_name: |
| + return prop_tuple[1] |
| + |
| + REVISION_VARIABLES = [ |
| + ('chromium', 'got_revision'), |
| + ('blink', 'got_webkit_revision'), |
| + ('v8', 'got_v8_revision'), |
| + ('nacl', 'got_nacl_revision'), |
| + # Skia, for whatever reason, isn't exposed in the buildbot properties so |
| + # don't bother to include it here. |
| + ] |
| + |
| + revisions = {} |
| + for repo_name, buildbot_property in REVISION_VARIABLES: |
| + # This is epicly stupid: 'tester' builders have the wrong |
| + # revision for 'got_foo_revision' and we have to use |
| + # parent_got_foo_revision instead, but non-tester builders |
| + # don't have the parent_ versions, so we have to fall back |
| + # to got_foo_revision in those cases! |
| + # Don't even think about using 'revision' that's wrong too. |
|
ojan
2014/07/22 02:01:24
Lol!
This should perhaps be a FIXME? Theoreticall
|
| + revision = _property_value(build_json, 'parent_' + buildbot_property) |
| + if not revision: |
| + revision = _property_value(build_json, buildbot_property) |
| + revisions[repo_name] = revision |
| + return revisions |
| + |
| + |
| +def latest_revisions_for_master(cache, master_url, master_json): |
| + latest_revisions = collections.defaultdict(dict) |
| + master_name = master_name_from_url(master_url) |
| + for builder_name, builder_json in master_json['builders'].items(): |
| + # recent_builds can include current builds |
| + recent_builds = set(builder_json['cachedBuilds']) |
| + active_builds = set(builder_json['currentBuilds']) |
| + last_finished_id = sorted(recent_builds - active_builds, reverse=True)[0] |
| + last_build = fetch_build_json(cache, master_url, builder_name, last_finished_id) |
| + latest_revisions[master_name][builder_name] = revisions_from_build(last_build) |
| + return latest_revisions |