OLD | NEW |
| (Empty) |
1 #!/usr/bin/env python | |
2 # Copyright 2015 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 | |
6 """Updates BuildBot builder directories to the new 'cbuildbot'-driven naming | |
7 scheme. | |
8 | |
9 Classic BuildBot CrOS waterfalls define build directories by composing the | |
10 directory name from component parts resembling the target and a final branch | |
11 name. Oftentimes, these component parts (and, therefore, the composition) don't | |
12 actually match the name of the underlying 'cbuildbot' target. | |
13 | |
14 This presents problems because the build target are fundamentally driven by | |
15 their underlying 'cbuildbot' target, but the composition scheme is extremely | |
16 arbitrary. | |
17 | |
18 Consequently, BuildBot masters are being migrated to a new, deterministic, | |
19 'cbuildbot'-driven naming scheme. A builder building 'cbuildbot' target | |
20 <target> and checking Chromite/'cbuildbot' from branch <branch> will use the | |
21 builder name: <target>-<branch>. This is universally sustainable across all | |
22 waterfalls and ensures that 'cbuildbot' builds are tracked and numbered based | |
23 on their underlying 'cbuildbot' target. | |
24 | |
25 This script is intended to be run on a stopped BuildBot master during build | |
26 directory migration. It will iterate through each build directory in the current | |
27 master naming scheme and rename the classic directories into their new | |
28 'cbuildbot'-driven namespace. | |
29 """ | |
30 | |
31 import argparse | |
32 import collections | |
33 import logging | |
34 import os | |
35 import re | |
36 import shutil | |
37 import sys | |
38 | |
39 from common import cros_chromite | |
40 | |
41 | |
42 class UpdateInfo(collections.namedtuple( | |
43 'UpdateInfo', | |
44 ('src', 'cbb_name', 'branch'))): | |
45 """Information about a single directory update action.""" | |
46 | |
47 _STATIC_PERMUTATIONS = { | |
48 'Canary master': 'master-canary', | |
49 } | |
50 | |
51 _TRANSFORMATIONS = ( | |
52 (r'-canary-', r'-release-'), | |
53 (r'-full', r'-release'), | |
54 (r'-pre-flight', r'-pre-flight-branch'), | |
55 (r'(x86|amd64)$', r'\1-generic'), | |
56 (r'^chromium-tot-chromeos-(.+)-asan', r'\1-tot-asan-informational'), | |
57 (r'^chromium-tot-chromeos-(.+)', r'\1-tot-chrome-pfq-informational'), | |
58 (r'^chromium-(.+)-telemetry$', r'\1-telemetry'), | |
59 (r'(.+)-bin$', r'\1'), | |
60 ) | |
61 | |
62 @property | |
63 def dst(self): | |
64 """Constructs the <cbuildbot>-<branch> form.""" | |
65 return '%s-%s' % (self.cbb_name, self.branch) | |
66 | |
67 @classmethod | |
68 def permutations(cls, name): | |
69 """Attempts to permute a legacy BuildBot name into a Chromite target. | |
70 | |
71 Args: | |
72 name (str): The source name to process and map. | |
73 Yields (str): Various permutations of 'name'. | |
74 """ | |
75 # No cbuildbot targets use upper-case letters. | |
76 name = name.lower() | |
77 | |
78 # If 'name' is already a 'cbuildbot' target, return it unmodified. | |
79 yield name | |
80 | |
81 # Apply static permutations. | |
82 p = cls._STATIC_PERMUTATIONS.get(name) | |
83 if p: | |
84 yield p | |
85 | |
86 # Replace 'canary' with 'release'. | |
87 for find, replace in cls._TRANSFORMATIONS: | |
88 name = re.sub(find, replace, name) | |
89 yield name | |
90 | |
91 # Is 'name' valid if it was a release group? | |
92 if not name.endswith('-group'): | |
93 # We never build 'full' group variants. | |
94 name_group = ('%s-group' % (name,)).replace('-full-', '-release-') | |
95 yield name_group | |
96 | |
97 @classmethod | |
98 def process(cls, config, name, branches=None, blacklist=None): | |
99 """Construct an UpdateInfo to map a source name. | |
100 | |
101 This function works by attempting to transform a source name into a known | |
102 'cbuildbot' target name. If successful, it will use that successful | |
103 transformation as validation of the correctness and return an UpdateInfo | |
104 describing the transformation. | |
105 | |
106 Args: | |
107 config (cros_chromite.ChromiteConfig) The Chromite config instance. | |
108 name (str): The source name to process and map. | |
109 branches (list): A list of valid branches, extracted from 'cros_chromite'. | |
110 Returns (UpdateInfo/None): The constructed UpdateInfo, or None if there was | |
111 no identified mapping. | |
112 """ | |
113 def sliding_split_gen(): | |
114 parts = name.split('-') | |
115 for i in xrange(len(parts), 0, -1): | |
116 yield '-'.join(parts[:i]), '-'.join(parts[i:]) | |
117 | |
118 logging.debug("Processing candidate name: %s", name) | |
119 candidates = set() | |
120 branch = None | |
121 for orig_name, branch in sliding_split_gen(): | |
122 logging.debug("Trying construction: Name(%s), Branch(%s)", | |
123 orig_name, branch) | |
124 if branches and not branch in branches: | |
125 logging.debug("Ignoring branch value '%s'.", branch) | |
126 continue | |
127 | |
128 # See if we can properly permute the original name. | |
129 for permuted_name in cls.permutations(orig_name): | |
130 if blacklist and any(b in permuted_name for b in blacklist): | |
131 logging.debug("Ignoring blacklisted config name: %s", permuted_name) | |
132 continue | |
133 if permuted_name in config: | |
134 candidates.add(permuted_name) | |
135 if not candidates: | |
136 logging.debug("No 'cbuildbot' config for attempts [%s] branch [%s].", | |
137 orig_name, branch) | |
138 continue | |
139 | |
140 # We've found a permutation that matches a 'cbuildbot' target. | |
141 break | |
142 else: | |
143 logging.info("No 'cbuildbot' permutations for [%s].", name) | |
144 return None | |
145 | |
146 if not branch: | |
147 # We need to do an update to add the branch. Default to 'master'. | |
148 branch = 'master' | |
149 | |
150 candidates = sorted(candidates) | |
151 for candidate in candidates: | |
152 logging.debug("Identified 'cbuildbot' name [%s] => [%s] branch [%s].", | |
153 name, candidate, branch) | |
154 return [cls(name, p, branch) for p in candidates] | |
155 | |
156 | |
157 def main(args): | |
158 """Main execution function. | |
159 | |
160 Args: | |
161 args (list): Command-line argument array. | |
162 """ | |
163 parser = argparse.ArgumentParser() | |
164 parser.add_argument('path', nargs='+', metavar='PATH', | |
165 help='The path to the master directory to process.') | |
166 parser.add_argument('-v', '--verbose', action='count', default=0, | |
167 help='Increase verbosity. Can be specified multiple times.') | |
168 parser.add_argument('-d', '--dry-run', action='store_true', | |
169 help="Print what actions will be taken, but don't modify anything.") | |
170 parser.add_argument('-n', '--names', action='store_true', | |
171 help="If specified, then regard 'path' as directory names to test.") | |
172 parser.add_argument('-B', '--blacklist', action='append', default=[], | |
173 help="Blacklist configs that contain this text.") | |
174 args = parser.parse_args() | |
175 | |
176 # Select verbosity. | |
177 if args.verbose == 0: | |
178 loglevel = logging.WARNING | |
179 elif args.verbose == 1: | |
180 loglevel = logging.INFO | |
181 else: | |
182 loglevel = logging.DEBUG | |
183 logging.getLogger().setLevel(loglevel) | |
184 | |
185 # Load all availables Chromite configs. We're going to load ToT. | |
186 config_names = set() | |
187 branches = set() | |
188 for branch in cros_chromite.PINS.iterkeys(): | |
189 branches.add(branch) | |
190 config_names.update(cros_chromite.Get(branch=branch).iterkeys()) | |
191 | |
192 # If we're just testing against names, do that. | |
193 if args.names: | |
194 errors = 0 | |
195 for n in args.path: | |
196 update_info_list = UpdateInfo.process(config_names, n, branches=branches, | |
197 blacklist=args.blacklist) | |
198 if update_info_list: | |
199 for update_info in update_info_list: | |
200 logging.warning("[%s] => [%s]", update_info.src, update_info.dst) | |
201 else: | |
202 logging.warning("No transformation for name [%s].", n) | |
203 errors += 1 | |
204 return errors | |
205 | |
206 # Construct the set of actions to take. | |
207 cbb_already = set() | |
208 unmatched = set() | |
209 multiples = {} | |
210 updates = [] | |
211 for path in args.path: | |
212 if not os.path.isdir(path): | |
213 raise ValueError("Supplied master directory is not valid: %s" % (path,)) | |
214 | |
215 seen = set() | |
216 for f in os.listdir(path): | |
217 f_path = os.path.join(path, f) | |
218 if not os.path.isdir(f_path): | |
219 continue | |
220 | |
221 update_info_list = UpdateInfo.process(config_names, f, branches=branches, | |
222 blacklist=args.blacklist) | |
223 if not update_info_list: | |
224 logging.info("No update information for directory [%s]", f) | |
225 unmatched.add(f) | |
226 continue | |
227 elif len(update_info_list) != 1: | |
228 multiples[f] = update_info_list | |
229 continue | |
230 update_info = update_info_list[0] | |
231 | |
232 # Make sure that we don't stomp on directory names. This shouldn't happen, | |
233 # since the mapping to 'cbuildbot' names is inherently deconflicting, but | |
234 # it's good to assert it just in case. | |
235 update_info_names = set((update_info.src, update_info.dst)) | |
236 if update_info_names.intersection(seen): | |
237 logging.error("Updated names intersect with existing names: %s", | |
238 ", ".join(update_info_names.intersection(seen))) | |
239 return 1 | |
240 seen.update(update_info_names) | |
241 | |
242 # We are already in <cbuildbot>-<branch> format, so do nothing. | |
243 if update_info.src == update_info.dst: | |
244 cbb_already.add(update_info.src) | |
245 else: | |
246 updates.append((path, update_info)) | |
247 | |
248 # Execute the updates. | |
249 logging.info("Executing %d updates.", len(updates)) | |
250 for master_dir, update_info in updates: | |
251 logging.info("Updating [%s]: [%s] => [%s]", master_dir, update_info.src, | |
252 update_info.dst) | |
253 if not args.dry_run: | |
254 shutil.move(os.path.join(master_dir, update_info.src), | |
255 os.path.join(master_dir, update_info.dst)) | |
256 logging.info("Updated %d directories.", len(updates)) | |
257 if logging.getLogger().isEnabledFor(logging.DEBUG): | |
258 logging.debug("%d directories already matching: %s", | |
259 len(cbb_already), ', '.join(sorted(cbb_already))) | |
260 if unmatched: | |
261 logging.warning("%d unmatched directories: %s", | |
262 len(unmatched), ', '.join(sorted(unmatched))) | |
263 if multiples: | |
264 for f in sorted(multiples.iterkeys()): | |
265 logging.warning("Multiple permutations of [%s]: %s\n%s", | |
266 f, ", ".join(m.dst for m in multiples[f]), | |
267 '\n'.join('mv %s %s' % (f, m.dst) for m in multiples[f])) | |
268 return 0 | |
269 | |
270 | |
271 if __name__ == '__main__': | |
272 logging.basicConfig() | |
273 sys.exit(main(sys.argv[1:])) | |
OLD | NEW |