Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(133)

Side by Side Diff: infra/tools/master_cleaner/__main__.py

Issue 2059833002: Add master_cleaner tool. (Closed) Base URL: https://chromium.googlesource.com/infra/infra.git@master
Patch Set: Comments. Created 4 years, 6 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « infra/tools/master_cleaner/__init__.py ('k') | infra_libs/time_functions/parser.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(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:]))
OLDNEW
« no previous file with comments | « infra/tools/master_cleaner/__init__.py ('k') | infra_libs/time_functions/parser.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698