Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add argument for inactive_since to specify other timedeltas #5104

Merged
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 37 additions & 10 deletions scripts/delete-unused-users.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)
Expand All @@ -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
Expand All @@ -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"]
Expand All @@ -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)
Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -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
Expand All @@ -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',
Expand All @@ -145,5 +171,6 @@ def main(args):
logger.setLevel(logging.INFO)
elif args.debug:
logger.setLevel(logging.DEBUG)
logger.debug(args)

main(args)