mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-09-01 15:37:32 +08:00
1393 lines
59 KiB
Python
Executable File
1393 lines
59 KiB
Python
Executable File
#!/usr/bin/env python
|
|
"""
|
|
pgcleanup.py - A script for cleaning up datasets in Galaxy efficiently, by
|
|
bypassing the Galaxy model and operating directly on the database.
|
|
PostgreSQL 9.1 or greater is required.
|
|
"""
|
|
|
|
import argparse
|
|
import datetime
|
|
import inspect
|
|
import logging
|
|
import os
|
|
import re
|
|
import string
|
|
import sys
|
|
import time
|
|
import uuid
|
|
from collections import namedtuple
|
|
from functools import partial
|
|
|
|
import psycopg2
|
|
from psycopg2.extras import NamedTupleCursor
|
|
from sqlalchemy.engine.url import make_url
|
|
|
|
galaxy_root = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir, os.pardir))
|
|
sys.path.insert(1, os.path.join(galaxy_root, "lib"))
|
|
|
|
import galaxy.config
|
|
from galaxy.exceptions import ObjectNotFound
|
|
from galaxy.model import calculate_user_disk_usage_statements
|
|
from galaxy.objectstore import build_object_store_from_config
|
|
from galaxy.util.script import (
|
|
app_properties_from_args,
|
|
populate_config_args,
|
|
set_log_handler,
|
|
)
|
|
|
|
DEFAULT_LOG_DIR = os.path.join(galaxy_root, "scripts", "cleanup_datasets")
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
#
|
|
# BASE CLASSES, etc.
|
|
#
|
|
|
|
|
|
class LevelFormatter(logging.Formatter):
|
|
warn_fmt = "%(levelname)-5s %(funcName)s(): %(message)s"
|
|
def_fmt = "%(message)s"
|
|
|
|
def format(self, record):
|
|
if record.levelno > logging.INFO:
|
|
fmt = self.warn_fmt
|
|
else:
|
|
fmt = self.def_fmt
|
|
if hasattr(self, "_style"): # py3
|
|
self._style._fmt = fmt
|
|
else:
|
|
self._fmt = fmt
|
|
return logging.Formatter.format(self, record)
|
|
|
|
|
|
class Action:
|
|
"""Base class for all actions.
|
|
|
|
When writing new actions, the following things happen automatically:
|
|
- _action_sql's format() method is used to replace ``{update_time_sql}`` and ``{force_retry_sql}``
|
|
- an event is created and its id is added to the sql args with key ``event_id``
|
|
- ``days`` is added to the sql args
|
|
- ``causals`` (see examples) allow logging what events triggered what other events
|
|
|
|
The first column in the result set is considered the "primary" column upon which actions are being performed.
|
|
|
|
See the mixins for additional features.
|
|
|
|
Generally you should set at least ``_action_sql`` in subclasses (although it's possible to just override ``sql``
|
|
directly.)
|
|
"""
|
|
|
|
requires_objectstore = True
|
|
update_time_sql = ", update_time = NOW() AT TIME ZONE 'utc'"
|
|
force_retry_sql = " AND NOT purged"
|
|
primary_key = None
|
|
causals = ()
|
|
_action_sql = ""
|
|
_action_sql_args = {}
|
|
|
|
# these shouldn't be overridden by subclasses
|
|
|
|
@classmethod
|
|
def name_c(cls):
|
|
# special case - for more complex stuff you can always implement name_c() on subclasses
|
|
clsname = cls.__name__.replace("HDA", "Hda")
|
|
actname = [clsname[0].lower()]
|
|
for c in clsname[1:]:
|
|
if c in string.ascii_uppercase:
|
|
c = "_" + c.lower()
|
|
actname.append(c)
|
|
return "".join(actname)
|
|
|
|
@classmethod
|
|
def doc_iter(cls):
|
|
for line in cls.__doc__.splitlines():
|
|
yield line.replace(" ", "", 4)
|
|
|
|
def __init__(self, app):
|
|
self._log_dir = app.args.log_dir
|
|
self._log_file = app.args.log_file
|
|
self._dry_run = app.args.dry_run
|
|
self._debug = app.args.debug
|
|
self._update_time = app.args.update_time
|
|
self._force_retry = app.args.force_retry
|
|
if app.args.object_store_id:
|
|
self._object_store_id_sql = f" AND dataset.object_store_id = '{app.args.object_store_id}'"
|
|
else:
|
|
self._object_store_id_sql = ""
|
|
self._epoch_time = str(int(time.time()))
|
|
self._days = app.args.days
|
|
self._config = app.config
|
|
self._update = app._update
|
|
self.__log = None
|
|
self.__row_methods = []
|
|
self.__post_methods = []
|
|
self.__exit_methods = []
|
|
if self.requires_objectstore:
|
|
self.object_store = build_object_store_from_config(self._config)
|
|
self._register_exit_method(self.object_store.shutdown)
|
|
self._init()
|
|
|
|
def __enter__(self):
|
|
return self
|
|
|
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
for method in self.__exit_methods:
|
|
method()
|
|
if self.__log is not None:
|
|
self.__close_log()
|
|
|
|
def __open_log(self):
|
|
if self._log_file:
|
|
logf = os.path.join(self._log_dir, self._log_file)
|
|
else:
|
|
logf = os.path.join(self._log_dir, self.name + ".log")
|
|
if self._dry_run:
|
|
log.info("--dry-run specified, logging changes to stderr instead of log file: %s", logf)
|
|
h = set_log_handler()
|
|
else:
|
|
log.info("Opening log file: %s", logf)
|
|
h = set_log_handler(filename=logf)
|
|
h.setLevel(logging.DEBUG if self._debug else logging.INFO)
|
|
h.setFormatter(LevelFormatter())
|
|
self.__log = logging.getLogger(self.name)
|
|
self.__log.addHandler(h)
|
|
self.__log.propagate = False
|
|
m = (f"==== Log opened: {datetime.datetime.now().isoformat()} ").ljust(72, "=")
|
|
self.__log.info(m)
|
|
self.__log.info("Epoch time for this action: %s", self._epoch_time)
|
|
|
|
def __close_log(self):
|
|
m = (f"==== Log closed: {datetime.datetime.now().isoformat()} ").ljust(72, "=")
|
|
self.log.info(m)
|
|
self.__log = None
|
|
|
|
def _register_row_method(self, method):
|
|
self.__row_methods.append(method)
|
|
|
|
def _register_post_method(self, method):
|
|
self.__post_methods.append(method)
|
|
|
|
def _register_exit_method(self, method):
|
|
self.__exit_methods.append(method)
|
|
|
|
@property
|
|
def log(self):
|
|
if self.__log is None:
|
|
self.__open_log()
|
|
return self.__log
|
|
|
|
@property
|
|
def name(self):
|
|
return self.__class__.name_c()
|
|
|
|
# subclasses can override these as needed
|
|
|
|
@property
|
|
def _update_time_sql(self):
|
|
# only update time if not doing force retry (otherwise a lot of things would have their update times reset that
|
|
# were actually purged a long time ago)
|
|
if self._update_time and not self._force_retry:
|
|
return self.update_time_sql
|
|
return ""
|
|
|
|
@property
|
|
def _force_retry_sql(self):
|
|
# a misnomer, the value of force_retry_sql is actually the SQL that should be used if *not* forcibly retrying
|
|
if not self._force_retry:
|
|
return self.force_retry_sql
|
|
return ""
|
|
|
|
@property
|
|
def sql(self):
|
|
return self._action_sql.format(
|
|
update_time_sql=self._update_time_sql,
|
|
force_retry_sql=self._force_retry_sql,
|
|
epoch_time=self._epoch_time,
|
|
object_store_id_sql=self._object_store_id_sql,
|
|
)
|
|
|
|
@property
|
|
def sql_args(self):
|
|
args = self._action_sql_args.copy()
|
|
if "days" not in args:
|
|
args["days"] = self._days
|
|
return args
|
|
|
|
def _collect_row_results(self, row, results, primary_key):
|
|
primary_key = row._fields[0]
|
|
primary = getattr(row, primary_key)
|
|
if primary not in results:
|
|
results[primary] = [set() for x in range(len(self.causals))]
|
|
rowgetter = partial(getattr, row)
|
|
for i, causal in enumerate(self.causals):
|
|
vals = tuple(map(rowgetter, causal))
|
|
if any(vals[1:]):
|
|
results[primary][i].add(vals)
|
|
|
|
def _log_results(self, results, primary_key):
|
|
for primary in sorted(results.keys()):
|
|
self.log.info(f"{primary_key}: {primary}")
|
|
for causal, s in zip(self.causals, results[primary]):
|
|
for r in sorted(s):
|
|
secondaries = ", ".join(f"{x[0]}: {x[1]}" for x in zip(causal[1:], r[1:]))
|
|
self.log.info(f"{causal[0]} {r[0]} caused {secondaries}")
|
|
|
|
def handle_results(self, cur):
|
|
results = {}
|
|
primary_key = self.primary_key
|
|
for row in cur:
|
|
if primary_key is None:
|
|
primary_key = row._fields[0]
|
|
self._collect_row_results(row, results, primary_key)
|
|
for method in self.__row_methods:
|
|
method(row)
|
|
self._log_results(results, primary_key)
|
|
for method in self.__post_methods:
|
|
method()
|
|
|
|
# subclasses can implement these
|
|
|
|
def _init(self):
|
|
"""Action initialization routines.
|
|
|
|
Subclasses that implement ``_init()`` should call ``super()`` in it.
|
|
"""
|
|
pass
|
|
|
|
|
|
class RemovesObjects:
|
|
"""Base class for mixins that remove objects from object stores."""
|
|
|
|
requires_objectstore = True
|
|
|
|
def _init(self):
|
|
super()._init()
|
|
self.objects_to_remove = set()
|
|
log.info("Initializing object store for action %s", self.name)
|
|
self._register_row_method(self.collect_removed_object_info)
|
|
self._register_post_method(self.remove_objects)
|
|
|
|
def collect_removed_object_info(self, row):
|
|
object_id = getattr(row, self.id_column, None)
|
|
object_uuid = getattr(row, self.uuid_column, None)
|
|
if object_uuid:
|
|
object_uuid = str(uuid.UUID(object_uuid))
|
|
if object_id:
|
|
self.objects_to_remove.add(self.object_class(object_id, row.object_store_id, object_uuid))
|
|
|
|
def remove_objects(self):
|
|
for object_to_remove in sorted(self.objects_to_remove):
|
|
self.remove_object(object_to_remove)
|
|
|
|
def remove_from_object_store(self, object_to_remove, object_store_kwargs, entire_dir=False, check_exists=False):
|
|
# only remove the "object store path" - if it's at an external_filename, that file will be untouched anyway
|
|
# (which is what we want)
|
|
loggers = (self.log, log)
|
|
try:
|
|
# TODO: get_filename() can do stuff like cache locally from S3, but we want the filename for logging
|
|
# purposes; this isn't ideal, object stores should probably have a noop way to get the "object store native
|
|
# identifier" which in the case of Disk would be the path
|
|
if not check_exists or self.object_store.exists(object_to_remove, **object_store_kwargs):
|
|
filename = self.object_store.get_filename(object_to_remove, **object_store_kwargs)
|
|
self.log.info("removing %s at: %s", object_to_remove, filename)
|
|
if not self._dry_run:
|
|
self.object_store.delete(object_to_remove, entire_dir=entire_dir, **object_store_kwargs)
|
|
except ObjectNotFound as e:
|
|
[log_.warning("object store failure: %s: %s", object_to_remove, e) for log_ in loggers]
|
|
except Exception as e:
|
|
[log_.error("delete failure: %s: %s", object_to_remove, e) for log_ in loggers]
|
|
|
|
def remove_object(self, object_to_remove):
|
|
raise NotImplementedError()
|
|
|
|
|
|
#
|
|
# MIXINS
|
|
#
|
|
|
|
|
|
class PurgesHDAs:
|
|
"""Avoid repetition in queries that purge HDAs, since they must also delete MetadataFiles and ICDAs.
|
|
|
|
To use, place ``{purge_hda_dependencies_sql}`` somewhere in your CTEs after a ``purged_hda_ids`` CTE returning HDA
|
|
ids. If you have additional CTEs after the template point, be sure to append a ``,``.
|
|
"""
|
|
|
|
_purge_hda_dependencies_sql = """deleted_metadata_file_ids
|
|
AS ( UPDATE metadata_file
|
|
SET deleted = true{update_time_sql}
|
|
FROM purged_hda_ids
|
|
WHERE purged_hda_ids.id = metadata_file.hda_id
|
|
RETURNING metadata_file.hda_id AS hda_id,
|
|
metadata_file.id AS id,
|
|
metadata_file.uuid AS uuid,
|
|
metadata_file.object_store_id AS object_store_id),
|
|
deleted_icda_ids
|
|
AS ( UPDATE implicitly_converted_dataset_association
|
|
SET deleted = true{update_time_sql}
|
|
FROM purged_hda_ids
|
|
WHERE purged_hda_ids.id = implicitly_converted_dataset_association.hda_parent_id
|
|
RETURNING implicitly_converted_dataset_association.hda_id AS hda_id,
|
|
implicitly_converted_dataset_association.hda_parent_id AS hda_parent_id,
|
|
implicitly_converted_dataset_association.id AS id),
|
|
deleted_icda_purged_child_hda_ids
|
|
AS ( UPDATE history_dataset_association
|
|
SET purged = true, deleted = true{update_time_sql}
|
|
FROM deleted_icda_ids
|
|
WHERE deleted_icda_ids.hda_id = history_dataset_association.id),
|
|
metadata_file_events
|
|
AS (INSERT INTO cleanup_event_metadata_file_association
|
|
(create_time, cleanup_event_id, metadata_file_id)
|
|
SELECT NOW() AT TIME ZONE 'utc', %(event_id)s, id
|
|
FROM deleted_metadata_file_ids),
|
|
icda_events
|
|
AS (INSERT INTO cleanup_event_icda_association
|
|
(create_time, cleanup_event_id, icda_id)
|
|
SELECT NOW() AT TIME ZONE 'utc', %(event_id)s, id
|
|
FROM deleted_icda_ids),
|
|
icda_hda_events
|
|
AS (INSERT INTO cleanup_event_hda_association
|
|
(create_time, cleanup_event_id, hda_id)
|
|
SELECT NOW() AT TIME ZONE 'utc', %(event_id)s, hda_id
|
|
FROM deleted_icda_ids)"""
|
|
|
|
@property
|
|
def sql(self):
|
|
_purge_hda_dependencies_sql = self._purge_hda_dependencies_sql.format(
|
|
update_time_sql=self._update_time_sql,
|
|
force_retry_sql=self._force_retry_sql,
|
|
epoch_time=self._epoch_time,
|
|
)
|
|
return self._action_sql.format(
|
|
purge_hda_dependencies_sql=_purge_hda_dependencies_sql,
|
|
update_time_sql=self._update_time_sql,
|
|
force_retry_sql=self._force_retry_sql,
|
|
epoch_time=self._epoch_time,
|
|
object_store_id_sql=self._object_store_id_sql,
|
|
)
|
|
|
|
|
|
class RequiresDiskUsageRecalculation:
|
|
"""Causes disk usage to be recalculated for affected users.
|
|
|
|
To use, ensure your query returns a ``recalculate_disk_usage_user_id`` column.
|
|
"""
|
|
|
|
requires_objectstore = True
|
|
|
|
def _init(self):
|
|
super()._init()
|
|
self.__recalculate_disk_usage_user_ids = set()
|
|
self._register_row_method(self.collect_recalculate_disk_usage_user_id)
|
|
self._register_post_method(self.recalculate_disk_usage)
|
|
|
|
def collect_recalculate_disk_usage_user_id(self, row):
|
|
if row.recalculate_disk_usage_user_id:
|
|
self.__recalculate_disk_usage_user_ids.add(row.recalculate_disk_usage_user_id)
|
|
|
|
def recalculate_disk_usage(self):
|
|
"""
|
|
Any operation that purges a HistoryDatasetAssociation may require
|
|
updating a user's disk usage. Rather than attempt to resolve dataset
|
|
copies at purge-time, simply maintain a list of users that have had
|
|
HDAs purged, and update their usages once all updates are complete.
|
|
|
|
This could probably be done more efficiently.
|
|
"""
|
|
log.info("Recalculating disk usage for users whose data were purged")
|
|
for user_id in sorted(self.__recalculate_disk_usage_user_ids):
|
|
quota_source_map = self.object_store.get_quota_source_map()
|
|
statements = calculate_user_disk_usage_statements(user_id, quota_source_map)
|
|
|
|
for sql, args in statements:
|
|
sql, _ = re.subn(r"\:([\w]+)", r"%(\1)s", sql)
|
|
new_args = {}
|
|
for key, val in args.items():
|
|
if isinstance(val, list):
|
|
val = tuple(val)
|
|
new_args[key] = val
|
|
self._update(sql, new_args, add_event=False)
|
|
|
|
self.log.info("recalculate_disk_usage user_id %i" % user_id)
|
|
|
|
|
|
class RemovesMetadataFiles(RemovesObjects):
|
|
"""Causes MetadataFiles to be removed from the object store.
|
|
|
|
To use, ensure your query returns ``deleted_metadata_file_id``, ``object_store_id``, and
|
|
``deleted_metadata_file_uuid`` columns.
|
|
"""
|
|
|
|
object_class = namedtuple("MetadataFile", ["id", "object_store_id", "uuid"])
|
|
id_column = "deleted_metadata_file_id"
|
|
uuid_column = "deleted_metadata_file_uuid"
|
|
|
|
def remove_object(self, metadata_file):
|
|
store_by = self.object_store.get_store_by(metadata_file)
|
|
if store_by == "uuid":
|
|
alt_name = f"metadata_{metadata_file.uuid}.dat"
|
|
else:
|
|
alt_name = f"metadata_{metadata_file.id}.dat"
|
|
self.remove_from_object_store(
|
|
metadata_file, dict(extra_dir="_metadata_files", extra_dir_at_root=True, alt_name=alt_name)
|
|
)
|
|
|
|
|
|
class RemovesDatasets(RemovesObjects):
|
|
"""Causes Datasets to be removed from the object store.
|
|
|
|
To use, ensure your query returns ``purged_dataset_id``, ``object_store_id``, and ``purged_dataset_uuid`` columns.
|
|
"""
|
|
|
|
object_class = namedtuple("Dataset", ["id", "object_store_id", "uuid"])
|
|
id_column = "purged_dataset_id"
|
|
uuid_column = "purged_dataset_uuid"
|
|
|
|
def remove_object(self, dataset):
|
|
store_by = self.object_store.get_store_by(dataset)
|
|
if store_by == "uuid":
|
|
extra_dir = f"dataset_{dataset.uuid}_files"
|
|
else:
|
|
extra_dir = f"dataset_{dataset.id}_files"
|
|
self.remove_from_object_store(dataset, {})
|
|
self.remove_from_object_store(
|
|
dataset, dict(dir_only=True, extra_dir=extra_dir), entire_dir=True, check_exists=True
|
|
)
|
|
|
|
|
|
#
|
|
# ACTIONS
|
|
#
|
|
|
|
|
|
class UpdateHDAPurgedFlag(Action):
|
|
"""
|
|
The old cleanup script does not mark HistoryDatasetAssociations as purged when deleted Histories
|
|
are purged. This action can be used to rectify that situation.
|
|
"""
|
|
|
|
# update_time is intentionally left unmodified.
|
|
_action_sql = """
|
|
WITH purged_hda_ids
|
|
AS ( UPDATE history_dataset_association
|
|
SET purged = true, deleted = true
|
|
FROM dataset
|
|
WHERE history_dataset_association.dataset_id = dataset.id
|
|
AND dataset.purged
|
|
AND NOT history_dataset_association.purged
|
|
RETURNING history_dataset_association.id),
|
|
hda_events
|
|
AS (INSERT INTO cleanup_event_hda_association
|
|
(create_time, cleanup_event_id, hda_id)
|
|
SELECT NOW(), %(event_id)s, id
|
|
FROM purged_hda_ids)
|
|
SELECT id AS purged_hda_id
|
|
FROM purged_hda_ids
|
|
ORDER BY id;
|
|
"""
|
|
|
|
|
|
class DeleteUserlessHistories(Action):
|
|
"""
|
|
- Mark deleted all "anonymous" Histories (not owned by a registered user) that are older than
|
|
the specified number of days.
|
|
"""
|
|
|
|
_action_sql = """
|
|
WITH deleted_history_ids
|
|
AS ( UPDATE history
|
|
SET deleted = true{update_time_sql}
|
|
WHERE user_id is null
|
|
AND NOT deleted
|
|
AND update_time < (NOW() AT TIME ZONE 'utc' - interval '%(days)s days')
|
|
RETURNING id),
|
|
history_events
|
|
AS (INSERT INTO cleanup_event_history_association
|
|
(create_time, cleanup_event_id, history_id)
|
|
SELECT NOW() AT TIME ZONE 'utc', %(event_id)s, id
|
|
FROM deleted_history_ids)
|
|
SELECT id AS deleted_history_id
|
|
FROM deleted_history_ids
|
|
ORDER BY id
|
|
"""
|
|
|
|
|
|
class DeleteInactiveUsers(Action):
|
|
"""
|
|
- Mark deleted all users that are older than the specified number of days.
|
|
- Mark deleted (state = 'deleted') all Jobs whose user_ids are deleted in this step.
|
|
"""
|
|
|
|
force_retry_sql = " AND NOT deleted"
|
|
_action_sql = """
|
|
WITH deleted_user_ids
|
|
AS ( UPDATE galaxy_user
|
|
SET deleted = true{update_time_sql}
|
|
WHERE NOT active{force_retry_sql}
|
|
AND update_time < (NOW() AT TIME ZONE 'utc' - interval '%(days)s days')
|
|
RETURNING id),
|
|
deleted_job_ids
|
|
AS ( UPDATE job
|
|
SET state = 'deleted'{update_time_sql}
|
|
FROM deleted_user_ids
|
|
WHERE deleted_user_ids.id = job.user_id
|
|
AND job.state = 'new'
|
|
RETURNING job.user_id AS user_id,
|
|
job.id AS id),
|
|
user_events
|
|
AS (INSERT INTO cleanup_event_user_association
|
|
(create_time, cleanup_event_id, user_id)
|
|
SELECT NOW() AT TIME ZONE 'utc', %(event_id)s, id
|
|
FROM deleted_user_ids)
|
|
SELECT deleted_user_ids.id AS deleted_user_id,
|
|
deleted_job_ids.id AS deleted_job_id
|
|
FROM deleted_user_ids
|
|
LEFT OUTER JOIN deleted_job_ids
|
|
ON deleted_job_ids.user_id = deleted_user_ids.id
|
|
ORDER BY deleted_user_ids.id
|
|
"""
|
|
causals = (("purged_user_id", "purged_job_id"),)
|
|
|
|
|
|
class PurgeDeletedUsers(PurgesHDAs, RemovesMetadataFiles, Action):
|
|
"""
|
|
- Mark purged all users that are older than the specified number of days.
|
|
- Set User.disk_usage = 0 for all users purged in this step.
|
|
- Mark purged all Histories whose user_ids are purged in this step.
|
|
- Mark purged all HistoryDatasetAssociations whose history_ids are purged in this step.
|
|
- Delete all UserGroupAssociations whose user_ids are purged in this step.
|
|
- Delete all UserRoleAssociations whose user_ids are purged in this step EXCEPT FOR THE PRIVATE
|
|
ROLE.
|
|
- Delete all UserAddresses whose user_ids are purged in this step.
|
|
"""
|
|
|
|
_action_sql = """
|
|
WITH purged_user_ids
|
|
AS ( UPDATE galaxy_user
|
|
SET purged = true{update_time_sql}
|
|
WHERE deleted{force_retry_sql}
|
|
AND update_time < (NOW() AT TIME ZONE 'utc' - interval '%(days)s days')
|
|
RETURNING id),
|
|
deleted_uga_ids
|
|
AS (DELETE FROM user_group_association
|
|
USING purged_user_ids
|
|
WHERE user_group_association.user_id = purged_user_ids.id
|
|
RETURNING user_group_association.user_id AS user_id,
|
|
user_group_association.id AS id),
|
|
deleted_ura_ids
|
|
AS (DELETE FROM user_role_association
|
|
USING role
|
|
WHERE role.id = user_role_association.role_id
|
|
AND role.type != 'private'
|
|
AND user_role_association.user_id IN
|
|
(SELECT id
|
|
FROM purged_user_ids)
|
|
RETURNING user_role_association.user_id AS user_id,
|
|
user_role_association.id AS id),
|
|
deleted_ua_ids
|
|
AS (DELETE FROM user_address
|
|
USING purged_user_ids
|
|
WHERE user_address.user_id = purged_user_ids.id
|
|
RETURNING user_address.user_id AS user_id,
|
|
user_address.id AS id),
|
|
user_events
|
|
AS (INSERT INTO cleanup_event_user_association
|
|
(create_time, cleanup_event_id, user_id)
|
|
SELECT NOW() AT TIME ZONE 'utc', %(event_id)s, id
|
|
FROM purged_user_ids),
|
|
purged_history_ids
|
|
AS ( UPDATE history
|
|
SET purged = true{update_time_sql}
|
|
FROM purged_user_ids
|
|
WHERE purged_user_ids.id = history.user_id
|
|
AND NOT history.purged
|
|
RETURNING history.user_id AS user_id,
|
|
history.id AS id),
|
|
history_events
|
|
AS (INSERT INTO cleanup_event_history_association
|
|
(create_time, cleanup_event_id, history_id)
|
|
SELECT NOW() AT TIME ZONE 'utc', %(event_id)s, id
|
|
FROM purged_history_ids),
|
|
purged_hda_ids
|
|
AS ( UPDATE history_dataset_association
|
|
SET purged = true, deleted = true{update_time_sql}
|
|
FROM purged_history_ids
|
|
WHERE purged_history_ids.id = history_dataset_association.history_id
|
|
AND NOT history_dataset_association.purged
|
|
RETURNING history_dataset_association.history_id AS history_id,
|
|
history_dataset_association.id AS id),
|
|
hda_events
|
|
AS (INSERT INTO cleanup_event_hda_association
|
|
(create_time, cleanup_event_id, hda_id)
|
|
SELECT NOW() AT TIME ZONE 'utc', %(event_id)s, id
|
|
FROM purged_hda_ids),
|
|
{purge_hda_dependencies_sql}
|
|
SELECT purged_user_ids.id AS purged_user_id,
|
|
purged_user_ids.id AS zero_disk_usage_user_id,
|
|
purged_history_ids.id AS purged_history_id,
|
|
purged_hda_ids.id AS purged_hda_id,
|
|
deleted_metadata_file_ids.id AS deleted_metadata_file_id,
|
|
deleted_metadata_file_ids.uuid AS deleted_metadata_file_uuid,
|
|
deleted_metadata_file_ids.object_store_id AS object_store_id,
|
|
deleted_icda_ids.id AS deleted_icda_id,
|
|
deleted_icda_ids.hda_id AS deleted_icda_hda_id,
|
|
deleted_uga_ids.id AS deleted_uga_id,
|
|
deleted_ura_ids.id AS deleted_ura_id,
|
|
deleted_ua_ids.id AS deleted_ua_id
|
|
FROM purged_user_ids
|
|
LEFT OUTER JOIN purged_history_ids
|
|
ON purged_user_ids.id = purged_history_ids.user_id
|
|
LEFT OUTER JOIN purged_hda_ids
|
|
ON purged_history_ids.id = purged_hda_ids.history_id
|
|
LEFT OUTER JOIN deleted_metadata_file_ids
|
|
ON deleted_metadata_file_ids.hda_id = purged_hda_ids.id
|
|
LEFT OUTER JOIN deleted_icda_ids
|
|
ON deleted_icda_ids.hda_parent_id = purged_hda_ids.id
|
|
LEFT OUTER JOIN deleted_uga_ids
|
|
ON purged_user_ids.id = deleted_uga_ids.user_id
|
|
LEFT OUTER JOIN deleted_ura_ids
|
|
ON purged_user_ids.id = deleted_ura_ids.user_id
|
|
LEFT OUTER JOIN deleted_ua_ids
|
|
ON purged_user_ids.id = deleted_ua_ids.user_id
|
|
ORDER BY purged_user_ids.id
|
|
"""
|
|
causals = (
|
|
("purged_user_id", "purged_history_id"),
|
|
("purged_history_id", "purged_hda_id"),
|
|
("purged_hda_id", "deleted_metadata_file_id", "object_store_id"),
|
|
("purged_hda_id", "deleted_icda_id", "deleted_icda_hda_id"),
|
|
("purged_user_id", "deleted_uga_id"),
|
|
("purged_user_id", "deleted_ura_id"),
|
|
("purged_user_id", "deleted_ua_id"),
|
|
)
|
|
|
|
def _init(self):
|
|
super()._init()
|
|
self.__zero_disk_usage_user_ids = set()
|
|
self._register_row_method(self.collect_zero_disk_usage_user_id)
|
|
self._register_post_method(self.zero_disk_usage)
|
|
|
|
def collect_zero_disk_usage_user_id(self, row):
|
|
self.__zero_disk_usage_user_ids.add(row.zero_disk_usage_user_id)
|
|
|
|
def zero_disk_usage(self):
|
|
if not self.__zero_disk_usage_user_ids:
|
|
return
|
|
log.info("Zeroing disk usage for users who were purged")
|
|
sql = """
|
|
UPDATE galaxy_user
|
|
SET disk_usage = 0
|
|
WHERE id IN %(user_ids)s
|
|
"""
|
|
user_ids = sorted(self.__zero_disk_usage_user_ids)
|
|
args = {"user_ids": tuple(user_ids)}
|
|
self._update(sql, args, add_event=False)
|
|
self.log.info("zero_disk_usage user_ids: %s", " ".join(str(i) for i in user_ids))
|
|
|
|
|
|
class PurgeDeletedUsersGDPR(PurgesHDAs, RemovesMetadataFiles, Action):
|
|
"""
|
|
- Perform all steps in the PurgeDeletedUsers/purge_deleted_users action
|
|
- Obfuscate User.email and User.username for all users purged in this step.
|
|
|
|
NOTE: Your database must have the pgcrypto extension installed e.g. with:
|
|
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
|
"""
|
|
|
|
_action_sql = """
|
|
WITH purged_user_ids
|
|
AS ( UPDATE galaxy_user
|
|
SET purged = true,
|
|
disk_usage = 0,
|
|
email = encode(digest(email || '{epoch_time}', 'sha1'), 'hex'),
|
|
username = encode(digest(username || '{epoch_time}', 'sha1'), 'hex'){update_time_sql}
|
|
WHERE deleted{force_retry_sql}
|
|
AND update_time < (NOW() AT TIME ZONE 'utc' - interval '%(days)s days')
|
|
RETURNING id),
|
|
deleted_uga_ids
|
|
AS (DELETE FROM user_group_association
|
|
USING purged_user_ids
|
|
WHERE user_group_association.user_id = purged_user_ids.id
|
|
RETURNING user_group_association.user_id AS user_id,
|
|
user_group_association.id AS id),
|
|
deleted_ura_ids
|
|
AS (DELETE FROM user_role_association
|
|
USING role
|
|
WHERE role.id = user_role_association.role_id
|
|
AND role.type != 'private'
|
|
AND user_role_association.user_id IN
|
|
(SELECT id
|
|
FROM purged_user_ids)
|
|
RETURNING user_role_association.user_id AS user_id,
|
|
user_role_association.id AS id),
|
|
deleted_ua_ids
|
|
AS (DELETE FROM user_address
|
|
USING purged_user_ids
|
|
WHERE user_address.user_id = purged_user_ids.id
|
|
RETURNING user_address.user_id AS user_id,
|
|
user_address.id AS id),
|
|
user_events
|
|
AS (INSERT INTO cleanup_event_user_association
|
|
(create_time, cleanup_event_id, user_id)
|
|
SELECT NOW() AT TIME ZONE 'utc', %(event_id)s, id
|
|
FROM purged_user_ids),
|
|
purged_history_ids
|
|
AS ( UPDATE history
|
|
SET purged = true{update_time_sql}
|
|
FROM purged_user_ids
|
|
WHERE purged_user_ids.id = history.user_id
|
|
AND NOT history.purged
|
|
RETURNING history.user_id AS user_id,
|
|
history.id AS id),
|
|
history_events
|
|
AS (INSERT INTO cleanup_event_history_association
|
|
(create_time, cleanup_event_id, history_id)
|
|
SELECT NOW() AT TIME ZONE 'utc', %(event_id)s, id
|
|
FROM purged_history_ids),
|
|
purged_hda_ids
|
|
AS ( UPDATE history_dataset_association
|
|
SET purged = true, deleted = true{update_time_sql}
|
|
FROM purged_history_ids
|
|
WHERE purged_history_ids.id = history_dataset_association.history_id
|
|
AND NOT history_dataset_association.purged
|
|
RETURNING history_dataset_association.history_id AS history_id,
|
|
history_dataset_association.id AS id),
|
|
hda_events
|
|
AS (INSERT INTO cleanup_event_hda_association
|
|
(create_time, cleanup_event_id, hda_id)
|
|
SELECT NOW() AT TIME ZONE 'utc', %(event_id)s, id
|
|
FROM purged_hda_ids),
|
|
{purge_hda_dependencies_sql}
|
|
SELECT purged_user_ids.id AS purged_user_id,
|
|
purged_user_ids.id AS zero_disk_usage_user_id,
|
|
purged_history_ids.id AS purged_history_id,
|
|
purged_hda_ids.id AS purged_hda_id,
|
|
deleted_metadata_file_ids.id AS deleted_metadata_file_id,
|
|
deleted_metadata_file_ids.uuid AS deleted_metadata_file_uuid,
|
|
deleted_metadata_file_ids.object_store_id AS object_store_id,
|
|
deleted_icda_ids.id AS deleted_icda_id,
|
|
deleted_icda_ids.hda_id AS deleted_icda_hda_id,
|
|
deleted_uga_ids.id AS deleted_uga_id,
|
|
deleted_ura_ids.id AS deleted_ura_id,
|
|
deleted_ua_ids.id AS deleted_ua_id
|
|
FROM purged_user_ids
|
|
LEFT OUTER JOIN purged_history_ids
|
|
ON purged_user_ids.id = purged_history_ids.user_id
|
|
LEFT OUTER JOIN purged_hda_ids
|
|
ON purged_history_ids.id = purged_hda_ids.history_id
|
|
LEFT OUTER JOIN deleted_metadata_file_ids
|
|
ON deleted_metadata_file_ids.hda_id = purged_hda_ids.id
|
|
LEFT OUTER JOIN deleted_icda_ids
|
|
ON deleted_icda_ids.hda_parent_id = purged_hda_ids.id
|
|
LEFT OUTER JOIN deleted_uga_ids
|
|
ON purged_user_ids.id = deleted_uga_ids.user_id
|
|
LEFT OUTER JOIN deleted_ura_ids
|
|
ON purged_user_ids.id = deleted_ura_ids.user_id
|
|
LEFT OUTER JOIN deleted_ua_ids
|
|
ON purged_user_ids.id = deleted_ua_ids.user_id
|
|
ORDER BY purged_user_ids.id
|
|
"""
|
|
causals = (
|
|
("purged_user_id", "purged_history_id"),
|
|
("purged_history_id", "purged_hda_id"),
|
|
("purged_hda_id", "deleted_metadata_file_id", "object_store_id"),
|
|
("purged_hda_id", "deleted_icda_id", "deleted_icda_hda_id"),
|
|
("purged_user_id", "deleted_uga_id"),
|
|
("purged_user_id", "deleted_ura_id"),
|
|
("purged_user_id", "deleted_ua_id"),
|
|
)
|
|
|
|
@classmethod
|
|
def name_c(cls):
|
|
return "purge_deleted_users_gdpr"
|
|
|
|
|
|
class PurgeDeletedHDAs(PurgesHDAs, RemovesMetadataFiles, RequiresDiskUsageRecalculation, Action):
|
|
"""
|
|
- Mark purged all HistoryDatasetAssociations currently marked deleted that are older than the
|
|
specified number of days.
|
|
- Mark deleted all MetadataFiles whose hda_id is purged in this step.
|
|
- Mark deleted all ImplicitlyConvertedDatasetAssociations whose hda_parent_id is purged in this
|
|
step.
|
|
- Mark purged all HistoryDatasetAssociations for which an ImplicitlyConvertedDatasetAssociation
|
|
with matching hda_id is deleted in this step.
|
|
"""
|
|
|
|
_action_sql = """
|
|
WITH purged_hda_ids
|
|
AS ( UPDATE history_dataset_association
|
|
SET purged = true, deleted = true{update_time_sql}
|
|
WHERE deleted{force_retry_sql}
|
|
AND update_time < (NOW() AT TIME ZONE 'utc' - interval '%(days)s days')
|
|
RETURNING id,
|
|
history_id),
|
|
hda_events
|
|
AS (INSERT INTO cleanup_event_hda_association
|
|
(create_time, cleanup_event_id, hda_id)
|
|
SELECT NOW() AT TIME ZONE 'utc', %(event_id)s, id
|
|
FROM purged_hda_ids),
|
|
{purge_hda_dependencies_sql}
|
|
SELECT purged_hda_ids.id AS purged_hda_id,
|
|
history.user_id AS recalculate_disk_usage_user_id,
|
|
deleted_metadata_file_ids.id AS deleted_metadata_file_id,
|
|
deleted_metadata_file_ids.uuid AS deleted_metadata_file_uuid,
|
|
deleted_metadata_file_ids.object_store_id AS object_store_id,
|
|
deleted_icda_ids.id AS deleted_icda_id,
|
|
deleted_icda_ids.hda_id AS deleted_icda_hda_id
|
|
FROM purged_hda_ids
|
|
LEFT OUTER JOIN history
|
|
ON purged_hda_ids.history_id = history.id
|
|
LEFT OUTER JOIN deleted_metadata_file_ids
|
|
ON deleted_metadata_file_ids.hda_id = purged_hda_ids.id
|
|
LEFT OUTER JOIN deleted_icda_ids
|
|
ON deleted_icda_ids.hda_parent_id = purged_hda_ids.id
|
|
ORDER BY purged_hda_ids.id
|
|
"""
|
|
causals = (
|
|
("purged_hda_id", "deleted_metadata_file_id", "object_store_id"),
|
|
("purged_hda_id", "deleted_icda_id", "deleted_icda_hda_id"),
|
|
)
|
|
|
|
|
|
class PurgeOldHDAs(PurgesHDAs, RemovesMetadataFiles, RequiresDiskUsageRecalculation, Action):
|
|
"""
|
|
- Mark purged all HistoryDatasetAssociations that are older than the specified number of days.
|
|
- Mark deleted all MetadataFiles whose hda_id is purged in this step.
|
|
- Mark deleted all ImplicitlyConvertedDatasetAssociations whose hda_parent_id is purged in this
|
|
step.
|
|
- Mark purged all HistoryDatasetAssociations for which an ImplicitlyConvertedDatasetAssociation
|
|
with matching hda_id is deleted in this step.
|
|
"""
|
|
|
|
force_retry_sql = " AND NOT history_dataset_association.purged"
|
|
_action_sql = """
|
|
WITH purged_hda_ids
|
|
AS ( UPDATE history_dataset_association
|
|
SET purged = true, deleted = true{update_time_sql}
|
|
FROM dataset
|
|
WHERE history_dataset_association.dataset_id = dataset.id AND
|
|
dataset.create_time < (NOW() AT TIME ZONE 'utc' - interval '%(days)s days')
|
|
{force_retry_sql} {object_store_id_sql}
|
|
RETURNING history_dataset_association.id,
|
|
history_id),
|
|
hda_events
|
|
AS (INSERT INTO cleanup_event_hda_association
|
|
(create_time, cleanup_event_id, hda_id)
|
|
SELECT NOW() AT TIME ZONE 'utc', %(event_id)s, id
|
|
FROM purged_hda_ids),
|
|
{purge_hda_dependencies_sql}
|
|
SELECT purged_hda_ids.id AS purged_hda_id,
|
|
history.user_id AS recalculate_disk_usage_user_id,
|
|
deleted_metadata_file_ids.id AS deleted_metadata_file_id,
|
|
deleted_metadata_file_ids.uuid AS deleted_metadata_file_uuid,
|
|
deleted_metadata_file_ids.object_store_id AS object_store_id,
|
|
deleted_icda_ids.id AS deleted_icda_id,
|
|
deleted_icda_ids.hda_id AS deleted_icda_hda_id
|
|
FROM purged_hda_ids
|
|
LEFT OUTER JOIN history
|
|
ON purged_hda_ids.history_id = history.id
|
|
LEFT OUTER JOIN deleted_metadata_file_ids
|
|
ON deleted_metadata_file_ids.hda_id = purged_hda_ids.id
|
|
LEFT OUTER JOIN deleted_icda_ids
|
|
ON deleted_icda_ids.hda_parent_id = purged_hda_ids.id
|
|
ORDER BY purged_hda_ids.id
|
|
"""
|
|
causals = (
|
|
("purged_hda_id", "deleted_metadata_file_id", "object_store_id"),
|
|
("purged_hda_id", "deleted_icda_id", "deleted_icda_hda_id"),
|
|
)
|
|
|
|
|
|
class PurgeHistorylessHDAs(PurgesHDAs, RemovesMetadataFiles, RequiresDiskUsageRecalculation, Action):
|
|
"""
|
|
- Mark purged all HistoryDatasetAssociations whose history_id is null.
|
|
"""
|
|
|
|
force_retry_sql = " AND NOT history_dataset_association.purged"
|
|
_action_sql = """
|
|
WITH purged_hda_ids
|
|
AS ( UPDATE history_dataset_association
|
|
SET purged = true, deleted = true{update_time_sql}
|
|
FROM dataset
|
|
WHERE history_id IS NULL{force_retry_sql}{object_store_id_sql}
|
|
AND history_dataset_association.update_time < (NOW() AT TIME ZONE 'utc' - interval '%(days)s days')
|
|
RETURNING history_dataset_association.id as id,
|
|
history_dataset_association.history_id as history_id),
|
|
hda_events
|
|
AS (INSERT INTO cleanup_event_hda_association
|
|
(create_time, cleanup_event_id, hda_id)
|
|
SELECT NOW() AT TIME ZONE 'utc', %(event_id)s, id
|
|
FROM purged_hda_ids),
|
|
{purge_hda_dependencies_sql}
|
|
SELECT purged_hda_ids.id AS purged_hda_id,
|
|
history.user_id AS recalculate_disk_usage_user_id,
|
|
deleted_metadata_file_ids.id AS deleted_metadata_file_id,
|
|
deleted_metadata_file_ids.uuid AS deleted_metadata_file_uuid,
|
|
deleted_metadata_file_ids.object_store_id AS object_store_id,
|
|
deleted_icda_ids.id AS deleted_icda_id,
|
|
deleted_icda_ids.hda_id AS deleted_icda_hda_id
|
|
FROM purged_hda_ids
|
|
LEFT OUTER JOIN history
|
|
ON purged_hda_ids.history_id = history.id
|
|
LEFT OUTER JOIN deleted_metadata_file_ids
|
|
ON deleted_metadata_file_ids.hda_id = purged_hda_ids.id
|
|
LEFT OUTER JOIN deleted_icda_ids
|
|
ON deleted_icda_ids.hda_parent_id = purged_hda_ids.id
|
|
ORDER BY purged_hda_ids.id
|
|
"""
|
|
causals = (
|
|
("purged_hda_id", "deleted_metadata_file_id", "object_store_id"),
|
|
("purged_hda_id", "deleted_icda_id", "deleted_icda_hda_id"),
|
|
)
|
|
|
|
|
|
class PurgeErrorHDAs(PurgesHDAs, RemovesMetadataFiles, RequiresDiskUsageRecalculation, Action):
|
|
"""
|
|
- Mark purged all HistoryDatasetAssociations whose dataset_id is state = 'error' that are older
|
|
than the specified number of days.
|
|
"""
|
|
|
|
force_retry_sql = " AND NOT history_dataset_association.purged"
|
|
_action_sql = """
|
|
WITH purged_hda_ids
|
|
AS ( UPDATE history_dataset_association
|
|
SET purged = true, deleted = true{update_time_sql}
|
|
FROM dataset
|
|
WHERE history_dataset_association.dataset_id = dataset.id{force_retry_sql}{object_store_id_sql}
|
|
AND dataset.state = 'error'
|
|
AND history_dataset_association.update_time < (NOW() AT TIME ZONE 'utc' - interval '%(days)s days')
|
|
RETURNING history_dataset_association.id as id,
|
|
history_dataset_association.history_id as history_id),
|
|
hda_events
|
|
AS (INSERT INTO cleanup_event_hda_association
|
|
(create_time, cleanup_event_id, hda_id)
|
|
SELECT NOW() AT TIME ZONE 'utc', %(event_id)s, id
|
|
FROM purged_hda_ids),
|
|
{purge_hda_dependencies_sql}
|
|
SELECT purged_hda_ids.id AS purged_hda_id,
|
|
history.user_id AS recalculate_disk_usage_user_id,
|
|
deleted_metadata_file_ids.id AS deleted_metadata_file_id,
|
|
deleted_metadata_file_ids.uuid AS deleted_metadata_file_uuid,
|
|
deleted_metadata_file_ids.object_store_id AS object_store_id,
|
|
deleted_icda_ids.id AS deleted_icda_id,
|
|
deleted_icda_ids.hda_id AS deleted_icda_hda_id
|
|
FROM purged_hda_ids
|
|
LEFT OUTER JOIN history
|
|
ON purged_hda_ids.history_id = history.id
|
|
LEFT OUTER JOIN deleted_metadata_file_ids
|
|
ON deleted_metadata_file_ids.hda_id = purged_hda_ids.id
|
|
LEFT OUTER JOIN deleted_icda_ids
|
|
ON deleted_icda_ids.hda_parent_id = purged_hda_ids.id
|
|
ORDER BY purged_hda_ids.id
|
|
"""
|
|
causals = (
|
|
("purged_hda_id", "deleted_metadata_file_id", "object_store_id"),
|
|
("purged_hda_id", "deleted_icda_id", "deleted_icda_hda_id"),
|
|
)
|
|
|
|
|
|
class PurgeHDAsOfPurgedHistories(PurgesHDAs, RequiresDiskUsageRecalculation, Action):
|
|
"""
|
|
- Mark purged all HistoryDatasetAssociations in histories that are purged and older than the
|
|
specified number of days.
|
|
"""
|
|
|
|
force_retry_sql = " AND NOT history_dataset_association.purged"
|
|
_action_sql = """
|
|
WITH purged_hda_ids
|
|
AS ( UPDATE history_dataset_association
|
|
SET purged = true, deleted = true{update_time_sql}
|
|
FROM history
|
|
WHERE history_dataset_association.history_id = history.id{force_retry_sql}
|
|
AND history.purged
|
|
AND history.update_time < (NOW() AT TIME ZONE 'utc' - interval '%(days)s days')
|
|
RETURNING history_dataset_association.id as id,
|
|
history_dataset_association.history_id as history_id),
|
|
hda_events
|
|
AS (INSERT INTO cleanup_event_hda_association
|
|
(create_time, cleanup_event_id, hda_id)
|
|
SELECT NOW() AT TIME ZONE 'utc', %(event_id)s, id
|
|
FROM purged_hda_ids),
|
|
{purge_hda_dependencies_sql}
|
|
SELECT purged_hda_ids.id AS purged_hda_id,
|
|
history.user_id AS recalculate_disk_usage_user_id,
|
|
deleted_metadata_file_ids.id AS deleted_metadata_file_id,
|
|
deleted_metadata_file_ids.uuid AS deleted_metadata_file_uuid,
|
|
deleted_metadata_file_ids.object_store_id AS object_store_id,
|
|
deleted_icda_ids.id AS deleted_icda_id,
|
|
deleted_icda_ids.hda_id AS deleted_icda_hda_id
|
|
FROM purged_hda_ids
|
|
LEFT OUTER JOIN history
|
|
ON purged_hda_ids.history_id = history.id
|
|
LEFT OUTER JOIN deleted_metadata_file_ids
|
|
ON deleted_metadata_file_ids.hda_id = purged_hda_ids.id
|
|
LEFT OUTER JOIN deleted_icda_ids
|
|
ON deleted_icda_ids.hda_parent_id = purged_hda_ids.id
|
|
ORDER BY purged_hda_ids.id
|
|
"""
|
|
|
|
|
|
class PurgeDeletedHistories(PurgesHDAs, RequiresDiskUsageRecalculation, Action):
|
|
"""
|
|
- Mark purged all Histories marked deleted that are older than the specified number of days.
|
|
- Mark purged all HistoryDatasetAssociations in Histories marked purged in this step (if not
|
|
already purged).
|
|
"""
|
|
|
|
_action_sql = """
|
|
WITH purged_history_ids
|
|
AS ( UPDATE history
|
|
SET purged = true{update_time_sql}
|
|
WHERE deleted{force_retry_sql}
|
|
AND update_time < (NOW() AT TIME ZONE 'utc' - interval '%(days)s days')
|
|
RETURNING id,
|
|
user_id),
|
|
history_events
|
|
AS (INSERT INTO cleanup_event_history_association
|
|
(create_time, cleanup_event_id, history_id)
|
|
SELECT NOW() AT TIME ZONE 'utc', %(event_id)s, id
|
|
FROM purged_history_ids),
|
|
purged_hda_ids
|
|
AS ( UPDATE history_dataset_association
|
|
SET purged = true, deleted = true{update_time_sql}
|
|
FROM purged_history_ids
|
|
WHERE purged_history_ids.id = history_dataset_association.history_id
|
|
AND NOT history_dataset_association.purged
|
|
RETURNING history_dataset_association.history_id AS history_id,
|
|
history_dataset_association.id AS id),
|
|
hda_events
|
|
AS (INSERT INTO cleanup_event_hda_association
|
|
(create_time, cleanup_event_id, hda_id)
|
|
SELECT NOW() AT TIME ZONE 'utc', %(event_id)s, id
|
|
FROM purged_hda_ids),
|
|
{purge_hda_dependencies_sql}
|
|
SELECT purged_history_ids.id AS purged_history_id,
|
|
purged_history_ids.user_id AS recalculate_disk_usage_user_id,
|
|
purged_hda_ids.id AS purged_hda_id,
|
|
deleted_metadata_file_ids.id AS deleted_metadata_file_id,
|
|
deleted_metadata_file_ids.uuid AS deleted_metadata_file_uuid,
|
|
deleted_metadata_file_ids.object_store_id AS object_store_id,
|
|
deleted_icda_ids.id AS deleted_icda_id,
|
|
deleted_icda_ids.hda_id AS deleted_icda_hda_id
|
|
FROM purged_history_ids
|
|
LEFT OUTER JOIN purged_hda_ids
|
|
ON purged_history_ids.id = purged_hda_ids.history_id
|
|
LEFT OUTER JOIN deleted_metadata_file_ids
|
|
ON deleted_metadata_file_ids.hda_id = purged_hda_ids.id
|
|
LEFT OUTER JOIN deleted_icda_ids
|
|
ON deleted_icda_ids.hda_parent_id = purged_hda_ids.id
|
|
ORDER BY purged_history_ids.id
|
|
"""
|
|
causals = (
|
|
("purged_history_id", "purged_hda_id"),
|
|
("purged_hda_id", "deleted_metadata_file_id", "object_store_id"),
|
|
("purged_hda_id", "deleted_icda_id", "deleted_icda_hda_id"),
|
|
)
|
|
|
|
|
|
class DeleteExportedHistories(Action):
|
|
"""
|
|
- Mark deleted all Datasets that are derivative of JobExportHistoryArchives that are older than
|
|
the specified number of days.
|
|
"""
|
|
|
|
_action_sql = """
|
|
WITH deleted_dataset_ids
|
|
AS ( UPDATE dataset
|
|
SET deleted = true{update_time_sql}
|
|
FROM job_export_history_archive
|
|
WHERE job_export_history_archive.dataset_id = dataset.id
|
|
AND NOT deleted {object_store_id_sql}
|
|
AND dataset.update_time <= (NOW() AT TIME ZONE 'utc' - interval '%(days)s days')
|
|
RETURNING dataset.id),
|
|
dataset_events
|
|
AS (INSERT INTO cleanup_event_dataset_association
|
|
(create_time, cleanup_event_id, dataset_id)
|
|
SELECT NOW() AT TIME ZONE 'utc', %(event_id)s, id
|
|
FROM deleted_dataset_ids)
|
|
SELECT id AS deleted_dataset_id
|
|
FROM deleted_dataset_ids
|
|
ORDER BY id
|
|
"""
|
|
|
|
|
|
class DeleteDatasets(Action):
|
|
"""
|
|
- Mark deleted all Datasets whose associations are all marked as deleted (LDDA) or purged (HDA)
|
|
that are older than the specified number of days.
|
|
- JobExportHistoryArchives have no deleted column, so the datasets for these will simply be
|
|
deleted after the specified number of days
|
|
"""
|
|
|
|
_action_sql = """
|
|
WITH deleted_dataset_ids
|
|
AS ( UPDATE dataset
|
|
SET deleted = true{update_time_sql}
|
|
WHERE NOT deleted {object_store_id_sql}
|
|
AND NOT EXISTS
|
|
(SELECT true
|
|
FROM library_dataset_dataset_association
|
|
WHERE (NOT deleted
|
|
OR update_time >= (NOW() AT TIME ZONE 'utc' - interval '%(days)s days'))
|
|
AND dataset.id = dataset_id)
|
|
AND NOT EXISTS
|
|
(SELECT true
|
|
FROM history_dataset_association
|
|
WHERE (NOT purged
|
|
OR update_time >= (NOW() AT TIME ZONE 'utc' - interval '%(days)s days'))
|
|
AND dataset.id = dataset_id)
|
|
RETURNING id),
|
|
dataset_events
|
|
AS (INSERT INTO cleanup_event_dataset_association
|
|
(create_time, cleanup_event_id, dataset_id)
|
|
SELECT NOW() AT TIME ZONE 'utc', %(event_id)s, id
|
|
FROM deleted_dataset_ids)
|
|
SELECT id
|
|
FROM deleted_dataset_ids
|
|
ORDER BY id
|
|
"""
|
|
|
|
|
|
class PurgeDatasets(RemovesDatasets, Action):
|
|
"""
|
|
- Mark purged all Datasets marked deleted that are older than the specified number of days.
|
|
"""
|
|
|
|
_action_sql = """
|
|
WITH purged_dataset_ids
|
|
AS ( UPDATE dataset
|
|
SET purged = true{update_time_sql}
|
|
WHERE deleted{force_retry_sql}{object_store_id_sql}
|
|
AND update_time < (NOW() AT TIME ZONE 'utc' - interval '%(days)s days')
|
|
RETURNING id,
|
|
uuid,
|
|
object_store_id),
|
|
dataset_events
|
|
AS (INSERT INTO cleanup_event_dataset_association
|
|
(create_time, cleanup_event_id, dataset_id)
|
|
SELECT NOW() AT TIME ZONE 'utc', %(event_id)s, id
|
|
FROM purged_dataset_ids)
|
|
SELECT id AS purged_dataset_id,
|
|
uuid AS purged_dataset_uuid,
|
|
object_store_id AS object_store_id
|
|
FROM purged_dataset_ids
|
|
ORDER BY id
|
|
"""
|
|
|
|
|
|
class Cleanup:
|
|
def __init__(self):
|
|
self.args = None
|
|
self.config = None
|
|
self.__conn = None
|
|
self.__actions = None
|
|
self.__current_action = None
|
|
|
|
self.__parse_args()
|
|
self.__setup_logging()
|
|
self.__validate_actions()
|
|
self.__load_config()
|
|
|
|
def __enter__(self):
|
|
return self
|
|
|
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
if self.__conn is not None:
|
|
self.__conn.close()
|
|
|
|
@property
|
|
def actions(self):
|
|
if self.__actions is None:
|
|
self.__actions = {}
|
|
for name, value in inspect.getmembers(sys.modules[__name__]):
|
|
if (
|
|
not name.startswith("_")
|
|
and inspect.isclass(value)
|
|
and value != Action
|
|
and issubclass(value, Action)
|
|
):
|
|
self.__actions[value.name_c()] = value
|
|
return self.__actions
|
|
|
|
@property
|
|
def conn(self):
|
|
if self.__conn is None:
|
|
url = make_url(self.config.database_connection)
|
|
log.info(f"Connecting to database with URL: {url}")
|
|
args = url.translate_connect_args(username="user")
|
|
args.update(url.query)
|
|
assert url.get_dialect().name == "postgresql", "This script can only be used with PostgreSQL."
|
|
self.__conn = psycopg2.connect(cursor_factory=NamedTupleCursor, **args)
|
|
# TODO: is this per session or cursor?
|
|
if self.args.work_mem is not None:
|
|
log.info("Setting work_mem to %s", self.args.work_mem)
|
|
self.__conn.cursor().execute("SET work_mem TO %s", (self.args.work_mem,))
|
|
return self.__conn
|
|
|
|
def __parse_args(self):
|
|
parser = argparse.ArgumentParser()
|
|
populate_config_args(parser)
|
|
parser.add_argument(
|
|
"-d", "--debug", action="store_true", default=False, help="Enable debug logging (SQL queries)"
|
|
)
|
|
parser.add_argument("--dry-run", action="store_true", default=False, help="Dry run (rollback all transactions)")
|
|
parser.add_argument(
|
|
"--force-retry", action="store_true", default=False, help="Retry file removals (on applicable actions)"
|
|
)
|
|
parser.add_argument(
|
|
"-o",
|
|
"--older-than",
|
|
dest="days",
|
|
type=int,
|
|
default=14,
|
|
help="Only perform action(s) on objects that have not been updated since the specified number of days",
|
|
)
|
|
parser.add_argument(
|
|
"--object-store-id",
|
|
dest="object_store_id",
|
|
type=str,
|
|
default=None,
|
|
help="Only perform action(s) on objects stored in the target object store (for dataset operations - ignored by user/history centric operations)",
|
|
)
|
|
parser.add_argument(
|
|
"-U",
|
|
"--no-update-time",
|
|
action="store_false",
|
|
dest="update_time",
|
|
default=True,
|
|
help="Don't set update_time on updated objects",
|
|
)
|
|
parser.add_argument(
|
|
"-s", "--sequence", dest="sequence", default="", help="DEPRECATED: Comma-separated sequence of actions"
|
|
)
|
|
parser.add_argument(
|
|
"-w", "--work-mem", dest="work_mem", default=None, help="Set PostgreSQL work_mem for this connection"
|
|
)
|
|
parser.add_argument("-l", "--log-dir", default=DEFAULT_LOG_DIR, help="Log file directory")
|
|
parser.add_argument("-g", "--log-file", default=None, help="Log file name")
|
|
parser.add_argument(
|
|
"actions",
|
|
nargs="*",
|
|
metavar="ACTION",
|
|
default=[],
|
|
help="Action(s) to perform, chosen from: {}".format(", ".join(sorted(self.actions.keys()))),
|
|
)
|
|
self.args = parser.parse_args()
|
|
|
|
# add deprecated sequence arg to actions
|
|
self.args.sequence = [x.strip() for x in self.args.sequence.split(",")]
|
|
if self.args.sequence != [""]:
|
|
self.args.actions.extend(self.args.sequence)
|
|
if not self.args.actions:
|
|
parser.error("Please specify one or more actions")
|
|
|
|
def __setup_logging(self):
|
|
logging.basicConfig(
|
|
level=logging.DEBUG if self.args.debug else logging.INFO,
|
|
format="%(asctime)s %(levelname)-5s %(funcName)s(): %(message)s",
|
|
)
|
|
|
|
def __validate_actions(self):
|
|
ok = True
|
|
for name in self.args.actions:
|
|
if name not in self.actions.keys():
|
|
log.error("Unknown action in sequence: %s", name)
|
|
ok = False
|
|
if not ok:
|
|
log.critical("Exiting due to previous error(s)")
|
|
sys.exit(1)
|
|
|
|
def __load_config(self):
|
|
app_properties = app_properties_from_args(self.args)
|
|
self.config = galaxy.config.Configuration(**app_properties)
|
|
|
|
def _dry_run_event(self):
|
|
cur = self.conn.cursor()
|
|
sql = "SELECT MAX(id) FROM cleanup_event;"
|
|
cur.execute(sql)
|
|
max_id = cur.fetchone()[0]
|
|
if max_id is None:
|
|
# there has to be at least one event in the table, if there are none just create a fake one.
|
|
sql = "INSERT INTO cleanup_event (create_time, message) VALUES (NOW(), 'dry_run_event') RETURNING id;"
|
|
cur.execute(sql)
|
|
max_id = cur.fetchone()[0]
|
|
self.conn.commit()
|
|
log.info("An event must exist for the subsequent query to succeed, so a dummy event has been created")
|
|
else:
|
|
log.info(
|
|
"Not executing event creation (increments sequence even when rolling back), using an old "
|
|
"event ID (%i) for dry run" % max_id
|
|
)
|
|
return max_id
|
|
|
|
def _execute(self, sql, args):
|
|
cur = self.conn.cursor()
|
|
sql_str = cur.mogrify(sql, args).decode("utf-8")
|
|
log.debug(f"SQL is: {sql_str}")
|
|
log.info("Executing SQL")
|
|
cur.execute(sql, args)
|
|
log.info("Database status: %s", cur.statusmessage)
|
|
return cur
|
|
|
|
def _create_event(self, message=None):
|
|
"""
|
|
Create a new event in the cleanup_event table.
|
|
"""
|
|
if self.args.dry_run:
|
|
return self._dry_run_event()
|
|
|
|
sql = """
|
|
INSERT INTO cleanup_event
|
|
(create_time, message)
|
|
VALUES (NOW() AT TIME ZONE 'utc', %(message)s)
|
|
RETURNING id;
|
|
"""
|
|
message = message or self.__current_action
|
|
args = {"message": message}
|
|
event_id = self._execute(sql, args).fetchone()[0]
|
|
log.info("Created event %s for action: %s", event_id, self.__current_action)
|
|
return event_id
|
|
|
|
def _update(self, sql, args, add_event=True, event_message=None):
|
|
if add_event and "event_id" not in args:
|
|
args["event_id"] = self._create_event(message=event_message)
|
|
cur = self._execute(sql, args)
|
|
if cur.rowcount <= 0:
|
|
log.info("Update resulted in no changes, rolling back transaction")
|
|
self.conn.rollback()
|
|
else:
|
|
log.info("Flushing transaction")
|
|
self._flush()
|
|
return cur
|
|
|
|
def _flush(self):
|
|
if self.args.dry_run:
|
|
self.conn.rollback()
|
|
log.info("--dry-run specified, all changes rolled back")
|
|
else:
|
|
self.conn.commit()
|
|
log.info("All changes committed")
|
|
|
|
def _run_action(self, action):
|
|
cur = self._update(action.sql, action.sql_args)
|
|
action.handle_results(cur)
|
|
|
|
def run(self):
|
|
for name in self.args.actions:
|
|
cls = self.actions[name]
|
|
log.info("Running action '%s':", name)
|
|
map(log.info, cls.doc_iter())
|
|
self.__current_action = name
|
|
with cls(self) as action:
|
|
self._run_action(action)
|
|
log.info("Finished %s", name)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
with Cleanup() as app:
|
|
try:
|
|
app.run()
|
|
except Exception:
|
|
log.exception("Caught exception in run sequence:")
|