Chromium Code Reviews| 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 |