mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-09-19 10:51:34 +08:00
303 lines
11 KiB
Python
Executable File
303 lines
11 KiB
Python
Executable File
#!/usr/bin/env python
|
|
"""
|
|
Mark datasets as deleted that are older than specified cutoff
|
|
and (optionaly) 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
|
|
|
|
--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
|
|
|
|
import sqlalchemy as sa
|
|
from mako.template import Template
|
|
from sqlalchemy import (
|
|
and_,
|
|
false,
|
|
)
|
|
|
|
sys.path.insert(1, os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir, os.pardir, "lib")))
|
|
|
|
from cleanup_datasets import CleanupDatasetsApplication # noqa: I100
|
|
|
|
import galaxy.config
|
|
import galaxy.model.mapping
|
|
import galaxy.util
|
|
from galaxy.model.base import transaction
|
|
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] >= (2, 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(
|
|
"--smtp", default=None, help="SMTP Server to use to send email. " "Default: [read from galaxy ini file]"
|
|
)
|
|
parser.add_argument(
|
|
"--fromaddr", default=None, help="From address to use to send email. " "Default: [read from galaxy ini 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 = "%s.sample" % default_template
|
|
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("Specified template file (%s) not found." % template_file)
|
|
|
|
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("\n# %s - Handling stuff older than %i days" % (now, args.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,
|
|
)
|
|
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
|
|
):
|
|
# Marks dataset history association deleted and email users
|
|
start = time.time()
|
|
# Get HDAs older than cutoff time (ignore tool_id at this point)
|
|
# We really only need the id column here, but sqlalchemy barfs when
|
|
# trying to select only 1 column
|
|
hda_ids_query = sa.select(
|
|
(app.model.HistoryDatasetAssociation.__table__.c.id, app.model.HistoryDatasetAssociation.__table__.c.deleted),
|
|
whereclause=and_(
|
|
app.model.Dataset.__table__.c.deleted == false(),
|
|
app.model.HistoryDatasetAssociation.__table__.c.update_time < cutoff_time,
|
|
app.model.HistoryDatasetAssociation.__table__.c.deleted == false(),
|
|
),
|
|
from_obj=[sa.outerjoin(app.model.Dataset.__table__, app.model.HistoryDatasetAssociation.__table__)],
|
|
)
|
|
|
|
# Add all datasets associated with Histories to our list
|
|
hda_ids = []
|
|
hda_ids.extend([row.id for row in app.sa_session.execute(hda_ids_query)])
|
|
|
|
# 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:
|
|
user_query = sa.select(
|
|
[app.model.HistoryDatasetAssociation.__table__, app.model.History.__table__, app.model.User.__table__],
|
|
whereclause=and_(app.model.HistoryDatasetAssociation.__table__.c.id == hda_id),
|
|
from_obj=[
|
|
sa.join(app.model.User.__table__, app.model.History.__table__).join(
|
|
app.model.HistoryDatasetAssociation.__table__
|
|
)
|
|
],
|
|
use_labels=True,
|
|
)
|
|
for result in app.sa_session.execute(user_query):
|
|
user_notifications[result[app.model.User.__table__.c.email]].append(
|
|
(
|
|
result[app.model.HistoryDatasetAssociation.__table__.c.name],
|
|
result[app.model.History.__table__.c.name],
|
|
)
|
|
)
|
|
deleted_instance_count += 1
|
|
if not info_only and not email_only:
|
|
# Get the HistoryDatasetAssociation objects
|
|
hda = app.sa_session.query(app.model.HistoryDatasetAssociation).get(hda_id)
|
|
if not hda.deleted:
|
|
# Mark the HistoryDatasetAssociation as deleted
|
|
hda.deleted = True
|
|
app.sa_session.add(hda)
|
|
print("Marked HistoryDatasetAssociation id %d as " "deleted" % hda.id)
|
|
session = app.sa_session()
|
|
with transaction(session):
|
|
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)
|
|
subject = "Galaxy Server Cleanup " "- %d datasets DELETED" % len(dataset_list)
|
|
fromaddr = config.email_from
|
|
print()
|
|
print("From: %s" % fromaddr)
|
|
print("To: %s" % email)
|
|
print("Subject: %s" % subject)
|
|
print("----------")
|
|
print(msgtext)
|
|
if not info_only:
|
|
galaxy.util.send_mail(fromaddr, email, subject, msgtext, config)
|
|
|
|
stop = time.time()
|
|
print()
|
|
print("Marked %d dataset instances as deleted" % deleted_instance_count)
|
|
print("Total elapsed time: ", stop - start)
|
|
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
|
|
job = (
|
|
app.sa_session.query(app.model.Job)
|
|
.join(app.model.JobToOutputDatasetAssociation)
|
|
.filter(app.model.JobToOutputDatasetAssociation.__table__.c.dataset_id == hda_id)
|
|
.first()
|
|
)
|
|
if job is not None:
|
|
return job.tool_id
|
|
else:
|
|
hda = app.sa_session.query(app.model.HistoryDatasetAssociation).get(hda_id)
|
|
return _get_tool_id_for_hda(app, hda.copied_from_history_dataset_association_id)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|