diff --git a/scripts/delete-unused-users.py b/scripts/delete-unused-users.py index 15f46faff..f5285d311 100755 --- a/scripts/delete-unused-users.py +++ b/scripts/delete-unused-users.py @@ -22,7 +22,6 @@ import sys from dateutil.parser import parse -from jhub_client.api import JupyterHubAPI logging.basicConfig(stream=sys.stdout, level=logging.WARNING) logger = logging.getLogger(__name__) @@ -33,7 +32,27 @@ "Authorization": f"Bearer {token}", } -def retrieve_users(hub_url): +def parse_timedelta(args): + """ + Parse timedelta value from literal string constructor values + + Trying to support all possible values like described in + https://docs.python.org/3/library/datetime.html#datetime.timedelta + """ + result = {} + for arg in args.split(','): + key, value = arg.split('=') + try: + value = int(value) + except ValueError: + try: + value = float(value) + except ValueError as e: + raise argparse.ArgumentError from e + result[key] = value + return timedelta(**result) + +def retrieve_users(hub_url, inactive_since): """Returns generator of user models that should be deleted""" url = hub_url.rstrip("/") + "/hub/api/users" next_page = True @@ -46,7 +65,7 @@ def retrieve_users(hub_url): user_list = resp["items"] for user in user_list: # only yield users that should be deleted - if should_delete(user): + if should_delete(user, inactive_since): yield user pagination = resp["_pagination"] @@ -57,10 +76,10 @@ def retrieve_users(hub_url): "limit": next_page["limit"], } -def should_delete(user): +def should_delete(user, inactive_since): """ Returns a boolean if user is to be deleted. The critera are: - - was the user active in the past 24 hours? + - was the user active in the past inactive_since period? - is there a current user server running? """ last_activity_str = user.get('last_activity', False) @@ -71,16 +90,16 @@ def should_delete(user): logger.error(f"Unexpected value for user['last_activity']: {user['last_activity']}") raise if isinstance(last_activity, datetime): - was_active_last_day = datetime.now().astimezone() - last_activity < timedelta(hours=24) + was_active_recently = datetime.now().astimezone() - last_activity < inactive_since else: logger.error(f"For user {user['name']}, expected datetime.datetime class for last_activity but got {type(last_activity)} instead.") raise logger.debug(f"User: {user['name']}") logger.debug(f"Last login: {last_activity}") - logger.debug(f"24hrs since last login: {was_active_last_day}") + logger.debug(f"Recent activity: {was_active_recently}") logger.debug(f"Running server: {user['server']}") - if was_active_last_day or user['server'] is not None: + if was_active_recently or user['server'] is not None: logger.info(f"Not deleting {user['name']}") return False else: @@ -101,7 +120,7 @@ def main(args): and if so, delete them! """ count = 1 - for user in list(retrieve_users(args.hub_url)): + for user in list(retrieve_users(args.hub_url, args.inactive_since)): print(f"{count}: deleting {user['name']}") count += 1 if not args.dry_run: @@ -115,7 +134,7 @@ def main(args): if __name__ == "__main__": argparser = argparse.ArgumentParser() argparser.add_argument( - '-h', + '-H', '--hub_url', help='Fully qualified URL to the JupyterHub', required=True @@ -125,6 +144,13 @@ def main(args): action='store_true', help='Dry run without deleting users' ) + argparser.add_argument( + '--inactive_since', + default='hours=24', + type=parse_timedelta, + help='Period of inactivity after which users are considered for deletion (literal string constructor values for timedelta objects)' + # https://docs.python.org/3/library/datetime.html#timedelta-objects + ) argparser.add_argument( '-v', '--verbose', @@ -145,5 +171,6 @@ def main(args): logger.setLevel(logging.INFO) elif args.debug: logger.setLevel(logging.DEBUG) + logger.debug(args) main(args)