OLD | NEW |
---|---|
(Empty) | |
1 # Copyright 2014 The Chromium Authors. All rights reserved. | |
2 # Use of this source code is governed by a BSD-style license that can be | |
3 # found in the LICENSE file. | |
4 | |
5 import collections | |
6 import json | |
7 import logging | |
8 import operator | |
9 import os | |
10 import requests | |
11 import urlparse | |
12 import string_helpers | |
13 | |
14 | |
15 # Python logging is stupidly verbose to configure. | |
ojan
2014/07/22 02:01:24
Not really a useful comment.
| |
16 def setup_logging(): | |
17 logger = logging.getLogger(__name__) | |
18 logger.setLevel(logging.DEBUG) | |
19 handler = logging.StreamHandler() | |
20 handler.setLevel(logging.DEBUG) | |
21 formatter = logging.Formatter('%(levelname)s: %(message)s') | |
22 handler.setFormatter(formatter) | |
23 logger.addHandler(handler) | |
24 return logger, handler | |
25 | |
26 | |
27 log, logging_handler = setup_logging() | |
28 | |
29 | |
30 CBE_BASE = 'https://chrome-build-extract.appspot.com' | |
31 | |
32 # Unclear if this should be specific to builds. | |
33 class BuildCache(object): | |
34 def __init__(self, root_path): | |
35 self.root_path = root_path | |
36 | |
37 # Could be in operator. | |
38 def has(self, key): | |
39 path = os.path.join(self.root_path, key) | |
40 return os.path.exists(path) | |
41 | |
42 # Could be attr getter. | |
43 def get(self, key): | |
44 path = os.path.join(self.root_path, key) | |
45 if not self.has(path): | |
46 return None | |
47 with open(path) as cached: | |
48 return json.load(cached) | |
49 | |
50 # Could be attr setter. | |
51 def set(self, key, json_object): | |
52 path = os.path.join(self.root_path, key) | |
53 cache_dir = os.path.dirname(path) | |
54 if not os.path.exists(cache_dir): | |
55 os.makedirs(cache_dir) | |
56 with open(path, 'w') as cached: | |
57 cached.write(json.dumps(json_object)) | |
58 | |
59 | |
60 def master_name_from_url(master_url): | |
61 return urlparse.urlparse(master_url).path.split('/')[-1] | |
62 | |
63 | |
64 def cache_key_for_build(master_url, builder_name, build_number): | |
65 master_name = master_name_from_url(master_url) | |
66 return os.path.join(master_name, builder_name, "%s.json" % build_number) | |
67 | |
68 | |
69 def fetch_master_json(master_url): | |
70 master_name = master_name_from_url(master_url) | |
71 url = '%s/get_master/%s' % (CBE_BASE, master_name) | |
72 return requests.get(url).json() | |
73 | |
74 | |
75 def prefill_builds_cache(cache, master_url, builder_name): | |
76 master_name = master_name_from_url(master_url) | |
77 builds_url = '%s/get_builds' % CBE_BASE | |
78 params = { 'master': master_name, 'builder': builder_name } | |
79 response = requests.get(builds_url, params=params) | |
80 builds = response.json()['builds'] | |
81 for build in builds: | |
82 if not build.get('number'): | |
83 index = builds.index(build) | |
84 log.error('build at index %s in %s missing number?' % (index, respon se.url)) | |
85 continue | |
86 build_number = build['number'] | |
87 key = cache_key_for_build(master_url, builder_name, build_number) | |
88 cache.set(key, build) | |
89 build_numbers = map(operator.itemgetter('number'), builds) | |
90 log.debug('Prefilled (%.1fs) %s for %s %s' % | |
91 (response.elapsed.total_seconds(), | |
92 string_helpers.re_range(build_numbers), | |
93 master_name, builder_name)) | |
94 return build_numbers | |
95 | |
96 | |
97 def fetch_and_cache_build(cache, url, cache_key): | |
98 log.debug('Fetching %s.' % url) | |
99 try: | |
100 build = requests.get(url).json() | |
101 # Don't cache builds which are just errors? | |
102 if build.get('number'): | |
103 if build.get('eta') is None: | |
104 cache.set(cache_key, build) | |
105 else: | |
106 log.debug('Not caching in-progress build from %s.') | |
107 return build | |
108 except ValueError, e: | |
109 log.error('Not caching invalid json: %s: %s' % (url, e)) | |
110 | |
111 | |
112 def fetch_build_json(cache, master_url, builder_name, build_number): | |
113 cache_key = cache_key_for_build(master_url, builder_name, build_number) | |
114 build = cache.get(cache_key) | |
115 # I accidentally stored some error builds and incomplete builds before. | |
116 if build and (not build.get('number') or build.get('eta')): | |
117 log.warn('Refetching %s %s %s' % (master_url, builder_name, build_number)) | |
118 build = None | |
119 | |
120 master_name = master_name_from_url(master_url) | |
121 | |
122 cbe_url = "https://chrome-build-extract.appspot.com/p/%s/builders/%s/builds/%s ?json=1" % ( | |
123 master_name, builder_name, build_number) | |
124 if not build: | |
125 build = fetch_and_cache_build(cache, cbe_url, cache_key) | |
126 | |
127 if not build: | |
128 log.warn("CBE failed, failover to buildbot %s" % cbe_url) | |
129 buildbot_url = "https://build.chromium.org/p/%s/json/builders/%s/builds/%s" % ( | |
130 master_name, builder_name, build_number) | |
131 build = fetch_and_cache_build(cache, buildbot_url, cache_key) | |
132 | |
133 return build | |
134 | |
135 | |
136 # This effectively extracts the 'configuration' of the build | |
137 # we could extend this beyond repo versions in the future. | |
138 def revisions_from_build(build_json): | |
139 def _property_value(build_json, property_name): | |
140 for prop_tuple in build_json['properties']: | |
141 if prop_tuple[0] == property_name: | |
142 return prop_tuple[1] | |
143 | |
144 REVISION_VARIABLES = [ | |
145 ('chromium', 'got_revision'), | |
146 ('blink', 'got_webkit_revision'), | |
147 ('v8', 'got_v8_revision'), | |
148 ('nacl', 'got_nacl_revision'), | |
149 # Skia, for whatever reason, isn't exposed in the buildbot properties so | |
150 # don't bother to include it here. | |
151 ] | |
152 | |
153 revisions = {} | |
154 for repo_name, buildbot_property in REVISION_VARIABLES: | |
155 # This is epicly stupid: 'tester' builders have the wrong | |
156 # revision for 'got_foo_revision' and we have to use | |
157 # parent_got_foo_revision instead, but non-tester builders | |
158 # don't have the parent_ versions, so we have to fall back | |
159 # to got_foo_revision in those cases! | |
160 # 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
| |
161 revision = _property_value(build_json, 'parent_' + buildbot_property) | |
162 if not revision: | |
163 revision = _property_value(build_json, buildbot_property) | |
164 revisions[repo_name] = revision | |
165 return revisions | |
166 | |
167 | |
168 def latest_revisions_for_master(cache, master_url, master_json): | |
169 latest_revisions = collections.defaultdict(dict) | |
170 master_name = master_name_from_url(master_url) | |
171 for builder_name, builder_json in master_json['builders'].items(): | |
172 # recent_builds can include current builds | |
173 recent_builds = set(builder_json['cachedBuilds']) | |
174 active_builds = set(builder_json['currentBuilds']) | |
175 last_finished_id = sorted(recent_builds - active_builds, reverse=True)[0] | |
176 last_build = fetch_build_json(cache, master_url, builder_name, last_finished _id) | |
177 latest_revisions[master_name][builder_name] = revisions_from_build(last_buil d) | |
178 return latest_revisions | |
OLD | NEW |