OLD | NEW |
---|---|
1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
2 # Copyright 2013 The Chromium Authors. All rights reserved. | 2 # Copyright 2013 The Chromium Authors. All rights reserved. |
3 # Use of this source code is governed by a BSD-style license that can be | 3 # Use of this source code is governed by a BSD-style license that can be |
4 # found in the LICENSE file. | 4 # found in the LICENSE file. |
5 | 5 |
6 """Closes tree if configured masters have failed tree-closing steps. | 6 """Closes tree if configured masters have failed tree-closing steps. |
7 | 7 |
8 Given a list of masters, gatekeeper_ng will get a list of the latest builds from | 8 Given a list of masters, gatekeeper_ng will get a list of the latest builds from |
9 the specified masters. It then checks if any tree-closing steps have failed, and | 9 the specified masters. It then checks if any tree-closing steps have failed, and |
10 if so closes the tree and emails appropriate parties. Configuration for which | 10 if so closes the tree and emails appropriate parties. Configuration for which |
(...skipping 135 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
146 failed_builds = [] | 146 failed_builds = [] |
147 for build_json, master_url in master_builds: | 147 for build_json, master_url in master_builds: |
148 gatekeeper_sections = gatekeeper_config.get(master_url, []) | 148 gatekeeper_sections = gatekeeper_config.get(master_url, []) |
149 for gatekeeper_section in gatekeeper_sections: | 149 for gatekeeper_section in gatekeeper_sections: |
150 gatekeeper = gatekeeper_section.get(build_json['builderName'], {}) | 150 gatekeeper = gatekeeper_section.get(build_json['builderName'], {}) |
151 steps = build_json['steps'] | 151 steps = build_json['steps'] |
152 forgiving = set(gatekeeper.get('forgiving_steps', [])) | 152 forgiving = set(gatekeeper.get('forgiving_steps', [])) |
153 closing_steps = set(gatekeeper.get('closing_steps', [])) | forgiving | 153 closing_steps = set(gatekeeper.get('closing_steps', [])) | forgiving |
154 tree_notify = set(gatekeeper.get('tree_notify', [])) | 154 tree_notify = set(gatekeeper.get('tree_notify', [])) |
155 sheriff_classes = set(gatekeeper.get('sheriff_classes', [])) | 155 sheriff_classes = set(gatekeeper.get('sheriff_classes', [])) |
156 subject_template = gatekeeper['subject_template'] | |
156 finished = [s for s in steps if s.get('isFinished')] | 157 finished = [s for s in steps if s.get('isFinished')] |
157 close_tree = gatekeeper.get('close_tree', True) | 158 close_tree = gatekeeper.get('close_tree', True) |
158 respect_build_status = gatekeeper.get('respect_build_status', False) | 159 respect_build_status = gatekeeper.get('respect_build_status', False) |
159 | 160 |
160 successful_steps = set(s['name'] for s in finished | 161 successful_steps = set(s['name'] for s in finished |
161 if (s.get('results', [FAILURE])[0] == SUCCESS or | 162 if (s.get('results', [FAILURE])[0] == SUCCESS or |
162 s.get('results', [FAILURE])[0] == WARNINGS)) | 163 s.get('results', [FAILURE])[0] == WARNINGS)) |
163 | 164 |
164 unsatisfied_steps = closing_steps - successful_steps | 165 unsatisfied_steps = closing_steps - successful_steps |
165 | 166 |
(...skipping 28 matching lines...) Expand all Loading... | |
194 build_db[master_url][build_json['builderName']] = max( | 195 build_db[master_url][build_json['builderName']] = max( |
195 build_json['number'], | 196 build_json['number'], |
196 build_db[master_url][build_json['builderName']]) | 197 build_db[master_url][build_json['builderName']]) |
197 | 198 |
198 failed_builds.append({'base_url': buildbot_url, | 199 failed_builds.append({'base_url': buildbot_url, |
199 'build': build_json, | 200 'build': build_json, |
200 'close_tree': close_tree, | 201 'close_tree': close_tree, |
201 'forgiving_steps': forgiving, | 202 'forgiving_steps': forgiving, |
202 'project_name': project_name, | 203 'project_name': project_name, |
203 'sheriff_classes': sheriff_classes, | 204 'sheriff_classes': sheriff_classes, |
205 'subject_template': subject_template, | |
204 'tree_notify': tree_notify, | 206 'tree_notify': tree_notify, |
205 'unsatisfied': unsatisfied_steps, | 207 'unsatisfied': unsatisfied_steps, |
206 }) | 208 }) |
207 | 209 |
208 return failed_builds | 210 return failed_builds |
209 | 211 |
210 | 212 |
211 def allowed_keys(test_dict, *keys): | 213 def allowed_keys(test_dict, *keys): |
212 keys = keys + ('comment',) | 214 keys = keys + ('comment',) |
213 assert all(k in keys for k in test_dict), ( | 215 assert all(k in keys for k in test_dict), ( |
(...skipping 22 matching lines...) Expand all Loading... | |
236 is considered blank (empty set), and inheritance is always constructive (you | 238 is considered blank (empty set), and inheritance is always constructive (you |
237 can't remove a property by inheriting or overwriting it). Builders can inherit | 239 can't remove a property by inheriting or overwriting it). Builders can inherit |
238 categories from their master. | 240 categories from their master. |
239 | 241 |
240 A master consists of zero or more sections, which specify which builders are | 242 A master consists of zero or more sections, which specify which builders are |
241 watched by the section and what action should be taken. A section can specify | 243 watched by the section and what action should be taken. A section can specify |
242 tree_closing to be false, which causes the section to only send out emails | 244 tree_closing to be false, which causes the section to only send out emails |
243 instead of closing the tree. A section or builder can also specify to respect | 245 instead of closing the tree. A section or builder can also specify to respect |
244 a build's failure status with respect_build_status. | 246 a build's failure status with respect_build_status. |
245 | 247 |
248 The 'subject_template' key is the template used for the email subjects. Its | |
249 formatting arguments are found at https://chromium.googlesource.com/chromium/ | |
250 tools/chromium-build/+/master/gatekeeper_mailer.py, but the list is | |
251 reproduced here: | |
252 | |
253 %(result)s: 'warning' or 'failure' | |
254 %(projectName): 'Chromium', 'Chromium Perf', etc. | |
255 %(builder): the builder name | |
256 %(reason): reason for launching the build | |
257 %(revision): build revision | |
258 %(buildnumber): buildnumber | |
259 | |
246 The 'comment' key can be put anywhere and is ignored by the parser. | 260 The 'comment' key can be put anywhere and is ignored by the parser. |
247 | 261 |
248 # Python, not JSON. | 262 # Python, not JSON. |
249 { | 263 { |
250 'masters': { | 264 'masters': { |
251 'http://build.chromium.org/p/chromium.win': [ | 265 'http://build.chromium.org/p/chromium.win': [ |
252 { | 266 { |
253 'sheriff_classes': ['sheriff_win'], | 267 'sheriff_classes': ['sheriff_win'], |
254 'tree_notify': ['a_watcher@chromium.org'], | 268 'tree_notify': ['a_watcher@chromium.org'], |
255 'categories': ['win_extra'], | 269 'categories': ['win_extra'], |
(...skipping 12 matching lines...) Expand all Loading... | |
268 'categories': { | 282 'categories': { |
269 'win_tests': { | 283 'win_tests': { |
270 'comment': 'this is for all windows testers', | 284 'comment': 'this is for all windows testers', |
271 'closing_steps': ['startup_test'], | 285 'closing_steps': ['startup_test'], |
272 'forgiving_steps': ['boot_windows'], | 286 'forgiving_steps': ['boot_windows'], |
273 'tree_notify': ['win_watchers@chromium.org'], | 287 'tree_notify': ['win_watchers@chromium.org'], |
274 'sheriff_classes': ['sheriff_win_test'] | 288 'sheriff_classes': ['sheriff_win_test'] |
275 }, | 289 }, |
276 'win_extra': { | 290 'win_extra': { |
277 'closing_steps': ['extra_win_step'] | 291 'closing_steps': ['extra_win_step'] |
292 'subject_template': 'windows heads up on %(builder)', | |
278 } | 293 } |
279 } | 294 } |
280 } | 295 } |
281 | 296 |
282 In this case, XP Tests (1) would be flattened down to: | 297 In this case, XP Tests (1) would be flattened down to: |
283 closing_steps: ['startup_test', 'win_tests'] | 298 closing_steps: ['startup_test', 'win_tests'] |
284 forgiving_steps: ['archive', 'boot_windows'] | 299 forgiving_steps: ['archive', 'boot_windows'] |
285 tree_notify: ['xp_watchers@chromium.org', 'win_watchers@chromium.org', | 300 tree_notify: ['xp_watchers@chromium.org', 'win_watchers@chromium.org', |
286 'a_watcher@chromium.org'] | 301 'a_watcher@chromium.org'] |
287 sheriff_classes: ['sheriff_win', 'sheriff_win_test', 'sheriff_xp'] | 302 sheriff_classes: ['sheriff_win', 'sheriff_win_test', 'sheriff_xp'] |
288 | 303 |
289 Again, fields are optional and treated as empty lists/sets if not present. | 304 Again, fields are optional and treated as empty lists/sets if not present. |
290 """ | 305 """ |
291 | 306 |
292 master_keys = ['sheriff_classes', 'tree_notify'] | 307 # Keys which are allowed in a master or builder section. |
308 master_keys = ['sheriff_classes', 'tree_notify', 'subject_template'] | |
293 builder_keys = ['forgiving_steps', 'closing_steps', 'tree_notify', | 309 builder_keys = ['forgiving_steps', 'closing_steps', 'tree_notify', |
294 'sheriff_classes'] | 310 'sheriff_classes', 'subject_template'] |
311 | |
312 | |
313 # Keys which have defaults besides None or set([]). | |
314 defaults = { | |
315 'subject_template': ('buildbot %(result)s in %(projectName)s on ' | |
316 '%(builder)s, revision %(revision)s'), | |
317 } | |
318 | |
319 # These keys are strings instead of sets. Strings can't be merged, | |
320 # so more specific (master -> category -> builder) strings clobber | |
321 # more generic ones. | |
322 strings = ['subject_template'] | |
295 | 323 |
296 with open(filename) as f: | 324 with open(filename) as f: |
297 raw_gatekeeper_config = json.load(f) | 325 raw_gatekeeper_config = json.load(f) |
298 | 326 |
299 allowed_keys(raw_gatekeeper_config, 'categories', 'masters') | 327 allowed_keys(raw_gatekeeper_config, 'categories', 'masters') |
300 | 328 |
301 categories = raw_gatekeeper_config.get('categories', {}) | 329 categories = raw_gatekeeper_config.get('categories', {}) |
302 masters = raw_gatekeeper_config.get('masters', {}) | 330 masters = raw_gatekeeper_config.get('masters', {}) |
303 | 331 |
304 for category in categories.values(): | 332 for category in categories.values(): |
305 allowed_keys(category, *builder_keys) | 333 allowed_keys(category, *builder_keys) |
306 | 334 |
307 gatekeeper_config = {} | 335 gatekeeper_config = {} |
308 for master_url, master_sections in masters.iteritems(): | 336 for master_url, master_sections in masters.iteritems(): |
309 for master_section in master_sections: | 337 for master_section in master_sections: |
310 gatekeeper_config.setdefault(master_url, []).append({}) | 338 gatekeeper_config.setdefault(master_url, []).append({}) |
311 allowed_keys(master_section, 'builders', 'categories', 'close_tree', | 339 allowed_keys(master_section, 'builders', 'categories', 'close_tree', |
312 'respect_build_status', *master_keys) | 340 'respect_build_status', *master_keys) |
313 | 341 |
314 builders = master_section.get('builders', {}) | 342 builders = master_section.get('builders', {}) |
315 for buildername, builder in builders.iteritems(): | 343 for buildername, builder in builders.iteritems(): |
316 allowed_keys(builder, 'categories', *builder_keys) | 344 allowed_keys(builder, 'categories', *builder_keys) |
317 for item in builder.values(): | 345 for key, item in builder.iteritems(): |
318 assert isinstance(item, list) | 346 if key in strings: |
319 assert all(isinstance(elem, basestring) for elem in item) | 347 assert isinstance(item, basestring) |
348 else: | |
349 assert isinstance(item, list) | |
350 assert all(isinstance(elem, basestring) for elem in item) | |
320 | 351 |
321 gatekeeper_config[master_url][-1].setdefault(buildername, {}) | 352 gatekeeper_config[master_url][-1].setdefault(buildername, {}) |
322 gatekeeper_builder = gatekeeper_config[master_url][-1][buildername] | 353 gatekeeper_builder = gatekeeper_config[master_url][-1][buildername] |
323 | 354 |
324 # Populate with empty defaults. | 355 # Populate with specified defaults. |
325 for k in builder_keys: | 356 for k in builder_keys: |
326 gatekeeper_builder.setdefault(k, set()) | 357 if k in defaults: |
358 gatekeeper_builder.setdefault(k, defaults[k]) | |
359 elif k in strings: | |
360 gatekeeper_builder.setdefault(k, '') | |
361 else: | |
362 gatekeeper_builder.setdefault(k, set()) | |
327 | 363 |
328 # Inherit any values from the master. | 364 # Inherit any values from the master. |
329 for k in master_keys: | 365 for k in master_keys: |
330 gatekeeper_builder[k] |= set(master_section.get(k, [])) | 366 if k in strings: |
367 if k in master_section: | |
368 gatekeeper_builder[k] = master_section[k] | |
369 else: | |
370 gatekeeper_builder[k] |= set(master_section.get(k, [])) | |
331 | 371 |
332 gatekeeper_builder['close_tree'] = master_section.get('close_tree', | 372 gatekeeper_builder['close_tree'] = master_section.get('close_tree', |
333 True) | 373 True) |
334 gatekeeper_builder['respect_build_status'] = master_section.get( | 374 gatekeeper_builder['respect_build_status'] = master_section.get( |
335 'respect_build_status', False) | 375 'respect_build_status', False) |
336 | 376 |
337 # Inherit any values from the categories. | 377 # Inherit any values from the categories. |
338 all_categories = (builder.get('categories', []) + | 378 all_categories = (builder.get('categories', []) + |
339 master_section.get( 'categories', [])) | 379 master_section.get( 'categories', [])) |
340 for c in all_categories: | 380 for c in all_categories: |
341 for k in builder_keys: | 381 for k in builder_keys: |
342 gatekeeper_builder[k] |= set(categories[c].get(k, [])) | 382 if k in strings: |
383 if k in categories[c]: | |
384 gatekeeper_builder[k] = categories[c][k] | |
385 else: | |
386 gatekeeper_builder[k] |= set(categories[c].get(k, [])) | |
343 | 387 |
344 # Add in any builder-specific values. | 388 # Add in any builder-specific values. |
345 for k in builder_keys: | 389 for k in builder_keys: |
346 gatekeeper_builder[k] |= set(builder.get(k, [])) | 390 if k in strings: |
391 if k in builder: | |
392 gatekeeper_builder[k] = builder[k] | |
393 else: | |
394 gatekeeper_builder[k] |= set(builder.get(k, [])) | |
347 | 395 |
348 return gatekeeper_config | 396 return gatekeeper_config |
iannucci
2013/12/17 00:46:23
This whole function is pretty funky. It may be wor
ghost stip (do not use)
2013/12/17 18:37:14
Good point, might make it a bit easier to traverse
| |
349 | 397 |
350 | 398 |
351 def parse_sheriff_file(url): | 399 def parse_sheriff_file(url): |
352 """Given a sheriff url, download and parse the appropirate sheriff list.""" | 400 """Given a sheriff url, download and parse the appropirate sheriff list.""" |
353 with closing(urllib2.urlopen(url)) as f: | 401 with closing(urllib2.urlopen(url)) as f: |
354 line = f.readline() | 402 line = f.readline() |
355 usernames_matcher_ = re.compile(r'document.write\(\'([\w, ]+)\'\)') | 403 usernames_matcher_ = re.compile(r'document.write\(\'([\w, ]+)\'\)') |
356 usernames_match = usernames_matcher_.match(line) | 404 usernames_match = usernames_matcher_.match(line) |
357 sheriffs = set() | 405 sheriffs = set() |
358 if usernames_match: | 406 if usernames_match: |
(...skipping 83 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
442 tree_notify = failed_build['tree_notify'] | 490 tree_notify = failed_build['tree_notify'] |
443 | 491 |
444 if failed_build['unsatisfied'] <= failed_build['forgiving_steps']: | 492 if failed_build['unsatisfied'] <= failed_build['forgiving_steps']: |
445 blamelist = set() | 493 blamelist = set() |
446 else: | 494 else: |
447 blamelist = set(failed_build['build']['blame']) | 495 blamelist = set(failed_build['build']['blame']) |
448 | 496 |
449 sheriffs = get_sheriffs(failed_build['sheriff_classes'], sheriff_url) | 497 sheriffs = get_sheriffs(failed_build['sheriff_classes'], sheriff_url) |
450 watchers = list(tree_notify | blamelist | sheriffs) | 498 watchers = list(tree_notify | blamelist | sheriffs) |
451 | 499 |
452 | |
453 build_data = { | 500 build_data = { |
454 'build_url': build_url, | 501 'build_url': build_url, |
455 'from_addr': fromaddr, | 502 'from_addr': fromaddr, |
456 'project_name': project_name, | 503 'project_name': project_name, |
504 'subject_template': failed_build['subject_template'], | |
457 'steps': [], | 505 'steps': [], |
458 'unsatisfied': list(failed_build['unsatisfied']), | 506 'unsatisfied': list(failed_build['unsatisfied']), |
459 'waterfall_url': waterfall_url, | 507 'waterfall_url': waterfall_url, |
460 } | 508 } |
461 | 509 |
462 for field in ['builderName', 'number', 'reason']: | 510 for field in ['builderName', 'number', 'reason']: |
463 build_data[field] = failed_build['build'][field] | 511 build_data[field] = failed_build['build'][field] |
464 | 512 |
465 build_data['result'] = failed_build['build'].get('results', 0) | 513 build_data['result'] = failed_build['build'].get('results', 0) |
466 build_data['blamelist'] = failed_build['build']['blame'] | 514 build_data['blamelist'] = failed_build['build']['blame'] |
(...skipping 191 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
658 options.disable_domain_filter) | 706 options.disable_domain_filter) |
659 | 707 |
660 if not options.skip_build_db_update: | 708 if not options.skip_build_db_update: |
661 save_build_db(build_db, options.build_db) | 709 save_build_db(build_db, options.build_db) |
662 | 710 |
663 return 0 | 711 return 0 |
664 | 712 |
665 | 713 |
666 if __name__ == '__main__': | 714 if __name__ == '__main__': |
667 sys.exit(main()) | 715 sys.exit(main()) |
OLD | NEW |