| 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'(x86|amd64)$', r'\1-generic'), |
| 54 (r'^chromium-tot-chromeos-(.+)-asan', r'\1-tot-asan-informational'), |
| 55 (r'^chromium-tot-chromeos-(.+)', r'\1-tot-chrome-pfq-informational'), |
| 56 (r'^chromium-(.+)-telemetry$', r'\1-telemetry'), |
| 57 (r'(.+)-bin$', r'\1'), |
| 58 ) |
| 59 |
| 60 @property |
| 61 def dst(self): |
| 62 """Constructs the <cbuildbot>-<branch> form.""" |
| 63 return '%s-%s' % (self.cbb_name, self.branch) |
| 64 |
| 65 @classmethod |
| 66 def permutations(cls, name): |
| 67 """Attempts to permute a legacy BuildBot name into a Chromite target. |
| 68 |
| 69 Args: |
| 70 name (str): The source name to process and map. |
| 71 Yields (str): Various permutations of 'name'. |
| 72 """ |
| 73 # No cbuildbot targets use upper-case letters. |
| 74 name = name.lower() |
| 75 |
| 76 # If 'name' is already a 'cbuildbot' target, return it unmodified. |
| 77 yield name |
| 78 |
| 79 # Apply static permutations. |
| 80 p = cls._STATIC_PERMUTATIONS.get(name) |
| 81 if p: |
| 82 yield p |
| 83 |
| 84 # Replace 'canary' with 'release'. |
| 85 for find, replace in cls._TRANSFORMATIONS: |
| 86 name = re.sub(find, replace, name) |
| 87 print find, replace, name |
| 88 yield name |
| 89 |
| 90 # Is 'name' valid if it was a release group? |
| 91 if not name.endswith('-group'): |
| 92 # We never build 'full' group variants. |
| 93 name_group = ('%s-group' % (name,)).replace('-full-', '-release-') |
| 94 yield name_group |
| 95 |
| 96 @classmethod |
| 97 def process(cls, config, name, default_branch): |
| 98 """Construct an UpdateInfo to map a source name. |
| 99 |
| 100 This function works by attempting to transform a source name into a known |
| 101 'cbuildbot' target name. If successful, it will use that successful |
| 102 transformation as validation of the correctness and return an UpdateInfo |
| 103 describing the transformation. |
| 104 |
| 105 Args: |
| 106 config (cros_chromite.ChromiteConfig) The Chromite config instance. |
| 107 name (str): The source name to process and map. |
| 108 default_branch (str): The default branch to apply if the field is empty. |
| 109 Returns (UpdateInfo/None): The constructed UpdateInfo, or None if there was |
| 110 no identified mapping. |
| 111 """ |
| 112 def sliding_split_gen(): |
| 113 parts = name.split('-') |
| 114 for i in xrange(len(parts), 0, -1): |
| 115 yield '-'.join(parts[:i]), '-'.join(parts[i:]) |
| 116 |
| 117 logging.debug("Processing candidate name: %s", name) |
| 118 candidates = set() |
| 119 branch = None |
| 120 for orig_name, branch in sliding_split_gen(): |
| 121 logging.debug("Trying construction: Name(%s), Branch(%s)", |
| 122 orig_name, branch) |
| 123 |
| 124 # See if we can properly permute the original name. |
| 125 for permuted_name in cls.permutations(orig_name): |
| 126 if permuted_name in config: |
| 127 candidates.add(permuted_name) |
| 128 if not candidates: |
| 129 logging.debug("No 'cbuildbot' config for attempts [%s] branch [%s].", |
| 130 orig_name, branch) |
| 131 continue |
| 132 |
| 133 # We've found a permutation that matches a 'cbuildbot' target. |
| 134 break |
| 135 else: |
| 136 logging.info("No 'cbuildbot' permutations for [%s].", name) |
| 137 return None |
| 138 |
| 139 if not branch: |
| 140 # We need to do an update to add the branch. Default to 'master'. |
| 141 branch = default_branch |
| 142 |
| 143 candidates = sorted(candidates) |
| 144 for candidate in candidates: |
| 145 logging.debug("Identified 'cbuildbot' name [%s] => [%s] branch [%s].", |
| 146 name, candidate, branch) |
| 147 return [cls(name, p, branch) for p in candidates] |
| 148 |
| 149 |
| 150 def main(args): |
| 151 """Main execution function. |
| 152 |
| 153 Args: |
| 154 args (list): Command-line argument array. |
| 155 """ |
| 156 parser = argparse.ArgumentParser() |
| 157 parser.add_argument('path', nargs='+', metavar='PATH', |
| 158 help='The path to the master directory to process.') |
| 159 parser.add_argument('-v', '--verbose', action='count', default=0, |
| 160 help='Increase verbosity. Can be specified multiple times.') |
| 161 parser.add_argument('-d', '--dry-run', action='store_true', |
| 162 help="Print what actions will be taken, but don't modify anything.") |
| 163 parser.add_argument('-B', '--branch', default='master', |
| 164 help="The branch to use, if one is not present (default is %(default)s)") |
| 165 parser.add_argument('-n', '--names', action='store_true', |
| 166 help="If specified, then regard 'path' as directory names to test.") |
| 167 args = parser.parse_args() |
| 168 |
| 169 # Select verbosity. |
| 170 if args.verbose == 0: |
| 171 loglevel = logging.WARNING |
| 172 elif args.verbose == 1: |
| 173 loglevel = logging.INFO |
| 174 else: |
| 175 loglevel = logging.DEBUG |
| 176 logging.getLogger().setLevel(loglevel) |
| 177 |
| 178 # Load our Chromite config. We're going to load ToT. |
| 179 cbuildbot_config = cros_chromite.Get() |
| 180 |
| 181 # If we're just testing against names, do that. |
| 182 if args.names: |
| 183 errors = 0 |
| 184 for n in args.path: |
| 185 update_info = UpdateInfo.process(cbuildbot_config, n, args.branch) |
| 186 if update_info: |
| 187 logging.warning("[%s] => [%s]", update_info.src, update_info.dst) |
| 188 else: |
| 189 logging.warning("No transformation for name [%s].", n) |
| 190 errors += 1 |
| 191 return errors |
| 192 |
| 193 # Construct the set of actions to take. |
| 194 cbb_already = set() |
| 195 unmatched = set() |
| 196 multiples = {} |
| 197 updates = [] |
| 198 for path in args.path: |
| 199 if not os.path.isdir(path): |
| 200 raise ValueError("Supplied master directory is not valid: %s" % (path,)) |
| 201 |
| 202 seen = set() |
| 203 for f in os.listdir(path): |
| 204 f_path = os.path.join(path, f) |
| 205 if not os.path.isdir(f_path): |
| 206 continue |
| 207 |
| 208 update_info_list = UpdateInfo.process(cbuildbot_config, f, args.branch) |
| 209 if not update_info_list: |
| 210 logging.info("No update information for directory [%s]", f) |
| 211 unmatched.add(f) |
| 212 continue |
| 213 elif len(update_info_list) != 1: |
| 214 multiples[f] = update_info_list |
| 215 continue |
| 216 update_info = update_info_list[0] |
| 217 |
| 218 # Make sure that we don't stomp on directory names. This shouldn't happen, |
| 219 # since the mapping to 'cbuildbot' names is inherently deconflicting, but |
| 220 # it's good to assert it just in case. |
| 221 update_info_names = set((update_info.src, update_info.dst)) |
| 222 if update_info_names.intersection(seen): |
| 223 logging.error("Updated names intersect with existing names: %s", |
| 224 ", ".join(update_info_names.intersection(seen))) |
| 225 return 1 |
| 226 seen.update(update_info_names) |
| 227 |
| 228 # We are already in <cbuildbot>-<branch> format, so do nothing. |
| 229 if update_info.src == update_info.dst: |
| 230 cbb_already.add(update_info.src) |
| 231 else: |
| 232 updates.append((path, update_info)) |
| 233 |
| 234 # Execute the updates. |
| 235 logging.info("Executing %d updates.", len(updates)) |
| 236 for master_dir, update_info in updates: |
| 237 logging.info("Updating [%s]: [%s] => [%s]", master_dir, update_info.src, |
| 238 update_info.dst) |
| 239 if not args.dry_run: |
| 240 shutil.move(os.path.join(master_dir, update_info.src), |
| 241 os.path.join(master_dir, update_info.dst)) |
| 242 logging.info("Updated %d directories.", len(updates)) |
| 243 if logging.getLogger().isEnabledFor(logging.DEBUG): |
| 244 logging.debug("%d directories already matching: %s", |
| 245 len(cbb_already), ', '.join(sorted(cbb_already))) |
| 246 if unmatched: |
| 247 logging.warning("%d unmatched directories: %s", |
| 248 len(unmatched), ', '.join(sorted(unmatched))) |
| 249 if multiples: |
| 250 for f in sorted(multiples.iterkeys()): |
| 251 logging.warning("Multiple permutations of [%s]: %s", |
| 252 f, ", ".join(m.dst for m in multiples[f])) |
| 253 return 0 |
| 254 |
| 255 |
| 256 if __name__ == '__main__': |
| 257 logging.basicConfig() |
| 258 sys.exit(main(sys.argv[1:])) |
| OLD | NEW |