OLD | NEW |
---|---|
(Empty) | |
1 #!/usr/bin/env python | |
2 # Copyright 2014 The Chromium Authors. All rights reserved. | |
3 # Use of this source code is governed by a BSD-style license that can be | |
4 # found in the LICENSE file. | |
5 | |
ojan
2014/07/22 02:01:25
This should have a FIXME to get the file from else
| |
6 """Loads gatekeeper configuration files for use with gatekeeper_ng.py. | |
7 | |
8 The gatekeeper json configuration file has two main sections: 'masters' | |
9 and 'categories.' The following shows the breakdown of a possible config, | |
10 but note that all nodes are optional (including the root 'masters' and | |
11 'categories' nodes). | |
12 | |
13 A builder ultimately needs 4 lists (sets): | |
14 closing_steps: steps which close the tree on failure or omission | |
15 forgiving_steps: steps which close the tree but don't email committers | |
16 tree_notify: any additional emails to notify on tree failure | |
17 sheriff_classes: classes of sheriffs to notify on build failure | |
18 | |
19 Builders can inherit these properties from categories, they can inherit | |
20 tree_notify and sheriff_classes from their master, and they can have these | |
21 properties assigned in the builder itself. Any property not specified | |
22 is considered blank (empty set), and inheritance is always constructive (you | |
23 can't remove a property by inheriting or overwriting it). Builders can inherit | |
24 categories from their master. | |
25 | |
26 A master consists of zero or more sections, which specify which builders are | |
27 watched by the section and what action should be taken. A section can specify | |
28 tree_closing to be false, which causes the section to only send out emails | |
29 instead of closing the tree. A section or builder can also specify to respect | |
30 a build's failure status with respect_build_status. | |
31 | |
32 The 'subject_template' key is the template used for the email subjects. Its | |
33 formatting arguments are found at https://chromium.googlesource.com/chromium/ | |
34 tools/chromium-build/+/master/gatekeeper_mailer.py, but the list is | |
35 reproduced here: | |
36 | |
37 %(result)s: 'warning' or 'failure' | |
38 %(project_name): 'Chromium', 'Chromium Perf', etc. | |
39 %(builder_name): the builder name | |
40 %(reason): reason for launching the build | |
41 %(revision): build revision | |
42 %(buildnumber): buildnumber | |
43 | |
44 The 'status_template' is what is sent to the status app if the tree is set to be | |
45 closed. Its formatting arguments are found in gatekeeper_ng.py's | |
46 close_tree_if_necessary(). | |
47 | |
48 'forgive_all' converts all closing_steps to be forgiving_steps. Since | |
49 forgiving_steps only email sheriffs + watchlist (not the committer), this is a | |
50 great way to set up experimental or informational builders without spamming | |
51 people. It is enabled by providing the string 'true'. | |
52 | |
53 'forgiving_optional' and 'closing_optional' work just like 'forgiving_steps' | |
54 and 'closing_steps', but they won't close if the step is missing. This is like | |
55 previous gatekeeper behavior. They can be set to '*', which will match all | |
56 steps in the builder. | |
57 | |
58 The 'comment' key can be put anywhere and is ignored by the parser. | |
59 | |
60 # Python, not JSON. | |
61 { | |
62 'masters': { | |
63 'http://build.chromium.org/p/chromium.win': [ | |
64 { | |
65 'sheriff_classes': ['sheriff_win'], | |
66 'tree_notify': ['a_watcher@chromium.org'], | |
67 'categories': ['win_extra'], | |
68 'builders': { | |
69 'XP Tests (1)': { | |
70 'categories': ['win_tests'], | |
71 'closing_steps': ['xp_special_step'], | |
72 'forgiving_steps': ['archive'], | |
73 'tree_notify': ['xp_watchers@chromium.org'], | |
74 'sheriff_classes': ['sheriff_xp'], | |
75 } | |
76 } | |
77 } | |
78 ] | |
79 }, | |
80 'categories': { | |
81 'win_tests': { | |
82 'comment': 'this is for all windows testers', | |
83 'closing_steps': ['startup_test'], | |
84 'forgiving_steps': ['boot_windows'], | |
85 'tree_notify': ['win_watchers@chromium.org'], | |
86 'sheriff_classes': ['sheriff_win_test'] | |
87 }, | |
88 'win_extra': { | |
89 'closing_steps': ['extra_win_step'] | |
90 'subject_template': 'windows heads up on %(builder_name)', | |
91 } | |
92 } | |
93 } | |
94 | |
95 In this case, XP Tests (1) would be flattened down to: | |
96 closing_steps: ['startup_test', 'win_tests'] | |
97 forgiving_steps: ['archive', 'boot_windows'] | |
98 tree_notify: ['xp_watchers@chromium.org', 'win_watchers@chromium.org', | |
99 'a_watcher@chromium.org'] | |
100 sheriff_classes: ['sheriff_win', 'sheriff_win_test', 'sheriff_xp'] | |
101 | |
102 Again, fields are optional and treated as empty lists/sets/strings if not | |
103 present. | |
104 """ | |
105 | |
106 import copy | |
107 import cStringIO | |
108 import hashlib | |
109 import json | |
110 import optparse | |
111 import os | |
112 import sys | |
113 | |
114 | |
115 DATA_DIR = os.path.dirname(os.path.abspath(__file__)) | |
116 | |
117 | |
118 # Keys which have defaults besides None or set([]). | |
119 DEFAULTS = { | |
120 'status_template': ('Tree is closed (Automatic: "%(unsatisfied)s" on ' | |
121 '"%(builder_name)s" %(blamelist)s)'), | |
122 'subject_template': ('buildbot %(result)s in %(project_name)s on ' | |
123 '%(builder_name)s, revision %(revision)s'), | |
124 } | |
125 | |
126 | |
127 def allowed_keys(test_dict, *keys): | |
128 keys = keys + ('comment',) | |
129 assert all(k in keys for k in test_dict), ( | |
130 'not valid: %s; allowed: %s' % ( | |
131 ', '.join(set(test_dict.keys()) - set(keys)), | |
132 ', '.join(keys))) | |
133 | |
134 | |
135 def load_gatekeeper_config(filename): | |
136 """Loads and verifies config json, constructs builder config dict.""" | |
137 | |
138 # Keys which are allowed in a master or builder section. | |
139 master_keys = ['excluded_builders', | |
140 'excluded_steps', | |
141 'forgive_all', | |
142 'sheriff_classes', | |
143 'status_template', | |
144 'subject_template', | |
145 'tree_notify', | |
146 ] | |
147 | |
148 builder_keys = ['closing_optional', | |
149 'closing_steps', | |
150 'excluded_builders', | |
151 'excluded_steps', | |
152 'forgive_all', | |
153 'forgiving_optional', | |
154 'forgiving_steps', | |
155 'sheriff_classes', | |
156 'status_template', | |
157 'subject_template', | |
158 'tree_notify', | |
159 ] | |
160 | |
161 # These keys are strings instead of sets. Strings can't be merged, | |
162 # so more specific (master -> category -> builder) strings clobber | |
163 # more generic ones. | |
164 strings = ['forgive_all', 'status_template', 'subject_template'] | |
165 | |
166 with open(filename) as f: | |
167 raw_gatekeeper_config = json.load(f) | |
168 | |
169 allowed_keys(raw_gatekeeper_config, 'categories', 'masters') | |
170 | |
171 categories = raw_gatekeeper_config.get('categories', {}) | |
172 masters = raw_gatekeeper_config.get('masters', {}) | |
173 | |
174 for category in categories.values(): | |
175 allowed_keys(category, *builder_keys) | |
176 | |
177 gatekeeper_config = {} | |
178 for master_url, master_sections in masters.iteritems(): | |
179 for master_section in master_sections: | |
180 gatekeeper_config.setdefault(master_url, []).append({}) | |
181 allowed_keys(master_section, 'builders', 'categories', 'close_tree', | |
182 'respect_build_status', *master_keys) | |
183 | |
184 builders = master_section.get('builders', {}) | |
185 for buildername, builder in builders.iteritems(): | |
186 allowed_keys(builder, 'categories', *builder_keys) | |
187 for key, item in builder.iteritems(): | |
188 if key in strings: | |
189 assert isinstance(item, basestring) | |
190 else: | |
191 assert isinstance(item, list) | |
192 assert all(isinstance(elem, basestring) for elem in item) | |
193 | |
194 gatekeeper_config[master_url][-1].setdefault(buildername, {}) | |
195 gatekeeper_builder = gatekeeper_config[master_url][-1][buildername] | |
196 | |
197 # Populate with specified defaults. | |
198 for k in builder_keys: | |
199 if k in DEFAULTS: | |
200 gatekeeper_builder.setdefault(k, DEFAULTS[k]) | |
201 elif k in strings: | |
202 gatekeeper_builder.setdefault(k, '') | |
203 else: | |
204 gatekeeper_builder.setdefault(k, set()) | |
205 | |
206 # Inherit any values from the master. | |
207 for k in master_keys: | |
208 if k in strings: | |
209 if k in master_section: | |
210 gatekeeper_builder[k] = master_section[k] | |
211 else: | |
212 gatekeeper_builder[k] |= set(master_section.get(k, [])) | |
213 | |
214 gatekeeper_builder['close_tree'] = master_section.get('close_tree', | |
215 True) | |
216 gatekeeper_builder['respect_build_status'] = master_section.get( | |
217 'respect_build_status', False) | |
218 | |
219 # Inherit any values from the categories. | |
220 all_categories = (builder.get('categories', []) + | |
221 master_section.get( 'categories', [])) | |
222 for c in all_categories: | |
223 for k in builder_keys: | |
224 if k in strings: | |
225 if k in categories[c]: | |
226 gatekeeper_builder[k] = categories[c][k] | |
227 else: | |
228 gatekeeper_builder[k] |= set(categories[c].get(k, [])) | |
229 | |
230 # Add in any builder-specific values. | |
231 for k in builder_keys: | |
232 if k in strings: | |
233 if k in builder: | |
234 gatekeeper_builder[k] = builder[k] | |
235 else: | |
236 gatekeeper_builder[k] |= set(builder.get(k, [])) | |
237 | |
238 # Builder postprocessing. | |
239 if gatekeeper_builder['forgive_all'] == 'true': | |
240 gatekeeper_builder['forgiving_steps'] |= gatekeeper_builder[ | |
241 'closing_steps'] | |
242 gatekeeper_builder['forgiving_optional'] |= gatekeeper_builder[ | |
243 'closing_optional'] | |
244 gatekeeper_builder['closing_steps'] = set([]) | |
245 gatekeeper_builder['closing_optional'] = set([]) | |
246 | |
247 return gatekeeper_config | |
248 | |
249 | |
250 def gatekeeper_section_hash(gatekeeper_section): | |
251 st = cStringIO.StringIO() | |
252 flatten_to_json(gatekeeper_section, st) | |
253 return hashlib.sha256(st.getvalue()).hexdigest() | |
254 | |
255 | |
256 def inject_hashes(gatekeeper_config): | |
257 new_config = copy.deepcopy(gatekeeper_config) | |
258 for master in new_config.values(): | |
259 for section in master: | |
260 section['section_hash'] = gatekeeper_section_hash(section) | |
261 return new_config | |
262 | |
263 | |
264 # Python's sets aren't JSON-encodable, so we convert them to lists here. | |
265 class SetEncoder(json.JSONEncoder): | |
266 # pylint: disable=E0202 | |
267 def default(self, obj): | |
268 if isinstance(obj, set): | |
269 return sorted(list(obj)) | |
270 return json.JSONEncoder.default(self, obj) | |
271 | |
272 | |
273 def flatten_to_json(gatekeeper_config, stream): | |
274 json.dump(gatekeeper_config, stream, cls=SetEncoder, sort_keys=True) | |
275 | |
276 | |
277 def main(): | |
278 prog_desc = 'Reads gatekeeper.json and emits a flattened config.' | |
279 usage = '%prog [options]' | |
280 parser = optparse.OptionParser(usage=(usage + '\n\n' + prog_desc)) | |
281 parser.add_option('--json', default=os.path.join(DATA_DIR, 'gatekeeper.json'), | |
282 help='location of gatekeeper configuration file') | |
283 parser.add_option('--no-hashes', action='store_true', | |
284 help='don\'t insert gatekeeper section hashes') | |
285 options, _ = parser.parse_args() | |
286 | |
287 gatekeeper_config = load_gatekeeper_config(options.json) | |
288 | |
289 if not options.no_hashes: | |
290 gatekeeper_config = inject_hashes(gatekeeper_config) | |
291 | |
292 flatten_to_json(gatekeeper_config, sys.stdout) | |
293 print | |
294 | |
295 return 0 | |
296 | |
297 | |
298 if __name__ == '__main__': | |
299 sys.exit(main()) | |
OLD | NEW |