Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(23)

Side by Side Diff: Tools/AutoSheriff/gatekeeper_ng_config.py

Issue 398823008: WIP: Add auto-sheriff.appspot.com code to Blink Base URL: svn://svn.chromium.org/blink/trunk
Patch Set: Created 6 years, 5 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
(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())
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698