-
Notifications
You must be signed in to change notification settings - Fork 54
/
clobberer.py
executable file
·328 lines (274 loc) · 11.7 KB
/
clobberer.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
#!/usr/bin/python
# vim:sts=4 sw=4
import sys
import shutil
import urllib2
import urllib
import json
import os
import time
import socket
import logging
log = logging.getLogger()
if os.name == 'nt':
from win32file import RemoveDirectory, DeleteFile, \
SetFileAttributesW, \
FILE_ATTRIBUTE_NORMAL, FILE_ATTRIBUTE_DIRECTORY
from win32api import FindFiles
clobber_suffix = '.deleteme'
def get_hostname():
return socket.gethostname().split('.')[0]
def ts_to_str(ts):
if ts is None:
return None
return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(ts))
def write_file(ts, fn):
assert isinstance(ts, int)
f = open(fn, "w")
f.write(str(ts))
f.close()
def read_file(fn):
if not os.path.exists(fn):
return None
data = open(fn).read().strip()
try:
return int(data)
except ValueError:
return None
def safe_join(parent, path):
"""Returns $parent/$path
Raises IOError in case the resulting path ends up outside of parent for
some reason (e.g. by using ../../..)
"""
while os.path.isabs(path):
path = path.lstrip("/")
drive, tail = os.path.splitdrive(path)
path = tail
path = os.path.normpath(path)
if path.startswith("../"):
raise IOError("unsafe join of %s/%s" % (parent, path))
return os.path.join(parent, path)
def rmdir_recursive_windows(dir):
"""Windows-specific version of rmdir_recursive that handles
path lengths longer than MAX_PATH.
"""
dir = os.path.realpath(dir)
# Make sure directory is writable
SetFileAttributesW('\\\\?\\' + dir, FILE_ATTRIBUTE_NORMAL)
for ffrec in FindFiles('\\\\?\\' + dir + '\\*.*'):
file_attr = ffrec[0]
name = ffrec[8].decode(sys.getfilesystemencoding())
if name == '.' or name == '..':
continue
full_name = os.path.join(dir, name)
if file_attr & FILE_ATTRIBUTE_DIRECTORY:
rmdir_recursive_windows(full_name)
else:
SetFileAttributesW('\\\\?\\' + full_name, FILE_ATTRIBUTE_NORMAL)
DeleteFile('\\\\?\\' + full_name)
RemoveDirectory('\\\\?\\' + dir)
def rmdir_recursive(dir):
"""This is a replacement for shutil.rmtree that works better under
windows. Thanks to Bear at the OSAF for the code.
(Borrowed from buildbot.slave.commands)"""
if os.name == 'nt':
rmdir_recursive_windows(dir)
return
if not os.path.exists(dir):
# This handles broken links
if os.path.islink(dir):
os.remove(dir)
return
if os.path.islink(dir):
os.remove(dir)
return
# Verify the directory is read/write/execute for the current user
os.chmod(dir, 0o700)
for name in os.listdir(dir):
full_name = os.path.join(dir, name)
# on Windows, if we don't have write permission we can't remove
# the file/directory either, so turn that on
if os.name == 'nt':
if not os.access(full_name, os.W_OK):
# I think this is now redundant, but I don't have an NT
# machine to test on, so I'm going to leave it in place
# -warner
os.chmod(full_name, 0o600)
if os.path.isdir(full_name):
rmdir_recursive(full_name)
else:
# Don't try to chmod links
if not os.path.islink(full_name):
os.chmod(full_name, 0o700)
os.remove(full_name)
os.rmdir(dir)
def do_clobber(dir, dryrun=False, skip=None):
try:
for f in os.listdir(dir):
if skip is not None and f in skip:
log.info("Skipping %s", f)
continue
clobber_path = f + clobber_suffix
if os.path.isfile(f):
log.info("Removing %s", f)
if not dryrun:
if os.path.exists(clobber_path):
os.unlink(clobber_path)
# Prevent repeated moving.
if f.endswith(clobber_suffix):
os.unlink(f)
else:
shutil.move(f, clobber_path)
os.unlink(clobber_path)
elif os.path.isdir(f):
log.info("Removing %s/", f)
if not dryrun:
if os.path.exists(clobber_path):
rmdir_recursive(clobber_path)
# Prevent repeated moving.
if f.endswith(clobber_suffix):
rmdir_recursive(f)
else:
shutil.move(f, clobber_path)
rmdir_recursive(clobber_path)
except Exception as e:
log.error("[clobber failed]: %s", e.message)
sys.exit(1)
def get_clobber_times(clobber_url):
log.info("Checking clobber URL: %s", clobber_url)
data = urllib2.urlopen(clobber_url, timeout=30).read()
return json.loads(data)
def legacy_get_clobber_times(clobber_url, branch, buildername, builddir, slave, master=None):
params = dict(branch=branch, buildername=buildername,
builddir=builddir, slave=slave, master=master)
url = "%s?%s" % (clobber_url, urllib.urlencode(params))
# The timeout arg was added to urlopen() at Python 2.6
# Deprecate this test when esr17 reaches EOL
if sys.version_info[:2] < (2, 6):
data = urllib2.urlopen(url).read().strip()
else:
data = urllib2.urlopen(url, timeout=30).read().strip()
processed_data = data.strip().split(':')
if len(processed_data) < 2:
# We received an empty result, time to bail
return {'result': []}
builddir_result, lastclobber, who = processed_data
lastclobber = int(lastclobber)
# return a dictionary that mimicks the new endpoint
# json format
return {
'result': [{
'builddir': builddir_result,
'lastclobber': lastclobber,
'who': who,
'slave': None
}]
}
def process_clobber_times(server_clobber_times, args):
now = int(time.time())
root_dir = os.path.abspath(args.dir)
for clobber_time in server_clobber_times['result']:
slave = clobber_time['slave']
builddir = clobber_time['builddir']
server_clobber_date = clobber_time['lastclobber']
who = clobber_time['who']
builder_dir = safe_join(root_dir, builddir)
if not os.path.isdir(builder_dir):
log.info("%s doesn't exist, skipping", builder_dir)
continue
os.chdir(builder_dir)
our_clobber_date = read_file("last-clobber") or 0
clobber = False
clobber_type = None
log.info("%s:Our last clobber date: %s", builddir, ts_to_str(our_clobber_date))
log.info("%s:Server clobber date: %s", builddir, ts_to_str(server_clobber_date))
# If we don't have a last clobber date, then this is probably a fresh build.
# We should only do a forced server clobber if we know when our last clobber
# was, and if the server date is more recent than that.
if server_clobber_date is not None and our_clobber_date is not None:
# If the server is giving us a clobber date, compare the server's idea of
# the clobber date to our last clobber date also skip slave specific
# clobbers that aren't for the current host
if (server_clobber_date > our_clobber_date) and (slave is None or slave == args.slave):
# If the server's clobber date is greater than our last clobber date,
# then we should clobber.
clobber = True
clobber_type = "forced"
# We should also update our clobber date to match the server's
our_clobber_date = server_clobber_date
if who:
log.info("%s:Server is forcing a clobber, initiated by %s", builddir, who)
else:
log.info("%s:Server is forcing a clobber", builddir)
if not clobber:
# Disable periodic clobbers for builders that aren't args.builddir
if builddir != args.builddir:
continue
# Next, check if more than the periodic_clobber_time period has passed since
# our last clobber
if our_clobber_date is None:
# We've never been clobbered
# Set our last clobber time to now, so that we'll clobber
# properly after periodic_clobber_time
clobber_type = "purged"
our_clobber_date = now
write_file(our_clobber_date, "last-clobber")
elif args.periodic_clobber_time and now > our_clobber_date + args.periodic_clobber_time:
# periodic_clobber_time has passed since our last clobber
clobber = True
clobber_type = "periodic"
# Update our clobber date to now
our_clobber_date = now
log.info("%s:More than %s seconds have passed since our last clobber",
(builddir, args.periodic_clobber_time))
if clobber:
# Finally, perform a clobber if we're supposed to
log.info("%s:Clobbering...", builddir)
do_clobber(builder_dir, args.dryrun, args.skip)
write_file(our_clobber_date, "last-clobber")
# If this is the build dir for the current job, display the clobber type in TBPL.
# Note in the case of purged clobber, we output the clobber type even though no
# clobber was performed this time.
if clobber_type and builddir == args.builddir:
log.info("Tinderboxlog.info: %s clobber" % clobber_type)
def make_argparser():
import optparse
parser = optparse.OptionParser(
"%prog [options] clobberURL branch buildername builddir slave master"
)
parser.add_option("-n", "--dry-run", dest="dryrun", action="store_true",
default=False, help="don't actually delete anything")
parser.add_option("-t", "--periodic", dest="period", type=float,
default=None, help="hours between periodic clobbers")
parser.add_option('-s', '--skip', help='do not delete this file/directory',
action='append', dest='skip', default=['last-clobber'])
parser.add_option('-d', '--dir', help='clobber this directory',
dest='dir', default='.')
parser.add_option('--builddir', help='a specific builddir for periodic clobbers')
parser.add_option('-v', '--verbose', help='be more verbose',
dest='loglevel', action='store_const',
default=logging.INFO, const=logging.DEBUG)
parser.add_option('--url', dest='clobber_url', help='URL to clobberer service')
parser.add_option('--slave', default=get_hostname(),
help='Name of the slave being clobbered (defaults to hostname)')
return parser
def main(args, legacy=None):
if args.period:
args.periodic_clobber_time = args.period * 3600
else:
args.periodic_clobber_time = None
# Legacy support
if legacy and len(legacy) > 1:
log.info("Entering legacy mode.")
server_clobber_times = legacy_get_clobber_times(*legacy)
elif not args.clobber_url:
# we can't set required=True because of legacy support
raise Exception("--url required")
else:
server_clobber_times = get_clobber_times(args.clobber_url)
process_clobber_times(server_clobber_times, args)
if __name__ == "__main__":
parser = make_argparser()
parsed_args, legacy = parser.parse_args()
logging.basicConfig(level=parsed_args.loglevel, format="%(asctime)s - %(message)s")
main(parsed_args, legacy=legacy)