OLD | NEW |
(Empty) | |
| 1 #!/usr/bin/python |
| 2 # Copyright 2016 Google Inc. All Rights Reserved. |
| 3 # pylint: disable=F0401 |
| 4 |
| 5 """Cleanup directories on BuildBot master systems.""" |
| 6 |
| 7 import argparse |
| 8 import bisect |
| 9 import datetime |
| 10 import json |
| 11 import logging |
| 12 import os |
| 13 import shutil |
| 14 import subprocess |
| 15 import sys |
| 16 import time |
| 17 |
| 18 from infra_libs import logs |
| 19 from infra_libs.time_functions.parser import argparse_timedelta_type |
| 20 |
| 21 |
| 22 LOGGER = logging.getLogger(__name__) |
| 23 |
| 24 |
| 25 def _check_run(cmd, dry_run=True, cwd=None): |
| 26 if cwd is None: |
| 27 cwd = os.getcwd() |
| 28 |
| 29 if dry_run: |
| 30 LOGGER.info('(Dry run) Running command %s (cwd=%s)', cmd, cwd) |
| 31 return '', '' |
| 32 |
| 33 LOGGER.debug('Running command %s (cwd=%s)', cmd, cwd) |
| 34 proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, |
| 35 cwd=cwd) |
| 36 stdout, stderr = proc.communicate() |
| 37 |
| 38 rc = proc.returncode |
| 39 if rc != 0: |
| 40 LOGGER.error('Output for process %s (rc=%d, cwd=%s):\n' |
| 41 'STDOUT:\n%s\nSTDERR:\n%s', |
| 42 cmd, rc, cwd, stdout, stderr) |
| 43 raise subprocess.CalledProcessError(rc, cmd, None) |
| 44 return stdout, stderr |
| 45 |
| 46 |
| 47 def parse_args(argv): |
| 48 parser = argparse.ArgumentParser(description=__doc__) |
| 49 parser.add_argument('master', nargs='+', |
| 50 help='Name of masters (*, master.*) to clean.') |
| 51 parser.add_argument('--max-twistd-log-age', metavar='AGE-TOKENS', |
| 52 default=None, type=argparse_timedelta_type, |
| 53 help='If set, "twistd.log" files older than this will be purged.') |
| 54 parser.add_argument('--production', action='store_true', |
| 55 help='If set, actually delete the files instead of listing them.') |
| 56 parser.add_argument('--gclient-root', |
| 57 help='The path to the directory containing the master checkout ' |
| 58 '".gclient" file. If omitted, an attempt will be made to probe ' |
| 59 'one.') |
| 60 |
| 61 logs.add_argparse_options(parser) |
| 62 |
| 63 opts = parser.parse_args(argv) |
| 64 logs.process_argparse_options(opts) |
| 65 return opts |
| 66 |
| 67 |
| 68 def _process_master(opts, master_cfg): |
| 69 LOGGER.info('Cleaning up master: %s', master_cfg['mastername']) |
| 70 |
| 71 # Get a list of all files within the master directory. |
| 72 master_dir = master_cfg['master_dir'] |
| 73 files, dirs = _list_untracked_files(master_dir) |
| 74 |
| 75 # Run a filter to identify all "builder" directories that are not currently |
| 76 # configured to the master. |
| 77 dirs = [x for x in dirs if ( |
| 78 x not in master_cfg['builddirs'] and |
| 79 _is_builder_dir(os.path.join(master_dir, x)))] |
| 80 LOGGER.info('Identified %d superfluous build directories.', len(dirs)) |
| 81 |
| 82 # Find old "twistd.log" files. |
| 83 old_twistd_logs = _find_old_twistd_logs(master_dir, files, |
| 84 opts.max_twistd_log_age) |
| 85 if len(old_twistd_logs) > 0: |
| 86 LOGGER.info('Identified %d old twistd.log files, starting with %s.', |
| 87 len(old_twistd_logs), old_twistd_logs[-1]) |
| 88 |
| 89 for d in dirs: |
| 90 d = os.path.join(master_dir, d) |
| 91 LOGGER.info('Deleting superfluous directory: [%s]', d) |
| 92 if not opts.production: |
| 93 LOGGER.info('(Dry Run) Not deleting.') |
| 94 continue |
| 95 shutil.rmtree(d) |
| 96 |
| 97 for f in old_twistd_logs: |
| 98 f = os.path.join(master_dir, f) |
| 99 LOGGER.info('Removing old "twistd.log" file: [%s]', f) |
| 100 if not opts.production: |
| 101 LOGGER.info('(Dry Run) Not deleting.') |
| 102 continue |
| 103 os.remove(f) |
| 104 |
| 105 |
| 106 def _find_old_twistd_logs(base, files, max_age): |
| 107 twistd_log_files = [] |
| 108 if max_age is None: |
| 109 return twistd_log_files |
| 110 |
| 111 # Identify all "twistd.log" files to delete. We will do this by binary |
| 112 # searching the "twistd.log" space under the assumption that any log files |
| 113 # with higher generation than the specified file are older than files with |
| 114 # lower index. |
| 115 for f in files: |
| 116 gen = _parse_twistd_log_generation(f) |
| 117 if gen is not None: |
| 118 twistd_log_files.append((f, gen)) |
| 119 twistd_log_files.sort(key=lambda x: x[1], reverse=True) |
| 120 |
| 121 threshold = datetime.datetime.now() - max_age |
| 122 lo, hi = 0, len(twistd_log_files) |
| 123 while lo < hi: |
| 124 mid = (lo+hi)//2 |
| 125 path = os.path.join(base, twistd_log_files[mid][0]) |
| 126 create_time = datetime.datetime.fromtimestamp(os.path.getctime(path)) |
| 127 if create_time < threshold: |
| 128 hi = mid |
| 129 else: |
| 130 lo = mid+1 |
| 131 return [x[0] for x in twistd_log_files[:lo]] |
| 132 |
| 133 |
| 134 def _parse_twistd_log_generation(v): |
| 135 # Format is: "twistd.log[.###]" |
| 136 pieces = v.split('.') |
| 137 if len(pieces) != 3 or not (pieces[0] == 'twistd' and pieces[1] == 'log'): |
| 138 return None |
| 139 |
| 140 try: |
| 141 return int(pieces[2]) |
| 142 except ValueError: |
| 143 return None |
| 144 |
| 145 |
| 146 def _list_untracked_files(path): |
| 147 cmd = ['git', '-C', path, 'ls-files', '.', '--others', '--directory', '-z'] |
| 148 stdout, _ = _check_run(cmd, dry_run=False) |
| 149 files, dirs = [], [] |
| 150 |
| 151 def iter_null_terminated(data): |
| 152 while True: |
| 153 idx = data.find('\0') |
| 154 if idx < 0: |
| 155 yield data |
| 156 return |
| 157 v, data = data[:idx], data[idx+1:] |
| 158 yield v |
| 159 |
| 160 for name in iter_null_terminated(stdout): |
| 161 if name.endswith('/'): |
| 162 dirs.append(name.rstrip('/')) |
| 163 else: |
| 164 files.append(name) |
| 165 return files, dirs |
| 166 |
| 167 |
| 168 def _is_builder_dir(dirname): |
| 169 return os.path.isfile(os.path.join(dirname, 'builder')) |
| 170 |
| 171 |
| 172 def _load_master_cfg(gclient_root, master_dir): |
| 173 dump_master_cfg = os.path.join(gclient_root, 'build', 'scripts', 'tools', |
| 174 'dump_master_cfg.py') |
| 175 |
| 176 cmd = [sys.executable, dump_master_cfg, master_dir, '-'] |
| 177 config, _ = _check_run(cmd, dry_run=False) |
| 178 config = json.loads(config) |
| 179 |
| 180 result = { |
| 181 'mastername': os.path.split(master_dir)[-1], |
| 182 'master_dir': master_dir, |
| 183 'builddirs': set(), |
| 184 } |
| 185 for bcfg in config.get('builders', ()): |
| 186 result['builddirs'].add(bcfg.get('builddir') or bcfg['name']) |
| 187 return result |
| 188 |
| 189 |
| 190 def _find_master(gclient_root, mastername): |
| 191 if not mastername.startswith('master.'): |
| 192 mastername = 'master.' + mastername |
| 193 |
| 194 for candidate in ( |
| 195 os.path.join(gclient_root, 'build', 'masters'), |
| 196 os.path.join(gclient_root, 'build_internal', 'masters'), |
| 197 ): |
| 198 candidate = os.path.join(candidate, mastername) |
| 199 if os.path.isdir(candidate): |
| 200 return candidate |
| 201 raise ValueError('Unable to locate master %s' % (mastername,)) |
| 202 |
| 203 |
| 204 def _find_gclient_root(opts): |
| 205 for candidate in ( |
| 206 opts.gclient_root, |
| 207 os.path.join(os.path.expanduser('~'), 'buildbot'), |
| 208 ): |
| 209 if not candidate: |
| 210 continue |
| 211 candidate = os.path.abspath(candidate) |
| 212 if os.path.isfile(os.path.join(candidate, '.gclient')): |
| 213 return candidate |
| 214 raise Exception('Unable to find ".gclient" root.') |
| 215 |
| 216 |
| 217 def _trim_prefix(v, prefix): |
| 218 if v.startswith(prefix): |
| 219 v = v[len(prefix)] |
| 220 return v |
| 221 |
| 222 |
| 223 def _main(argv): |
| 224 opts = parse_args(argv) |
| 225 |
| 226 # Locate our gclient file root. |
| 227 gclient_root = _find_gclient_root(opts) |
| 228 |
| 229 # Dump the builders configured for each master. |
| 230 for master in sorted(set(opts.master)): |
| 231 LOGGER.info('Loading configuration for master "%s"...', master) |
| 232 master_dir = _find_master(gclient_root, master) |
| 233 master_cfg = _load_master_cfg(gclient_root, master_dir) |
| 234 _process_master(opts, master_cfg) |
| 235 |
| 236 return 0 |
| 237 |
| 238 if __name__ == '__main__': |
| 239 sys.exit(_main(sys.argv[1:])) |
OLD | NEW |