Skip to content

Commit 73ac30e

Browse files
committed
YDA-6976 - Added script for SRAM migration
1 parent 50e33cc commit 73ac30e

4 files changed

Lines changed: 238 additions & 20 deletions

File tree

groups.py

Lines changed: 97 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@
3939
'api_group_get_user_role',
4040
'api_group_remove_user_from_group',
4141
'rule_group_sram_sync',
42-
'rule_external_users_sram_sync']
42+
'rule_external_users_sram_sync',
43+
'rule_sram_migration']
4344

4445

4546
def get_groups_data(ctx: rule.Context) -> Iterable[Any]:
@@ -1376,23 +1377,105 @@ def rule_external_users_sram_sync(ctx: rule.Context) -> None:
13761377
group_name = group["name"]
13771378
members = group['members'] + group['read'] + group['managers']
13781379
invited = group['invited']
1380+
co_identifier = sram.get_co_identifier(ctx, group_name)
1381+
sram_co = group['sram_co']
1382+
1383+
if not co_identifier and sram_co:
1384+
log.write(ctx, f"Sync members of group {group_name} with SRAM external users CO")
1385+
1386+
for member in members:
1387+
# Check if member has valid email and is an external user.
1388+
username, _ = user.from_str(ctx, member)
1389+
if yoda_names.is_email_username(username) and not yoda_names.is_internal_user(username):
1390+
# Remove invitation metadata if user is member of the external users CO.
1391+
if username in co_members and member in invited:
1392+
log.write(ctx, f"User {username} is member of group {group_name}, removing invitation metadata")
1393+
if sram.is_user_marked_invited(ctx, username, group_name):
1394+
msi.sudo_obj_meta_remove(ctx, member, "-u", "", constants.UUORGMETADATAPREFIX + "sram_invited", group_name, "", "")
1395+
# Put invite and add invitation metadata if user is not member of the external users CO.
1396+
elif username not in co_members and member not in invited:
1397+
sram.put_collaboration_invitation(ctx, group_name, username, config.sram_external_users_co)
1398+
if not sram.is_user_marked_invited(ctx, username, group_name):
1399+
msi.sudo_obj_meta_add(ctx, member, "-u", constants.UUORGMETADATAPREFIX + "sram_invited", group_name, "", "")
1400+
log.write(ctx, f"User {username} invited to group {group_name}")
13791401

1380-
log.write(ctx, f"Sync members of group {group_name} with SRAM external users CO")
1402+
log.write(ctx, "Finished syncing external users with SRAM")
1403+
1404+
1405+
@rule.make(inputs=[0, 1, 2, 3])
1406+
def rule_sram_migration(ctx: rule.Context, log_file: bool, dry_run:bool, convert_group: str, group_name: str) -> None:
1407+
""" Migrate existing groups to SRAM or non-SRAM group.
1408+
1409+
:param ctx: Combined type of a ctx and rei struct
1410+
:param log_file: If True, the log file will be generated at /var/lib/irods/log/sram-migration.log
1411+
:param dry_run: If True, the migration will run all the steps without making the changes.
1412+
:param convert_group: Convert group to either 'SRAM' or 'non-SRAM' group.
1413+
:param group_name: Name of the group to be migrated
1414+
"""
1415+
1416+
1417+
if not user.is_rodsadmin(ctx):
1418+
log.write(ctx, "SRAM migration requires rodsadmin privileges")
1419+
return
1420+
1421+
if not config.enable_sram:
1422+
log.write(ctx, "SRAM needs to be enabled to migrate existing group")
1423+
return
1424+
1425+
if not config.sram_external_users_co:
1426+
log.write(ctx, "SRAM external user CO needs to be configured for migration")
1427+
return
1428+
1429+
# Get group data
1430+
group = get_group_data(ctx, group_name)
1431+
1432+
# Get SRAM related metadata
1433+
co_identifier = sram.get_co_identifier(ctx, group_name)
1434+
sram_co = group['sram_co']
1435+
1436+
# Migrating SRAM to non-SRAM group
1437+
if convert_group == 'non-sram':
1438+
# Get external users CO members from SRAM
1439+
try:
1440+
co_members = [member['email'] for member in sram.get_co_members(ctx, config.sram_external_users_co)]
1441+
except Exception as e:
1442+
log.write(ctx, f"Migration failed during fetch: {e}")
1443+
1444+
# Check if external user exists in the group
1445+
members = group['members'] + group['read'] + group['managers']
13811446

