Chromium Code Reviews| Index: Tools/AutoSheriff/gatekeeper_ng_config.py |
| diff --git a/Tools/AutoSheriff/gatekeeper_ng_config.py b/Tools/AutoSheriff/gatekeeper_ng_config.py |
| new file mode 100755 |
| index 0000000000000000000000000000000000000000..c5183d98f9b0b1a575b5d7f4fa8a0ba564619066 |
| --- /dev/null |
| +++ b/Tools/AutoSheriff/gatekeeper_ng_config.py |
| @@ -0,0 +1,299 @@ |
| +#!/usr/bin/env python |
| +# 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. |
| + |
|
ojan
2014/07/22 02:01:25
This should have a FIXME to get the file from else
|
| +"""Loads gatekeeper configuration files for use with gatekeeper_ng.py. |
| + |
| +The gatekeeper json configuration file has two main sections: 'masters' |
| +and 'categories.' The following shows the breakdown of a possible config, |
| +but note that all nodes are optional (including the root 'masters' and |
| +'categories' nodes). |
| + |
| +A builder ultimately needs 4 lists (sets): |
| + closing_steps: steps which close the tree on failure or omission |
| + forgiving_steps: steps which close the tree but don't email committers |
| + tree_notify: any additional emails to notify on tree failure |
| + sheriff_classes: classes of sheriffs to notify on build failure |
| + |
| +Builders can inherit these properties from categories, they can inherit |
| +tree_notify and sheriff_classes from their master, and they can have these |
| +properties assigned in the builder itself. Any property not specified |
| +is considered blank (empty set), and inheritance is always constructive (you |
| +can't remove a property by inheriting or overwriting it). Builders can inherit |
| +categories from their master. |
| + |
| +A master consists of zero or more sections, which specify which builders are |
| +watched by the section and what action should be taken. A section can specify |
| +tree_closing to be false, which causes the section to only send out emails |
| +instead of closing the tree. A section or builder can also specify to respect |
| +a build's failure status with respect_build_status. |
| + |
| +The 'subject_template' key is the template used for the email subjects. Its |
| +formatting arguments are found at https://chromium.googlesource.com/chromium/ |
| + tools/chromium-build/+/master/gatekeeper_mailer.py, but the list is |
| +reproduced here: |
| + |
| + %(result)s: 'warning' or 'failure' |
| + %(project_name): 'Chromium', 'Chromium Perf', etc. |
| + %(builder_name): the builder name |
| + %(reason): reason for launching the build |
| + %(revision): build revision |
| + %(buildnumber): buildnumber |
| + |
| +The 'status_template' is what is sent to the status app if the tree is set to be |
| +closed. Its formatting arguments are found in gatekeeper_ng.py's |
| +close_tree_if_necessary(). |
| + |
| +'forgive_all' converts all closing_steps to be forgiving_steps. Since |
| +forgiving_steps only email sheriffs + watchlist (not the committer), this is a |
| +great way to set up experimental or informational builders without spamming |
| +people. It is enabled by providing the string 'true'. |
| + |
| +'forgiving_optional' and 'closing_optional' work just like 'forgiving_steps' |
| +and 'closing_steps', but they won't close if the step is missing. This is like |
| +previous gatekeeper behavior. They can be set to '*', which will match all |
| +steps in the builder. |
| + |
| +The 'comment' key can be put anywhere and is ignored by the parser. |
| + |
| +# Python, not JSON. |
| +{ |
| + 'masters': { |
| + 'http://build.chromium.org/p/chromium.win': [ |
| + { |
| + 'sheriff_classes': ['sheriff_win'], |
| + 'tree_notify': ['a_watcher@chromium.org'], |
| + 'categories': ['win_extra'], |
| + 'builders': { |
| + 'XP Tests (1)': { |
| + 'categories': ['win_tests'], |
| + 'closing_steps': ['xp_special_step'], |
| + 'forgiving_steps': ['archive'], |
| + 'tree_notify': ['xp_watchers@chromium.org'], |
| + 'sheriff_classes': ['sheriff_xp'], |
| + } |
| + } |
| + } |
| + ] |
| + }, |
| + 'categories': { |
| + 'win_tests': { |
| + 'comment': 'this is for all windows testers', |
| + 'closing_steps': ['startup_test'], |
| + 'forgiving_steps': ['boot_windows'], |
| + 'tree_notify': ['win_watchers@chromium.org'], |
| + 'sheriff_classes': ['sheriff_win_test'] |
| + }, |
| + 'win_extra': { |
| + 'closing_steps': ['extra_win_step'] |
| + 'subject_template': 'windows heads up on %(builder_name)', |
| + } |
| + } |
| +} |
| + |
| +In this case, XP Tests (1) would be flattened down to: |
| + closing_steps: ['startup_test', 'win_tests'] |
| + forgiving_steps: ['archive', 'boot_windows'] |
| + tree_notify: ['xp_watchers@chromium.org', 'win_watchers@chromium.org', |
| + 'a_watcher@chromium.org'] |
| + sheriff_classes: ['sheriff_win', 'sheriff_win_test', 'sheriff_xp'] |
| + |
| +Again, fields are optional and treated as empty lists/sets/strings if not |
| +present. |
| +""" |
| + |
| +import copy |
| +import cStringIO |
| +import hashlib |
| +import json |
| +import optparse |
| +import os |
| +import sys |
| + |
| + |
| +DATA_DIR = os.path.dirname(os.path.abspath(__file__)) |
| + |
| + |
| +# Keys which have defaults besides None or set([]). |
| +DEFAULTS = { |
| + 'status_template': ('Tree is closed (Automatic: "%(unsatisfied)s" on ' |
| + '"%(builder_name)s" %(blamelist)s)'), |
| + 'subject_template': ('buildbot %(result)s in %(project_name)s on ' |
| + '%(builder_name)s, revision %(revision)s'), |
| +} |
| + |
| + |
| +def allowed_keys(test_dict, *keys): |
| + keys = keys + ('comment',) |
| + assert all(k in keys for k in test_dict), ( |
| + 'not valid: %s; allowed: %s' % ( |
| + ', '.join(set(test_dict.keys()) - set(keys)), |
| + ', '.join(keys))) |
| + |
| + |
| +def load_gatekeeper_config(filename): |
| + """Loads and verifies config json, constructs builder config dict.""" |
| + |
| + # Keys which are allowed in a master or builder section. |
| + master_keys = ['excluded_builders', |
| + 'excluded_steps', |
| + 'forgive_all', |
| + 'sheriff_classes', |
| + 'status_template', |
| + 'subject_template', |
| + 'tree_notify', |
| + ] |
| + |
| + builder_keys = ['closing_optional', |
| + 'closing_steps', |
| + 'excluded_builders', |
| + 'excluded_steps', |
| + 'forgive_all', |
| + 'forgiving_optional', |
| + 'forgiving_steps', |
| + 'sheriff_classes', |
| + 'status_template', |
| + 'subject_template', |
| + 'tree_notify', |
| + ] |
| + |
| + # These keys are strings instead of sets. Strings can't be merged, |
| + # so more specific (master -> category -> builder) strings clobber |
| + # more generic ones. |
| + strings = ['forgive_all', 'status_template', 'subject_template'] |
| + |
| + with open(filename) as f: |
| + raw_gatekeeper_config = json.load(f) |
| + |
| + allowed_keys(raw_gatekeeper_config, 'categories', 'masters') |
| + |
| + categories = raw_gatekeeper_config.get('categories', {}) |
| + masters = raw_gatekeeper_config.get('masters', {}) |
| + |
| + for category in categories.values(): |
| + allowed_keys(category, *builder_keys) |
| + |
| + gatekeeper_config = {} |
| + for master_url, master_sections in masters.iteritems(): |
| + for master_section in master_sections: |
| + gatekeeper_config.setdefault(master_url, []).append({}) |
| + allowed_keys(master_section, 'builders', 'categories', 'close_tree', |
| + 'respect_build_status', *master_keys) |
| + |
| + builders = master_section.get('builders', {}) |
| + for buildername, builder in builders.iteritems(): |
| + allowed_keys(builder, 'categories', *builder_keys) |
| + for key, item in builder.iteritems(): |
| + if key in strings: |
| + assert isinstance(item, basestring) |
| + else: |
| + assert isinstance(item, list) |
| + assert all(isinstance(elem, basestring) for elem in item) |
| + |
| + gatekeeper_config[master_url][-1].setdefault(buildername, {}) |
| + gatekeeper_builder = gatekeeper_config[master_url][-1][buildername] |
| + |
| + # Populate with specified defaults. |
| + for k in builder_keys: |
| + if k in DEFAULTS: |
| + gatekeeper_builder.setdefault(k, DEFAULTS[k]) |
| + elif k in strings: |
| + gatekeeper_builder.setdefault(k, '') |
| + else: |
| + gatekeeper_builder.setdefault(k, set()) |
| + |
| + # Inherit any values from the master. |
| + for k in master_keys: |
| + if k in strings: |
| + if k in master_section: |
| + gatekeeper_builder[k] = master_section[k] |
| + else: |
| + gatekeeper_builder[k] |= set(master_section.get(k, [])) |
| + |
| + gatekeeper_builder['close_tree'] = master_section.get('close_tree', |
| + True) |
| + gatekeeper_builder['respect_build_status'] = master_section.get( |
| + 'respect_build_status', False) |
| + |
| + # Inherit any values from the categories. |
| + all_categories = (builder.get('categories', []) + |
| + master_section.get( 'categories', [])) |
| + for c in all_categories: |
| + for k in builder_keys: |
| + if k in strings: |
| + if k in categories[c]: |
| + gatekeeper_builder[k] = categories[c][k] |
| + else: |
| + gatekeeper_builder[k] |= set(categories[c].get(k, [])) |
| + |
| + # Add in any builder-specific values. |
| + for k in builder_keys: |
| + if k in strings: |
| + if k in builder: |
| + gatekeeper_builder[k] = builder[k] |
| + else: |
| + gatekeeper_builder[k] |= set(builder.get(k, [])) |
| + |
| + # Builder postprocessing. |
| + if gatekeeper_builder['forgive_all'] == 'true': |
| + gatekeeper_builder['forgiving_steps'] |= gatekeeper_builder[ |
| + 'closing_steps'] |
| + gatekeeper_builder['forgiving_optional'] |= gatekeeper_builder[ |
| + 'closing_optional'] |
| + gatekeeper_builder['closing_steps'] = set([]) |
| + gatekeeper_builder['closing_optional'] = set([]) |
| + |
| + return gatekeeper_config |
| + |
| + |
| +def gatekeeper_section_hash(gatekeeper_section): |
| + st = cStringIO.StringIO() |
| + flatten_to_json(gatekeeper_section, st) |
| + return hashlib.sha256(st.getvalue()).hexdigest() |
| + |
| + |
| +def inject_hashes(gatekeeper_config): |
| + new_config = copy.deepcopy(gatekeeper_config) |
| + for master in new_config.values(): |
| + for section in master: |
| + section['section_hash'] = gatekeeper_section_hash(section) |
| + return new_config |
| + |
| + |
| +# Python's sets aren't JSON-encodable, so we convert them to lists here. |
| +class SetEncoder(json.JSONEncoder): |
| + # pylint: disable=E0202 |
| + def default(self, obj): |
| + if isinstance(obj, set): |
| + return sorted(list(obj)) |
| + return json.JSONEncoder.default(self, obj) |
| + |
| + |
| +def flatten_to_json(gatekeeper_config, stream): |
| + json.dump(gatekeeper_config, stream, cls=SetEncoder, sort_keys=True) |
| + |
| + |
| +def main(): |
| + prog_desc = 'Reads gatekeeper.json and emits a flattened config.' |
| + usage = '%prog [options]' |
| + parser = optparse.OptionParser(usage=(usage + '\n\n' + prog_desc)) |
| + parser.add_option('--json', default=os.path.join(DATA_DIR, 'gatekeeper.json'), |
| + help='location of gatekeeper configuration file') |
| + parser.add_option('--no-hashes', action='store_true', |
| + help='don\'t insert gatekeeper section hashes') |
| + options, _ = parser.parse_args() |
| + |
| + gatekeeper_config = load_gatekeeper_config(options.json) |
| + |
| + if not options.no_hashes: |
| + gatekeeper_config = inject_hashes(gatekeeper_config) |
| + |
| + flatten_to_json(gatekeeper_config, sys.stdout) |
| + |
| + return 0 |
| + |
| + |
| +if __name__ == '__main__': |
| + sys.exit(main()) |