Skip to content

Commit c4262b9

Browse files
Merge pull request #18 from williamnswanson/SOFTWARE-5619.assign-new-project-gids
Create script to assign osg GIDs to OSPool projects (SOFTWARE-5619)
2 parents 46c2108 + cf48240 commit c4262b9

File tree

2 files changed

+267
-0
lines changed

2 files changed

+267
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.vscode/launch.json

group_identifier_assigner.py

Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
#!/usr/bin/env python3
2+
3+
import os
4+
import re
5+
import sys
6+
import json
7+
import getopt
8+
import urllib.error
9+
import urllib.request
10+
11+
SCRIPT = os.path.basename(__file__)
12+
ENDPOINT = "https://registry-test.cilogon.org/registry/"
13+
OSG_CO_ID = 8
14+
MINTIMEOUT = 5
15+
MAXTIMEOUT = 625
16+
TIMEOUTMULTIPLE = 5
17+
18+
GET = "GET"
19+
PUT = "PUT"
20+
POST = "POST"
21+
DELETE = "DELETE"
22+
23+
OSPOOL_PROJECT_PREFIX_STR = "Yes-"
24+
PROJECT_GIDS_START = 200000
25+
26+
_usage = f"""\
27+
usage: [PASS=...] {SCRIPT} [OPTIONS]
28+
29+
OPTIONS:
30+
-u USER[:PASS] specify USER and optionally PASS on command line
31+
-c OSG_CO_ID specify OSG CO ID (default = {OSG_CO_ID})
32+
-d passfd specify open fd to read PASS
33+
-f passfile specify path to file to open and read PASS
34+
-e ENDPOINT specify REST endpoint
35+
(default = {ENDPOINT})
36+
-o outfile specify output file (default: write to stdout)
37+
-t minTimeout set minimum timeout, in seconds, for API call (default to {MINTIMEOUT})
38+
-T maxTimeout set maximum timeout, in seconds, for API call (default to {MAXTIMEOUT})
39+
-h display this help text
40+
41+
PASS for USER is taken from the first of:
42+
1. -u USER:PASS
43+
2. -d passfd (read from fd)
44+
3. -f passfile (read from file)
45+
4. read from $PASS env var
46+
"""
47+
48+
49+
def usage(msg=None):
50+
if msg:
51+
print(msg + "\n", file=sys.stderr)
52+
53+
print(_usage, file=sys.stderr)
54+
sys.exit()
55+
56+
57+
class Options:
58+
endpoint = ENDPOINT
59+
user = "co_8.william_test"
60+
osg_co_id = OSG_CO_ID
61+
outfile = None
62+
authstr = None
63+
min_timeout = MINTIMEOUT
64+
max_timeout = MAXTIMEOUT
65+
project_gid_startval = PROJECT_GIDS_START
66+
67+
68+
options = Options()
69+
70+
71+
def getpw(user, passfd, passfile):
72+
if ":" in user:
73+
user, pw = user.split(":", 1)
74+
elif passfd is not None:
75+
pw = os.fdopen(passfd).readline().rstrip("\n")
76+
elif passfile is not None:
77+
pw = open(passfile).readline().rstrip("\n")
78+
elif "PASS" in os.environ:
79+
pw = os.environ["PASS"]
80+
else:
81+
usage("PASS required")
82+
return user, pw
83+
84+
85+
def mkauthstr(user, passwd):
86+
from base64 import encodebytes
87+
88+
raw_authstr = "%s:%s" % (user, passwd)
89+
return encodebytes(raw_authstr.encode()).decode().replace("\n", "")
90+
91+
92+
def mkrequest(method, target, data, **kw):
93+
url = os.path.join(options.endpoint, target)
94+
if kw:
95+
url += "?" + "&".join("{}={}".format(k, v) for k, v in kw.items())
96+
req = urllib.request.Request(url, json.dumps(data).encode("utf-8"))
97+
req.add_header("Authorization", "Basic %s" % options.authstr)
98+
req.add_header("Content-Type", "application/json")
99+
req.get_method = lambda: method
100+
return req
101+
102+
103+
def call_api(target, **kw):
104+
return call_api2(GET, target, **kw)
105+
106+
107+
def call_api2(method, target, **kw):
108+
return call_api3(method, target, data=None, **kw)
109+
110+
111+
def call_api3(method, target, data, **kw):
112+
req = mkrequest(method, target, data, **kw)
113+
trying = True
114+
currentTimeout = options.min_timeout
115+
while trying:
116+
try:
117+
resp = urllib.request.urlopen(req, timeout=currentTimeout)
118+
payload = resp.read()
119+
trying = False
120+
except urllib.error.URLError as exception:
121+
if currentTimeout < options.max_timeout:
122+
currentTimeout *= TIMEOUTMULTIPLE
123+
else:
124+
sys.exit(
125+
f"Exception raised after maximum timeout {options.max_timeout} seconds reached. "
126+
+ f"Exception reason: {exception.reason}.\n Request: {req.full_url}"
127+
)
128+
129+
return json.loads(payload) if payload else None
130+
131+
132+
def get_osg_co_groups():
133+
return call_api("co_groups.json", coid=options.osg_co_id)
134+
135+
136+
# primary api calls
137+
138+
139+
def get_co_group_identifiers(gid):
140+
return call_api("identifiers.json", cogroupid=gid)
141+
142+
143+
def get_co_group_members(gid):
144+
return call_api("co_group_members.json", cogroupid=gid)
145+
146+
147+
def get_co_person_identifiers(pid):
148+
return call_api("identifiers.json", copersonid=pid)
149+
150+
151+
def get_datalist(data, listname):
152+
return data[listname] if data else []
153+
154+
155+
def identifier_index(id_list, id_type):
156+
id_type_list = [id["Type"] for id in id_list]
157+
try:
158+
return id_type_list.index(id_type)
159+
except ValueError:
160+
return -1
161+
162+
163+
def identifier_matches(id_list, id_type, regex_string):
164+
pattern = re.compile(regex_string)
165+
index = identifier_index(id_list, id_type)
166+
return (index != -1) & (pattern.match(id_list[index]["Identifier"]) is not None)
167+
168+
169+
def add_identifier_to_group(gid, type, identifier_name):
170+
new_identifier_info = {
171+
"Version": "1.0",
172+
"Type": type,
173+
"Identifier": identifier_name,
174+
"Login": False,
175+
"Person": {"Type": "Group", "Id": str(gid)},
176+
"Status": "Active",
177+
}
178+
data = {
179+
"RequestType": "Identifiers",
180+
"Version": "1.0",
181+
"Identifiers": [new_identifier_info],
182+
}
183+
return call_api3(POST, "identifiers.json", data)
184+
185+
186+
def parse_options(args):
187+
try:
188+
ops, args = getopt.getopt(args, "u:c:d:f:e:o:t:T:h")
189+
except getopt.GetoptError:
190+
usage()
191+
192+
if args:
193+
usage("Extra arguments: %s" % repr(args))
194+
195+
passfd = None
196+
passfile = None
197+
198+
for op, arg in ops:
199+
if op == "-h":
200+
usage()
201+
if op == "-u":
202+
options.user = arg
203+
if op == "-c":
204+
options.osg_co_id = int(arg)
205+
if op == "-d":
206+
passfd = int(arg)
207+
if op == "-f":
208+
passfile = arg
209+
if op == "-e":
210+
options.endpoint = arg
211+
if op == "-o":
212+
options.outfile = arg
213+
if op == "-t":
214+
options.min_timeout = float(arg)
215+
if op == "-T":
216+
options.max_timeout = float(arg)
217+
218+
user, passwd = getpw(options.user, passfd, passfile)
219+
options.authstr = mkauthstr(user, passwd)
220+
221+
222+
def main(args):
223+
parse_options(args)
224+
225+
# get groups with 'OSPool project name' matching "Yes-*" that don't have a 'OSG GID'
226+
227+
co_groups = get_osg_co_groups()["CoGroups"]
228+
highest_osggid = 0
229+
projects_to_assign_identifiers = []
230+
231+
for group in co_groups:
232+
gid = group["Id"]
233+
identifier_data = get_co_group_identifiers(gid)
234+
235+
if identifier_data:
236+
identifier_list = identifier_data["Identifiers"]
237+
238+
project_id_index = identifier_index(identifier_list, "ospoolproject")
239+
if project_id_index != -1:
240+
project_id = str(identifier_list[project_id_index]["Identifier"])
241+
is_project = re.compile(OSPOOL_PROJECT_PREFIX_STR + "*").match(project_id) is not None
242+
else:
243+
is_project = False
244+
245+
osggid_index = identifier_index(identifier_list, "osggid")
246+
if osggid_index != -1:
247+
highest_osggid = max(highest_osggid, int(identifier_list[osggid_index]["Identifier"]))
248+
elif is_project is True:
249+
project_name = project_id.replace(OSPOOL_PROJECT_PREFIX_STR, "", 1).lower()
250+
projects_to_assign_identifiers.append((gid, project_name,))
251+
252+
for gid, project_name in projects_to_assign_identifiers:
253+
# for each, set a 'OSG GID' starting from 200000 and a 'OSG Group Name' that is the group name
254+
osggid_to_assign = max(highest_osggid + 1, options.project_gid_startval)
255+
highest_osggid = osggid_to_assign
256+
add_identifier_to_group(gid, type="osggid", identifier_name=osggid_to_assign)
257+
add_identifier_to_group(gid, type="osggroup", identifier_name=project_name)
258+
print(f"project {project_name}: added osggid {osggid_to_assign} and osg project name {project_name}")
259+
260+
261+
if __name__ == "__main__":
262+
try:
263+
main(sys.argv[1:])
264+
except urllib.error.HTTPError as e:
265+
print(e, file=sys.stderr)
266+
sys.exit(1)

0 commit comments

Comments
 (0)