-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathmanage-replicas
executable file
·300 lines (282 loc) · 10.4 KB
/
manage-replicas
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
#!/usr/bin/python3
#
# Manage adding and removing CVMFS replicas based on a configuration
# file and the repositories list from stratum 0s or other stratum 1s
# Written by Dave Dykstra, February 2017
from optparse import OptionParser
import urllib.request, urllib.error, urllib.parse
import sys
import os
import time
import json
import subprocess
import fnmatch
prog = 'manage-replicas'
addcmd = 'add-repository @fqrn@ @url@'
remcmd = 'remove-repository -f @fqrn@'
replist = ''
source = ''
keypath = ''
keysource = ''
excludes = []
oldrepos = []
repospecs = {}
replicalist = []
usagestr = "Usage: %prog [-h ] [ other_options ]\n" \
" Use -h to see help"
parser = OptionParser(usage=usagestr, prog=prog)
parser.add_option("-f", "--config",
metavar="file", default="/etc/cvmfs/manage-replicas.conf",
help="use file as configuration file. Default value: /etc/cvmfs/manage-replicas.conf")
parser.add_option("-n", "--dry-run",
action="store_true", default=False,
help="Show what would be done, don't actually do it")
parser.add_option("-l", "--list",
action="store_true", default=False,
help="Show full source url of all repos and exit")
parser.add_option("-r", "--remove",
action="store_true", default=False,
help="Remove repositories that don't belong, don't just warn about them")
parser.add_option("-c", "--continue-failed",
action="store_true", default=False,
help="Continue failed initial snapshots, replacing url with \'continue\'")
parser.add_option("-d", "--repo-directory",
metavar="dir", default='/srv/cvmfs',
help="Directory to find repositories in (for -c option)")
parser.add_option("-k", "--only-download-keys",
action="store_true", default=False,
help="Only download any missing keys, do not add replicas")
(options, args) = parser.parse_args(sys.argv)
def logmsg(msg):
print(time.asctime(time.localtime(time.time())) + ' ' + msg)
sys.stdout.flush()
def fatal(msg, code=1):
print(prog + ": " + msg, file=sys.stderr)
sys.exit(code)
def efatal(msg, e, code=1):
fatal(msg + ': ' + type(e).__name__ + ': ' + str(e), code)
# read the config file
try:
fd = open(options.config, 'r')
lines = fd.readlines()
fd.close()
except Exception as e:
efatal('could not open config file', e)
#
# go through the config file lines
#
linenum = 0
def configfatal(msg):
fatal(options.config + ' line ' + str(linenum) + ': ' + msg)
if not options.list:
logmsg('Starting')
# find out existing repository names
for dir in os.listdir('/etc/cvmfs/repositories.d'):
if os.path.exists('/etc/cvmfs/repositories.d/' + dir + '/replica.conf'):
oldrepos.append(dir)
def excluded(repo):
for exclude in excludes:
if fnmatch.fnmatch(repo, exclude):
return True
return False
# go through the config file
for line in lines:
linenum += 1
# remove comments and trailing whitespace
ihash = line.find('#')
if ihash >= 0:
line = line[0:ihash]
line = line.rstrip()
if line == "":
continue
(key, value) = line.split(None, 1)
if key == 'addcmd':
addcmd = value
elif key == 'remcmd':
remcmd = value
elif key == 'replist':
try:
response = urllib.request.urlopen(value)
reps = json.loads(response.read().decode())
except Exception as e:
efatal('failure reading and/or decoding ' + value, e)
replicalist = []
for typ in ['replicas', 'repositories']:
for rep in reps[typ]:
replicalist.append(rep['name'])
elif key == 'source':
source = value
elif key == 'keypath':
keypath = value
elif key == 'keysource':
keysource = value
elif key == 'exclude':
for exclude in value.split():
excludes.append(exclude)
elif key == 'repos':
if source == '':
configfatal('No source specified before repos')
for repo in value.split():
if '*' in repo or '?' in repo or '[' in repo or ']' in repo:
# match against a replist
if len(replicalist) == 0:
configfatal('No replist specified before repos')
for replica in replicalist:
if fnmatch.fnmatch(replica, repo):
if not excluded(replica):
repospecs[replica] = [source, keypath, keysource, addcmd, remcmd]
if not options.list:
# look for extra repositories matching this wildcard
for oldrepo in oldrepos:
if oldrepo in repospecs:
continue
if excluded(oldrepo):
continue
if fnmatch.fnmatch(oldrepo, repo):
if options.remove:
cmd = remcmd.replace('@fqrn@',oldrepo)
logmsg('Running ' + cmd)
if not options.dry_run:
code = os.system(cmd)
if code != 0:
logmsg('Remove failed with exit code ' + hex(code))
else:
logmsg('WARNING: extra repository ' + oldrepo + ' matches managed repos ' + repo)
else:
# no wildcards, just see if it has to be excluded
# although that's rather unlikely
if not excluded(repo):
repospecs[repo] = [source, keypath, keysource, addcmd, remcmd]
# exclude this repo pattern from future matches
excludes.append(repo)
else:
configfatal('unrecognized keyword: ' + key)
if options.list:
for repo, value in sorted(repospecs.items()):
source = value[0]
print(source + '/cvmfs/' + repo)
sys.exit(0)
# download a file from github, following symlinks
def download_github_file(infile, outfile):
# python2 https loading isn't reliable so call out to curl
try:
cmd = 'curl -m 10 -f -s https://raw.githubusercontent.com/' + infile
answer = subprocess.check_output(cmd, shell=True)
except:
return False
answer = answer.decode()
if answer.count('\n') > 0:
try:
with open(outfile, 'w') as fd:
fd.write(answer)
except:
logmsg('Error writing ' + outfile)
return False
else:
# does not have a newline, follow a symlink
infile = os.path.normpath(os.path.dirname(infile) + '/' + answer)
cmd = 'curl -m 10 -f -s -o ' + outfile + \
' https://raw.githubusercontent.com/' + infile
if os.system(cmd) != 0:
logmsg('Error following symlink for ' + outfile + ' to ' + infile)
return False
logmsg('Downloaded ' + outfile)
return True
prevdomains = []
for repo, value in sorted(repospecs.items()):
(url, keypath, keysource, addcmd, remcmd) = value
domain = repo.split('.',1)[1]
if len(keysource) > 0 and domain not in prevdomains:
# this is fairly complicated so only check once per domain
prevdomains.append(domain)
# first make sure domain key exists
keydir = '/etc/cvmfs/keys'
if options.dry_run:
keydir = '/tmp'
if not os.path.isfile(keydir + '/' + domain + '.pub') and \
not os.path.isdir(keydir + '/' + domain):
pubpath = '/' + domain + '.pub'
if not download_github_file(keysource + pubpath, keydir + pubpath):
# use github api to list keys in a keydir
keysindir = []
kss = keysource.split('/',3)
try:
# NOTE: this api is only allowed to be used 60 times
# per hour per IP address
cmd = 'curl -m 10 -f -s ' + \
'-H "Accept: application/vnd.github.v3+json" ' + \
'"https://api.github.com/repos/' + \
kss[0] + '/' + kss[1] + '/contents/' + kss[3] + \
'/' + domain + '?ref=' + kss[2] + '"'
answer = subprocess.check_output(cmd, shell=True)
except:
pass
else:
arr = json.loads(answer)
for rec in arr:
if 'name' in rec:
keysindir.append(rec['name'])
pubdir = '/' + domain
if len(keysindir) > 0:
os.mkdir(keydir + pubdir)
else:
logmsg('No pub key found for ' + domain)
for key in keysindir:
pubpath = pubdir + '/' + key
download_github_file(keysource + pubpath, keydir + pubpath)
if options.only_download_keys:
continue
serverconf = '/etc/cvmfs/repositories.d/' + repo + '/server.conf'
replicaconf = '/etc/cvmfs/repositories.d/' + repo + '/replica.conf'
addrepo = True
if os.path.exists(serverconf):
addrepo = False
# Repo exists. If there's no replica.conf, it may
# be blanked so skip that.
# If replica.conf exists and the full url does not
# match what is in server.conf, edit server.conf.
fullurl = url + '/cvmfs/' + repo
if os.path.exists(replicaconf):
contents = open(serverconf).read()
start = contents.find('CVMFS_STRATUM0=')
if start > 0:
start += len('CVMFS_STRATUM0=')
end = contents.find('\n', start)
if contents.find('{', start, end) >= 0:
#
# look past through the hyphen sign inside the curly brackets
# this is a convention in the cvmfs-hastratum1 package
start = contents.find('-', start, end)
if (start > 0):
start += 1
if start > 0:
if fullurl != contents[start:start+len(fullurl)]:
end = contents.find('}', start, end)
if end < 0:
end = contents.find('\n',start)
if (end > 0):
contents = contents[:start] + fullurl + contents[end:]
logmsg('Setting new url for ' + repo + ': ' + fullurl)
if not options.dry_run:
open(serverconf, 'w').write(contents)
if options.continue_failed and \
not os.path.exists(options.repo_directory +'/' + repo + '/.cvmfs_last_snapshot') and \
not os.path.exists('/var/spool/cvmfs/' + repo + '/is_snapshotting.lock') and \
not os.path.exists('/var/spool/cvmfs/' + repo + '/is_updating.lock'):
addrepo = True
url = 'continue'
if addrepo:
repokeypath = keypath
cmd = addcmd.replace('@url@', url).replace('@fqrn@', repo).replace('@keypath@', repokeypath)
logmsg('Running ' + cmd)
if not options.dry_run:
code = os.system(cmd)
if code != 0:
logmsg('Add failed with exit code ' + hex(code))
if not options.continue_failed:
cmd = remcmd.replace('@fqrn@',repo)
logmsg('Running ' + cmd)
code = os.system(cmd)
if code != 0:
logmsg('Undo failed with exit code ' + hex(code))
logmsg('Finished')