13821447
for member in members:
1383-
# Check if member has valid email and is an external user.
13841448
username, _ = user.from_str(ctx, member)
13851449
if yoda_names.is_email_username(username) and not yoda_names.is_internal_user(username):
1386-
# Remove invitation metadata if user is member of the external users CO.
1387-
if username in co_members and member in invited:
1388-
log.write(ctx, f"User {username} is member of group {group_name}, removing invitation metadata")
1389-
if sram.is_user_marked_invited(ctx, username, group_name):
1390-
msi.sudo_obj_meta_remove(ctx, member, "-u", "", constants.UUORGMETADATAPREFIX + "sram_invited", group_name, "", "")
1391-
# Put invite and add invitation metadata if user is not member of the external users CO.
1392-
elif username not in co_members and member not in invited:
1393-
sram.put_collaboration_invitation(ctx, group_name, username, config.sram_external_users_co)
1394-
if not sram.is_user_marked_invited(ctx, username, group_name):
1450+
# Check if sram invitation metadata exists for this external user
1451+
if sram.is_user_marked_invited(ctx, username, group_name):
1452+
# Delete invitation from SRAM
1453+
try:
1454+
sram.delete_pending_invitation(ctx, co_identifier, username)
1455+
except Exception:
1456+
log.write(ctx, f"Something went wrong while deleting the open invitation for user {username} in SRAM CO {group_name}.")
1457+
1458+
# Delete SRAM invited metadata
1459+
msi.sudo_obj_meta_remove(ctx, member, "-u", "", constants.UUORGMETADATAPREFIX + "sram_invited", group_name, "", "")
1460+
else:
1461+
# Check if the user exists in SRAM external users CO, if not, then send the invitation
1462+
if username not in co_members:
1463+
sram.put_collaboration_invitation(ctx, group_name, username, config.sram_external_users_co)
13951464
msi.sudo_obj_meta_add(ctx, member, "-u", constants.UUORGMETADATAPREFIX + "sram_invited", group_name, "", "")
1396-
log.write(ctx, f"User {username} invited to group {group_name}")
1465+
log.write(ctx, f"User {username} invited to group {group_name}")
1466+
else:
1467+
log.write(ctx, f"User {username} already exists in SRAM external users CO.")
1468+
# Delete SRAM CO metadata
1469+
msi.sudo_obj_meta_remove(ctx, group_name, "-u", "", "co_identifier", co_identifier, "", "")
1470+
msi.sudo_obj_meta_remove(ctx, group_name, "-u", "", "sram_co", sram_co, "", "")
13971471

1398-
log.write(ctx, "Finished syncing external users with SRAM")
1472+
# Delete collaboration in SRAM
1473+
if co_identifier and not sram.delete_collaboration(ctx, co_identifier):
1474+
return api.Error('sram_error', 'Something went wrong deleting group "{}" in SRAM'.format(group_name))
1475+
elif convert_group == 'sram':
1476+
if not sram_co and co_identifier:
1477+
# Add sram_co metadata to SRAM group
1478+
msi.sudo_obj_meta_add(ctx, group_name, "-u", "sram_co", sram_co, "", "")
1479+
else:
1480+
log.write(ctx, "Could not convert group to {}. Invalid argument.".format(convert_group))
1481+

sram.py

