Skip to content

Commit 476fbf2

Browse files
committed
copy group_fixup.py -> create_project.py (SOFTWARE-5058)
From SOFTWARE-5057.group-fixups branch. Separate copy commit done to show changes.
1 parent b575ea5 commit 476fbf2

File tree

1 file changed

+341
-0
lines changed

1 file changed

+341
-0
lines changed

create_project.py

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

0 commit comments

Comments
 (0)