mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-10-24 23:49:37 +08:00
309 lines
10 KiB
Python
Executable File
309 lines
10 KiB
Python
Executable File
#!/usr/bin/env python
|
||
"""
|
||
Mark datasets as deleted that are older than specified cutoff
|
||
and (optionally) with a tool_id that matches the specified search
|
||
string.
|
||
|
||
This script is useful for administrators to cleanup after users who
|
||
leave many old datasets around. It was modeled after the cleanup_datasets.py
|
||
script originally distributed with Galaxy.
|
||
|
||
Basic Usage:
|
||
admin_cleanup_datasets.py galaxy.ini -d 60 \
|
||
--template=email_template.txt
|
||
|
||
Required Arguments:
|
||
config_file - the Galaxy configuration file (galaxy.ini)
|
||
|
||
Optional Arguments:
|
||
-d --days - number of days old the dataset must be (default: 60)
|
||
--tool_id - string to search for in dataset tool_id (default: all)
|
||
--template - Mako template file to use for email notification
|
||
-i --info_only - Print results, but don't email or delete anything
|
||
-e --email_only - Email notifications, but don't delete anything
|
||
Useful for notifying users of pending deletion
|
||
--no-send - Do no send email (Default: false)
|
||
|
||
--smtp - Specify smtp server
|
||
If not specified, use smtp settings specified in config file
|
||
--fromaddr - Specify from address
|
||
If not specified, use email_from specified in config file
|
||
|
||
Email Template Variables:
|
||
cutoff - the cutoff in days
|
||
email - the users email address
|
||
datasets - a list of tuples containing 'dataset' and 'history' names
|
||
|
||
|
||
Author: Lance Parsons (lparsons@princeton.edu)
|
||
"""
|
||
|
||
import argparse
|
||
import logging
|
||
import os
|
||
import shutil
|
||
import sys
|
||
import time
|
||
from collections import defaultdict
|
||
from datetime import (
|
||
datetime,
|
||
timedelta,
|
||
)
|
||
from time import strftime
|
||
|
||
from mako.template import Template
|
||
from sqlalchemy import (
|
||
and_,
|
||
false,
|
||
select,
|
||
)
|
||
from sqlalchemy.orm import aliased
|
||
|
||
sys.path.insert(1, os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir, os.pardir, "lib")))
|
||
|
||
from cleanup_datasets import CleanupDatasetsApplication
|
||
|
||
import galaxy.config
|
||
import galaxy.util
|
||
from galaxy.model import (
|
||
Dataset,
|
||
History,
|
||
HistoryDatasetAssociation,
|
||
User,
|
||
)
|
||
from galaxy.util.script import (
|
||
app_properties_from_args,
|
||
populate_config_args,
|
||
)
|
||
|
||
log = logging.getLogger()
|
||
log.setLevel(logging.INFO)
|
||
log.addHandler(logging.StreamHandler(sys.stdout))
|
||
|
||
assert sys.version_info[:2] >= (3, 6)
|
||
|
||
|
||
def main():
|
||
"""
|
||
Datasets that are older than the specified cutoff and for which the tool_id
|
||
contains the specified text will be marked as deleted in user's history and
|
||
the user will be notified by email using the specified template file.
|
||
"""
|
||
parser = argparse.ArgumentParser()
|
||
parser.add_argument(
|
||
"legacy_config",
|
||
metavar="CONFIG",
|
||
type=str,
|
||
default=None,
|
||
nargs="?",
|
||
help="config file (legacy, use --config instead)",
|
||
)
|
||
parser.add_argument("-d", "--days", dest="days", action="store", type=int, help="number of days (60)", default=60)
|
||
parser.add_argument("--tool_id", default=None, help="Text to match against tool_id. Default: match all")
|
||
parser.add_argument(
|
||
"--template",
|
||
default=None,
|
||
help="Mako Template file to use as email "
|
||
"Variables are 'cutoff' for the cutoff in days, "
|
||
"'email' for users email and "
|
||
"'datasets' which is a list of tuples "
|
||
"containing 'dataset' and 'history' names. "
|
||
"Default: admin_cleanup_deletion_template.txt",
|
||
)
|
||
parser.add_argument(
|
||
"-i",
|
||
"--info_only",
|
||
action="store_true",
|
||
dest="info_only",
|
||
help="info about the requested action",
|
||
default=False,
|
||
)
|
||
parser.add_argument(
|
||
"-e",
|
||
"--email_only",
|
||
action="store_true",
|
||
dest="email_only",
|
||
help="Send emails only, don't delete",
|
||
default=False,
|
||
)
|
||
parser.add_argument("--no-send", action="store_true", help="Do not send email", default=False)
|
||
parser.add_argument(
|
||
"--smtp", default=None, help="SMTP Server to use to send email. Default: [read from galaxy config file]"
|
||
)
|
||
parser.add_argument(
|
||
"--fromaddr", default=None, help="From address to use to send email. Default: [read from galaxy config file]"
|
||
)
|
||
populate_config_args(parser)
|
||
|
||
args = parser.parse_args()
|
||
config_override = None
|
||
if args.legacy_config:
|
||
config_override = args.legacy_config
|
||
|
||
app_properties = app_properties_from_args(args, legacy_config_override=config_override)
|
||
|
||
if args.smtp is not None:
|
||
app_properties["smtp_server"] = args.smtp
|
||
if app_properties.get("smtp_server") is None:
|
||
parser.error("SMTP Server must be specified as an option (--smtp) or in the config file (smtp_server)")
|
||
|
||
if args.fromaddr is not None:
|
||
app_properties["email_from"] = args.fromaddr
|
||
if app_properties.get("email_from") is None:
|
||
parser.error("From address must be specified as an option (--fromaddr) or in the config file (email_from)")
|
||
|
||
scriptdir = os.path.dirname(os.path.abspath(__file__))
|
||
template_file = args.template
|
||
if template_file is None:
|
||
default_template = os.path.join(scriptdir, "admin_cleanup_deletion_template.txt")
|
||
sample_template_file = f"{default_template}.sample"
|
||
if os.path.exists(default_template):
|
||
template_file = default_template
|
||
elif os.path.exists(sample_template_file):
|
||
print(f"Copying {sample_template_file} to {default_template}")
|
||
shutil.copyfile(sample_template_file, default_template)
|
||
template_file = default_template
|
||
else:
|
||
parser.error(
|
||
"Default template ({default_template}) or sample template ({sample_template_file}) not "
|
||
"found, please specify template as an option (--template)."
|
||
)
|
||
elif not os.path.exists(template_file):
|
||
parser.error(f"Specified template file ({template_file}) not found.")
|
||
|
||
config = galaxy.config.Configuration(**app_properties)
|
||
|
||
app = CleanupDatasetsApplication(config)
|
||
cutoff_time = datetime.utcnow() - timedelta(days=args.days)
|
||
now = strftime("%Y-%m-%d %H:%M:%S")
|
||
|
||
print("##########################################")
|
||
print(f"\n# {now} - Handling stuff older than {args.days} days")
|
||
|
||
if args.info_only:
|
||
print("# Displaying info only ( --info_only )\n")
|
||
elif args.email_only:
|
||
print("# Sending emails only, not deleting ( --email_only )\n")
|
||
|
||
administrative_delete_datasets(
|
||
app,
|
||
cutoff_time,
|
||
args.days,
|
||
tool_id=args.tool_id,
|
||
template_file=template_file,
|
||
config=config,
|
||
email_only=args.email_only,
|
||
info_only=args.info_only,
|
||
no_send=args.no_send,
|
||
)
|
||
app.shutdown()
|
||
sys.exit(0)
|
||
|
||
|
||
def administrative_delete_datasets(
|
||
app, cutoff_time, cutoff_days, tool_id, template_file, config, email_only=False, info_only=False, no_send=False
|
||
):
|
||
# Marks dataset history association deleted and email users
|
||
start = time.time()
|
||
session = app.sa_session
|
||
|
||
# Get HDAs older than cutoff time (ignore tool_id at this point)
|
||
hda_ids_query = (
|
||
select(HistoryDatasetAssociation.id)
|
||
.join(Dataset, isouter=True)
|
||
.where(
|
||
and_(
|
||
Dataset.deleted == false(),
|
||
HistoryDatasetAssociation.update_time < cutoff_time,
|
||
HistoryDatasetAssociation.deleted == false(),
|
||
)
|
||
)
|
||
)
|
||
|
||
# Add all datasets associated with Histories to our list
|
||
hda_ids = session.execute(hda_ids_query).scalars().all()
|
||
|
||
# Now find the tool_id that generated the dataset (even if it was copied)
|
||
tool_matched_ids = []
|
||
if tool_id is not None:
|
||
for hda_id in hda_ids:
|
||
this_tool_id = _get_tool_id_for_hda(app, hda_id)
|
||
if this_tool_id is not None and tool_id in this_tool_id:
|
||
tool_matched_ids.append(hda_id)
|
||
hda_ids = tool_matched_ids
|
||
|
||
deleted_instance_count = 0
|
||
user_notifications = defaultdict(list)
|
||
|
||
# Process each of the Dataset objects
|
||
for hda_id in hda_ids:
|
||
# Bind hda_id for current iteration
|
||
rows = session.execute(
|
||
select(User.email, HistoryDatasetAssociation.name, History.name)
|
||
.join(History)
|
||
.join(HistoryDatasetAssociation)
|
||
.where(HistoryDatasetAssociation.id == hda_id)
|
||
).all()
|
||
|
||
for email, dataset_name, history_name in rows:
|
||
user_notifications[email].append((dataset_name, history_name))
|
||
deleted_instance_count += 1
|
||
|
||
if not info_only and not email_only:
|
||
# Get the HistoryDatasetAssociation objects
|
||
hda = session.get(HistoryDatasetAssociation, hda_id)
|
||
if not hda.deleted:
|
||
# Mark the HistoryDatasetAssociation as deleted
|
||
hda.deleted = True
|
||
session.add(hda)
|
||
print(f"Marked HistoryDatasetAssociation id {hda.id} as deleted")
|
||
|
||
if not info_only and not email_only:
|
||
session.commit()
|
||
|
||
emailtemplate = Template(filename=template_file)
|
||
for email, dataset_list in user_notifications.items():
|
||
msgtext = emailtemplate.render(email=email, datasets=dataset_list, cutoff=cutoff_days)
|
||
state = "" if info_only or email_only else " DELETED"
|
||
subject = f"Galaxy Server Cleanup - {len(dataset_list)} datasets{state}"
|
||
fromaddr = config.email_from
|
||
print()
|
||
print(f"From: {fromaddr}")
|
||
print(f"To: {email}")
|
||
print(f"Subject: {subject}")
|
||
print("----------")
|
||
print(msgtext)
|
||
if not no_send and not info_only:
|
||
galaxy.util.send_mail(fromaddr, email, subject, msgtext, config)
|
||
|
||
stop = time.time()
|
||
print()
|
||
print(f"Marked {deleted_instance_count} dataset instances as deleted")
|
||
print(f"Total elapsed time: {stop - start:.3f} seconds")
|
||
print("##########################################")
|
||
|
||
|
||
def _get_tool_id_for_hda(app, hda_id):
|
||
# TODO Some datasets don't seem to have an entry in jtod or a copied_from
|
||
if hda_id is None:
|
||
return None
|
||
|
||
# Aliases for ORM‑mapped classes
|
||
Job = aliased(app.model.Job)
|
||
JTODA = aliased(app.model.JobToOutputDatasetAssociation)
|
||
|
||
session = app.sa_session
|
||
|
||
job_query = select(Job.tool_id).join(JTODA).where(JTODA.dataset_id == hda_id)
|
||
|
||
tool_id = session.execute(job_query).scalars().first()
|
||
if tool_id is not None:
|
||
return tool_id
|
||
|
||
hda = session.get(HistoryDatasetAssociation, hda_id)
|
||
return _get_tool_id_for_hda(app, hda.copied_from_history_dataset_association_id)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|