Skip to content

Commit 24dc331

Browse files
authored
Merge pull request #5 from edquist/SOFTWARE-5057.group-fixups
new script for UnixCluster autogroup fixups (SOFTWARE-5057)
2 parents e14e18f + 41abbff commit 24dc331

File tree

1 file changed

+353
-0
lines changed

1 file changed

+353
-0
lines changed

group_fixup.py

Lines changed: 353 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,353 @@
1+
#!/usr/bin/env python3
2+
3+
import os
4+
import re
5+
import sys
6+
import json
7+
import getopt
8+
import collections
9+
import urllib.error
10+
import urllib.request
11+
12+
13+
SCRIPT = os.path.basename(__file__)
14+
ENDPOINT = "https://registry.cilogon.org/registry/"
15+
USER = "co_7.group_fixup"
16+
OSG_CO_ID = 7
17+
18+
GET = "GET"
19+
PUT = "PUT"
20+
POST = "POST"
21+
DELETE = "DELETE"
22+
23+
24+
_usage = f"""\
25+
usage: [PASS=...] {SCRIPT} [OPTIONS]
26+
27+
OPTIONS:
28+
-u USER[:PASS] specify USER and optionally PASS on command line
29+
-c OSG_CO_ID specify OSG CO ID (default = {OSG_CO_ID})
30+
-d passfd specify open fd to read PASS
31+
-f passfile specify path to file to open and read PASS
32+
-e ENDPOINT specify REST endpoint
33+
(default = {ENDPOINT})
34+
-a show all UnixCluster autogroups, not just misnamed ones
35+
-i COGroupId show fixup info for a specific CO Group
36+
-x COGroupId run UnixCluster Group fixups for given CO Group Id
37+
--fix-all run UnixCluster Group fixups for all misnamed groups
38+
-h display this help text
39+
40+
Run without options to display misnamed UnixCluster autogroups.
41+
Run with -a to include UnixCluster autogroups with fixed names, too.
42+
Run with -i to display only a given CO Group.
43+
Run with -x to fixup a given CO Group.
44+
45+
PASS for USER is taken from the first of:
46+
1. -u USER:PASS
47+
2. -d passfd (read from fd)
48+
3. -f passfile (read from file)
49+
4. read from $PASS env var
50+
"""
51+
52+
def usage(msg=None):
53+
if msg:
54+
print(msg + "\n", file=sys.stderr)
55+
56+
print(_usage, file=sys.stderr)
57+
sys.exit()
58+
59+
60+
class Options:
61+
endpoint = ENDPOINT
62+
osg_co_id = OSG_CO_ID
63+
user = USER
64+
authstr = None
65+
fix_gid = None
66+
info_gid = None
67+
showall = False
68+
fix_all = False
69+
70+
71+
options = Options()
72+
73+
74+
def getpw(user, passfd, passfile):
75+
if ':' in user:
76+
user, pw = user.split(':', 1)
77+
elif passfd is not None:
78+
pw = os.fdopen(passfd).readline().rstrip('\n')
79+
elif passfile is not None:
80+
pw = open(passfile).readline().rstrip('\n')
81+
elif 'PASS' in os.environ:
82+
pw = os.environ['PASS']
83+
else:
84+
usage("PASS required")
85+
return user, pw
86+
87+
88+
def mkauthstr(user, passwd):
89+
from base64 import encodebytes
90+
raw_authstr = '%s:%s' % (user, passwd)
91+
return encodebytes(raw_authstr.encode()).decode().replace('\n', '')
92+
93+
94+
def mkrequest(target, **kw):
95+
return mkrequest2(GET, target, **kw)
96+
97+
98+
def mkrequest2(method, target, **kw):
99+
return mkrequest3(method, target, data=None, **kw)
100+
101+
102+
def mkrequest3(method, target, data, **kw):
103+
url = os.path.join(options.endpoint, target)
104+
if kw:
105+
url += "?" + "&".join( "{}={}".format(k,v) for k,v in kw.items() )
106+
req = urllib.request.Request(url, json.dumps(data).encode("utf-8"))
107+
req.add_header("Authorization", "Basic %s" % options.authstr)
108+
req.add_header("Content-Type", "application/json")
109+
req.get_method = lambda: method
110+
return req
111+
112+
113+
def call_api(target, **kw):
114+
return call_api2(GET, target, **kw)
115+
116+
117+
def call_api2(method, target, **kw):
118+
return call_api3(method, target, data=None, **kw)
119+
120+
121+
def call_api3(method, target, data, **kw):
122+
req = mkrequest3(method, target, data, **kw)
123+
resp = urllib.request.urlopen(req)
124+
payload = resp.read()
125+
return json.loads(payload) if payload else None
126+
127+
128+
# primary api calls
129+
130+
131+
def get_osg_co_groups():
132+
return call_api("co_groups.json", coid=options.osg_co_id)
133+
134+
135+
def get_co_group_identifiers(gid):
136+
return call_api("identifiers.json", cogroupid=gid)
137+
138+
139+
def get_co_group_members(gid):
140+
return call_api("co_group_members.json", cogroupid=gid)
141+
142+
143+
def get_co_person_identifiers(pid):
144+
return call_api("identifiers.json", copersonid=pid)
145+
146+
147+
def get_co_group(gid):
148+
grouplist = call_api("co_groups/%d.json" % gid) | get_datalist("CoGroups")
149+
if not grouplist:
150+
raise RuntimeError("No such CO Group Id: %s" % gid)
151+
return grouplist[0]
152+
153+
154+
# @rorable
155+
# def foo(x): ...
156+
# x | foo -> foo(x)
157+
class rorable:
158+
def __init__(self, f): self.f = f
159+
def __call__(self, *a, **kw): return self.f(*a, **kw)
160+
def __ror__ (self, x): return self.f(x)
161+
162+
163+
def get_datalist(listname):
164+
def get(data):
165+
return data[listname] if data else []
166+
return rorable(get)
167+
168+
169+
# api call results massagers
170+
171+
172+
def get_unixcluster_autogroups():
173+
groups = get_osg_co_groups()
174+
return [ g for g in groups["CoGroups"]
175+
if "automatically by UnixCluster" in g["Description"] ]
176+
177+
178+
def get_misnamed_unixcluster_groups():
179+
groups = get_osg_co_groups()
180+
return [ g for g in groups["CoGroups"]
181+
if "UnixCluster Group" in g["Name"] ]
182+
183+
184+
def _osgid_sortkey(i):
185+
return int(i["Identifier"])
186+
187+
def get_identifiers_to_delete(identifiers):
188+
by_type = collections.defaultdict(list)
189+
ids_to_delete = []
190+
191+
for i in identifiers:
192+
by_type[i["Type"]].append(i)
193+
194+
if len(by_type["osggid"]) == 2:
195+
min_identifier = min(by_type["osggid"], key=_osgid_sortkey)
196+
ids_to_delete.append(min_identifier["Id"])
197+
198+
for i in by_type["osggroup"]:
199+
if i["Identifier"].endswith("unixclustergroup"):
200+
ids_to_delete.append(i["Id"])
201+
202+
return ids_to_delete
203+
204+
205+
def get_fixed_unixcluster_group_name(name):
206+
m = re.search(r'^(.*) UnixCluster Group', name)
207+
return m.group(1) if m else name
208+
209+
210+
# display functions
211+
212+
213+
def show_misnamed_unixcluster_group(group):
214+
print('CO {CoId} Group {Id}: "{Name}"'.format(**group))
215+
oldname = group["Name"]
216+
newname = get_fixed_unixcluster_group_name(oldname)
217+
if oldname != newname:
218+
print(' ** Rename group to: "%s"' % newname)
219+
show_group_identifiers(group["Id"])
220+
print("")
221+
222+
223+
def show_all_unixcluster_groups():
224+
groups = get_unixcluster_autogroups()
225+
for group in groups:
226+
show_misnamed_unixcluster_group(group)
227+
228+
229+
def show_one_unixcluster_group(gid):
230+
group = get_co_group(gid)
231+
show_misnamed_unixcluster_group(group)
232+
233+
234+
def show_misnamed_unixcluster_groups():
235+
groups = get_misnamed_unixcluster_groups()
236+
for group in groups:
237+
show_misnamed_unixcluster_group(group)
238+
239+
240+
def show_group_identifiers(gid):
241+
identifiers = get_co_group_identifiers(gid) | get_datalist("Identifiers")
242+
for i in identifiers:
243+
print(' - Identifier {Id}: ({Type}) "{Identifier}"'.format(**i))
244+
245+
ids_to_delete = get_identifiers_to_delete(identifiers)
246+
if ids_to_delete:
247+
print(' ** Identifier Ids to delete: %s' % ', '.join(ids_to_delete))
248+
249+
250+
251+
# fixup functions
252+
253+
254+
def delete_identifier(id_):
255+
return call_api2(DELETE, "identifiers/%d.json" % id_)
256+
257+
258+
def rename_co_group(gid, group, newname):
259+
# minimal edit CoGroup Request includes Name+CoId+Status+Version
260+
new_group_info = {
261+
"Name" : newname,
262+
"CoId" : group["CoId"],
263+
"Status" : group["Status"],
264+
"Version" : group["Version"]
265+
}
266+
data = {
267+
"CoGroups" : [new_group_info],
268+
"RequestType" : "CoGroups",
269+
"Version" : "1.0"
270+
}
271+
return call_api3(PUT, "co_groups/%d.json" % gid, data)
272+
273+
274+
def fixup_unixcluster_group(gid):
275+
group = get_co_group(gid)
276+
oldname = group["Name"]
277+
newname = get_fixed_unixcluster_group_name(oldname)
278+
identifiers = get_co_group_identifiers(gid) | get_datalist("Identifiers")
279+
ids_to_delete = get_identifiers_to_delete(identifiers)
280+
281+
show_misnamed_unixcluster_group(group)
282+
if oldname != newname:
283+
rename_co_group(gid, group, newname)
284+
for id_ in ids_to_delete:
285+
delete_identifier(id_)
286+
287+
# http errors raise exceptions, so at this point we apparently succeeded
288+
print(":thumbsup:")
289+
return 0
290+
291+
292+
def fixup_all_unixcluster_groups():
293+
groups = get_misnamed_unixcluster_groups()
294+
for group in groups:
295+
fixup_unixcluster_group(group["Id"])
296+
297+
298+
# CLI
299+
300+
301+
def parse_options(args):
302+
try:
303+
ops, args = getopt.getopt(args, 'u:c:d:f:e:x:i:ah', ["fix-all"])
304+
except getopt.GetoptError:
305+
usage()
306+
307+
if args:
308+
usage("Extra arguments: %s" % repr(args))
309+
310+
passfd = None
311+
passfile = None
312+
313+
for op, arg in ops:
314+
if op == '-h': usage()
315+
if op == '-u': options.user = arg
316+
if op == '-c': options.osg_co_id = int(arg)
317+
if op == '-d': passfd = int(arg)
318+
if op == '-f': passfile = arg
319+
if op == '-e': options.endpoint = arg
320+
if op == '-x': options.fix_gid = int(arg)
321+
if op == '-i': options.info_gid = int(arg)
322+
if op == '-a': options.showall = True
323+
324+
if op == '--fix-all': options.fix_all = True
325+
326+
user, passwd = getpw(options.user, passfd, passfile)
327+
options.authstr = mkauthstr(user, passwd)
328+
329+
330+
def main(args):
331+
parse_options(args)
332+
333+
if options.fix_gid:
334+
return fixup_unixcluster_group(options.fix_gid)
335+
elif options.fix_all:
336+
return fixup_all_unixcluster_groups()
337+
elif options.showall:
338+
show_all_unixcluster_groups()
339+
elif options.info_gid:
340+
show_one_unixcluster_group(options.info_gid)
341+
else:
342+
show_misnamed_unixcluster_groups()
343+
344+
return 0
345+
346+
347+
if __name__ == "__main__":
348+
try:
349+
sys.exit(main(sys.argv[1:]))
350+
except (RuntimeError, urllib.error.HTTPError) as e:
351+
print(e, file=sys.stderr)
352+
sys.exit(1)
353+

0 commit comments

Comments
 (0)