Skip to content

Commit 06eb1de

Browse files
authored
Merge pull request #6 from edquist/SOFTWARE-5058.create-projects
Add script to create comanage projects (SOFTWARE-5058)
2 parents 24dc331 + afd1398 commit 06eb1de

File tree

1 file changed

+275
-0
lines changed

1 file changed

+275
-0
lines changed

create_project.py

Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
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] COGroupNameOrId ProjectName
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+
-h display this help text
35+
36+
Adds an identifier of type ospoolproject named Yes-ProjectName to
37+
a COGroup based on its Name or CO Group Id.
38+
39+
PASS for USER is taken from the first of:
40+
1. -u USER:PASS
41+
2. -d passfd (read from fd)
42+
3. -f passfile (read from file)
43+
4. read from $PASS env var
44+
"""
45+
46+
def usage(msg=None):
47+
if msg:
48+
print(msg + "\n", file=sys.stderr)
49+
50+
print(_usage, file=sys.stderr)
51+
sys.exit()
52+
53+
54+
class Options:
55+
endpoint = ENDPOINT
56+
osg_co_id = OSG_CO_ID
57+
user = USER
58+
authstr = None
59+
gid = None
60+
gname = None
61+
project = None
62+
63+
64+
options = Options()
65+
66+
67+
def getpw(user, passfd, passfile):
68+
if ':' in user:
69+
user, pw = user.split(':', 1)
70+
elif passfd is not None:
71+
pw = os.fdopen(passfd).readline().rstrip('\n')
72+
elif passfile is not None:
73+
pw = open(passfile).readline().rstrip('\n')
74+
elif 'PASS' in os.environ:
75+
pw = os.environ['PASS']
76+
else:
77+
usage("PASS required")
78+
return user, pw
79+
80+
81+
def mkauthstr(user, passwd):
82+
from base64 import encodebytes
83+
raw_authstr = '%s:%s' % (user, passwd)
84+
return encodebytes(raw_authstr.encode()).decode().replace('\n', '')
85+
86+
87+
def mkrequest(target, **kw):
88+
return mkrequest2(GET, target, **kw)
89+
90+
91+
def mkrequest2(method, target, **kw):
92+
return mkrequest3(method, target, data=None, **kw)
93+
94+
95+
def mkrequest3(method, target, data, **kw):
96+
url = os.path.join(options.endpoint, target)
97+
if kw:
98+
url += "?" + "&".join( "{}={}".format(k,v) for k,v in kw.items() )
99+
req = urllib.request.Request(url, json.dumps(data).encode("utf-8"))
100+
req.add_header("Authorization", "Basic %s" % options.authstr)
101+
req.add_header("Content-Type", "application/json")
102+
req.get_method = lambda: method
103+
return req
104+
105+
106+
def call_api(target, **kw):
107+
return call_api2(GET, target, **kw)
108+
109+
110+
def call_api2(method, target, **kw):
111+
return call_api3(method, target, data=None, **kw)
112+
113+
114+
def call_api3(method, target, data, **kw):
115+
req = mkrequest3(method, target, data, **kw)
116+
resp = urllib.request.urlopen(req)
117+
payload = resp.read()
118+
return json.loads(payload) if payload else None
119+
120+
121+
# primary api calls
122+
123+
124+
def get_osg_co_groups():
125+
return call_api("co_groups.json", coid=options.osg_co_id)
126+
127+
128+
def get_co_group_identifiers(gid):
129+
return call_api("identifiers.json", cogroupid=gid)
130+
131+
132+
def get_co_group_members(gid):
133+
return call_api("co_group_members.json", cogroupid=gid)
134+
135+
136+
def get_co_person_identifiers(pid):
137+
return call_api("identifiers.json", copersonid=pid)
138+
139+
140+
def get_co_group(gid):
141+
grouplist = call_api("co_groups/%d.json" % gid) | get_datalist("CoGroups")
142+
if not grouplist:
143+
raise RuntimeError("No such CO Group Id: %s" % gid)
144+
return grouplist[0]
145+
146+
147+
def get_identifier(id_):
148+
idfs = call_api("identifiers/%s.json" % id_) | get_datalist("Identifiers")
149+
if not idfs:
150+
raise RuntimeError("No such Identifier Id: %s" % id_)
151+
return idfs[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+
# script-specific functions
170+
171+
def add_project_identifier_to_group(gid, project_name):
172+
identifier_name = "Yes-%s" % project_name
173+
type_ = "ospoolproject"
174+
return add_identifier_to_group(gid, type_, identifier_name)
175+
176+
177+
def add_identifier_to_group(gid, type_, identifier_name):
178+
new_identifier_info = {
179+
"Version" : "1.0",
180+
"Type" : type_,
181+
"Identifier" : identifier_name,
182+
"Login" : False,
183+
"Person" : {"Type": "Group", "Id": str(gid)},
184+
"Status" : "Active"
185+
}
186+
data = {
187+
"RequestType" : "Identifiers",
188+
"Version" : "1.0",
189+
"Identifiers" : [new_identifier_info]
190+
}
191+
return call_api3(POST, "identifiers.json", data)
192+
193+
194+
def gname_to_gid(gname):
195+
groups = get_osg_co_groups() | get_datalist("CoGroups")
196+
matching = [ g for g in groups if g["Name"] == gname ]
197+
198+
if len(matching) > 1:
199+
raise RuntimeError("Multiple groups found with Name '%s'" % gname)
200+
elif not matching:
201+
raise RuntimeError("No group found with Name '%s'" % gname)
202+
203+
group = matching[0]
204+
return group["Id"]
205+
206+
207+
# CLI
208+
209+
210+
def parse_options(args):
211+
try:
212+
ops, args = getopt.getopt(args, 'u:c:d:f:e:h')
213+
except getopt.GetoptError:
214+
usage()
215+
216+
if len(args) != 2:
217+
usage()
218+
219+
cogroup, project = args
220+
if re.fullmatch(r'\d+', cogroup):
221+
options.gid = int(cogroup)
222+
else:
223+
options.gname = cogroup
224+
options.project = project
225+
226+
passfd = None
227+
passfile = None
228+
229+
for op, arg in ops:
230+
if op == '-h': usage()
231+
if op == '-u': options.user = arg
232+
if op == '-c': options.osg_co_id = int(arg)
233+
if op == '-d': passfd = int(arg)
234+
if op == '-f': passfile = arg
235+
if op == '-e': options.endpoint = arg
236+
237+
user, passwd = getpw(options.user, passfd, passfile)
238+
options.authstr = mkauthstr(user, passwd)
239+
240+
241+
def main(args):
242+
parse_options(args)
243+
244+
if options.gname:
245+
options.gid = gname_to_gid(options.gname)
246+
else:
247+
options.gname = get_co_group(options.gid)["Name"]
248+
249+
print('Creating new Identifier for project "%s"\n'
250+
'for CO Group "%s" (%s)'
251+
% (options.project, options.gname, options.gid))
252+
print("")
253+
254+
resp = add_project_identifier_to_group(options.gid, options.project)
255+
256+
print("Server Response:")
257+
print(json.dumps(resp, indent=2, sort_keys=True))
258+
259+
new_identifier = get_identifier(resp["Id"])
260+
print("")
261+
print("New Identifier Object:")
262+
print(json.dumps(new_identifier, indent=2, sort_keys=True))
263+
264+
# no exceptions, must have worked
265+
print("")
266+
print(":thumbsup:")
267+
268+
269+
if __name__ == "__main__":
270+
try:
271+
main(sys.argv[1:])
272+
except (RuntimeError, urllib.error.HTTPError) as e:
273+
print(e, file=sys.stderr)
274+
sys.exit(1)
275+

0 commit comments

Comments
 (0)