Lines changed: 72 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -136,12 +136,7 @@ def put_collaboration_invitation(ctx: rule.Context, group_name: str, username: s
136136
return False
137137

138138
# Request SRAM to lookup possible existing invitation for this user.
139-
url = f"{config.sram_rest_api_url}/api/invitations/v1/invitations/{co_identifier}"
140-
141-
if config.sram_verbose_logging:
142-
log.write(ctx, f"put_collaboration_invitation get: {url}")
143-
144-
response = requests.get(url, headers=_request_headers(), timeout=30, verify=config.sram_tls_verify)
139+
response = get_open_invitations(ctx, co_identifier)
145140

146141
if config.sram_verbose_logging:
147142
log.write(ctx, f"put_collaboration_invitation response: {response.status_code}")
@@ -373,3 +368,74 @@ def is_user_marked_invited(ctx: rule.Context, username: str, group_name: str) ->
373368
return True
374369

375370
return False
371+
372+
373+
def get_open_invitations(ctx: rule.Context, co_identifier: str) -> str:
374+
"""Get open invitations for a SRAM CO.
375+
376+
:param ctx: Combined type of a callback and rei struct
377+
:param co_identifier: SRAM CO identifier
378+
379+
:returns: Response including status code
380+
"""
381+
if not misc.is_valid_uuid(co_identifier):
382+
log.write(ctx, f"put_collaboration_invitation error: CO identifier is invalid {co_identifier}")
383+
return False
384+
385+
# Request SRAM to lookup possible existing invitation for this user.
386+
url = f"{config.sram_rest_api_url}/api/invitations/v1/invitations/{co_identifier}"
387+
388+
if config.sram_verbose_logging:
389+
log.write(ctx, f"get_open_invitations get: {url}")
390+
391+
response = requests.get(url, headers=_request_headers(), timeout=30, verify=config.sram_tls_verify)
392+
393+
if config.sram_verbose_logging:
394+
log.write(ctx, f"get_open_invitations response: {response.status_code}")
395+
396+
if response.status_code != HTTP_OK:
397+
log.write(ctx, f"get_open_invitations error: unable to retrieve existing invitations - http {response.status_code}")
398+
return False
399+
400+
return response
401+
402+
403+
def delete_pending_invitation(ctx: rule.Context, co_identifier: str, username: str) -> bool:
404+
"""Delete pending invitation for a user in SRAM Collaborative Organisation.
405+
406+
:param ctx: Combined type of a callback and rei struct
407+
:param co_identifier: SRAM CO identifier
408+
:param username: Name of the user whose invitation needs to be deleted
409+
410+
:returns: Boolean indicating of deletion of invitation succeeded
411+
"""
412+
if not misc.is_valid_uuid(co_identifier):
413+
log.write(ctx, f"delete_collaboration error: CO identifier is invalid {co_identifier}")
414+
return False
415+
416+
invitation_uuid = ''
417+
418+
# Get unique id of open invitation for a user
419+
invitations = get_open_invitations(ctx, co_identifier)
420+
421+
if invitations:
422+
for invite in invitations.json():
423+
if invite['invitation']['email'] == username and invite['status'] == 'open':
424+
invitation_uuid = invite['invitation']['identifier']
425+
426+
if not misc.is_valid_uuid(invitation_uuid):
427+
log.write(ctx, f"No open invitations exist for user: {username}")
428+
return True
429+
430+
url = f"{config.sram_rest_api_url}/api/invitations/v1/{invitation_uuid}"
431+
432+
if config.sram_verbose_logging:
433+
log.write(ctx, f"delete_pending_invitation delete {url}")
434+
435+
response = requests.delete(url, headers=_request_headers(), timeout=30, verify=config.sram_tls_verify)
436+
437+
if config.sram_verbose_logging:
438+
log.write(ctx, f"delete_pending_invitation response: {response.status_code}")
439+
440+
441+
return response.status_code == HTTP_NO_CONTENT
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
#!/usr/bin/env python3
2+
"""This script takes CSV file as input and includes pre-existing SRAM groups to be migrated to non-SRAM groups.
3+
4+
This script should be run before SRAM external users sync so that the sync has accurate user information as input.
5+
6+
Example:
7+
To migrate SRAM groups to non-SRAM groups and output the results in a log file :
8+
python3 sram-migration-script.py -f sram-groups-to-non-sram.py
9+
10+
To output the results in a log file :
11+
python3 sram-migration-script.py -l -f sram-groups-to-non-sram.py
12+
13+
To dry run the migration script :
14+
python3 sram-migration-script.py -d -f sram-groups-to-non-sram.py
15+
16+
"""
17+
import argparse
18+
import subprocess
19+
20+
21+
def parse_args():
22+
parser = argparse.ArgumentParser(
23+
prog="sram-migration-script.py",
24+
description=__doc__,
25+
formatter_class=argparse.RawTextHelpFormatter)
26+
parser.add_argument("-l", "--log-file", action='store_true',
27+
help="If log file parameter is true then write to log at: /var/lib/irods/log/sram-migration.log")
28+
parser.add_argument("-d", "--dry-run", action='store_true',
29+
help="Run the migration script offline.")
30+
parser.add_argument("-c", "--convert-group", type=str, required=True,
31+
help="Convert the group to either 'sram' or 'non-sram' group.")
32+
parser.add_argument("-f", "--file-name", type=str, required=True,
33+
help="CSV file containing SRAM group names to be migrated to non-SRAM group.")
34+
return parser.parse_args()
35+
36+
def main():
37+
args = parse_args()
38+
rule_name = "/etc/irods/yoda-ruleset/tools/sram/sram-migration.r"
39+
log_loc = f"*log_loc={args.log_file if args.log_file else ''}"
40+
dry_run = f"*dry_run={args.dry_run if args.dry_run else ''}"
41+
convert_group = f"*convert_group={args.convert_group}"
42+
file_name = f"*file_name={args.file_name}"
43+
44+
subprocess.call(['irule', '-r', 'irods_rule_engine_plugin-python-instance', '-F',
45+
rule_name, log_loc, dry_run, convert_group, file_name])
46+
47+
if __name__ == '__main__':
48+
main()

tools/sram/sram-migration.r

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
#!/usr/bin/irule -r irods_rule_engine_plugin-irods_rule_language-instance -F
2+
#
3+
import csv
4+
5+
def main(rule_args, callback, rei):
6+
log_loc = global_vars["*log_loc"]
7+
dry_run = global_vars["*dry_run"]
8+
convert_group = global_vars["*convert_group"]
9+
file_name = global_vars["*file_name"]
10+
11+
with open(file_name, "r") as read_groups:
12+
groups = csv.reader(read_groups)
13+
14+
15+
for row in groups:
16+
callback.writeLine("stdout", row[0])
17+
callback.rule_sram_migration(log_loc, dry_run, convert_group, row[0])
18+
19+
20+
input *log_loc="", *dry_run="", *convert_group= "non-sram", *file_name="/etc/irods/yoda-ruleset/tools/sram/test-sram-migration.csv"
21+
output ruleExecOut

0 commit comments

Comments
 (0)