From 0a32412e909146a5d5dc605f83bf61b8d5e9e7c3 Mon Sep 17 00:00:00 2001 From: Max Lamprecht Date: Wed, 24 Apr 2024 10:04:50 +0000 Subject: [PATCH] feat: nova-manage db instace events cleanup This introduces a new "nova-manage db clear_events" option that cleans instance events. This can be helpful to run regulary to reduce databasize size if you have many instance-actions on long existing VMs e.g. live-migration/migrations --- nova/cmd/manage.py | 74 +++++++++++++++++++++++++++++++++++ nova/db/main/api.py | 94 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 168 insertions(+) diff --git a/nova/cmd/manage.py b/nova/cmd/manage.py index 1b017654be..f5f076c40e 100644 --- a/nova/cmd/manage.py +++ b/nova/cmd/manage.py @@ -558,6 +558,80 @@ class DbCommands(object): else: return 3 + @args('--before', metavar='', dest='before', + help='If specified, purge rows from shadow tables that are older ' + 'than this. Accepts date strings in the default format output ' + 'by the ``date`` command, as well as ``YYYY-MM-DD ' + '[HH:mm:ss]``.') + @args('--verbose', dest='verbose', action='store_true', default=False, + help='Print information about purged records') + @args('--all-cells', dest='all_cells', action='store_true', default=False, + help='Run against all cell databases') + @args('--sleep', type=int, metavar='', dest='sleep', + help='The amount of time in seconds to sleep between batches when ' + '``--until-complete`` is used. Defaults to 0.') + @args('--max-rows', type=int, metavar='', dest='max_rows', + help='Maximum number of rows to delete per table. Defaults ' + 'to 1000.') + @args('--only-migration-events', action='store_true', + dest='only_migration_events', default=False, + help='Delete only live-migration and migration events.') + def clear_events(self, before=None, verbose=False, + all_cells=False, sleep=0, max_rows=1000, until_complete=False, + only_migration_events=False): + max_rows = int(max_rows) + if max_rows < 0: + print(_("Must supply a positive value for max_rows")) + return 2 + if max_rows > db_const.MAX_INT: + print(_('max rows must be <= %(max_value)d') % + {'max_value': db_const.MAX_INT}) + return 2 + + if before is None: + print(_('--before is required')) + return 1 + if before: + try: + before_date = dateutil_parser.parse(before, fuzzy=True) + except ValueError as e: + print(_('Invalid value for --before: %s') % e) + return 2 + else: + before_date = None + + def status(msg): + if verbose: + print('%s: %s' % (identity, msg)) + + deleted = 0 + admin_ctxt = context.get_admin_context() + + if all_cells: + try: + cells = objects.CellMappingList.get_all(admin_ctxt) + except db_exc.DBError: + print(_('Unable to get cell list from API DB. ' + 'Is it configured?')) + return 4 + for cell in cells: + deleted = 0 + identity = _('Cell %s') % cell.identity + with context.target_cell(admin_ctxt, cell) as cctxt: + deleted += db.clear_events( + cctxt, before_date, max_rows, sleep, + only_migration_events, + status_fn=status) + else: + identity = _('DB') + deleted = db.clear_events( + admin_ctxt, before_date, max_rows, sleep, + only_migration_events, status_fn=status) + if deleted: + return 0 + else: + return 3 + def _run_migration(self, ctxt, max_count): ran = 0 exceptions = False diff --git a/nova/db/main/api.py b/nova/db/main/api.py index d7ad3c39a5..c0672cd010 100644 --- a/nova/db/main/api.py +++ b/nova/db/main/api.py @@ -22,6 +22,7 @@ import copy import datetime import functools import inspect +import time import traceback from oslo_db import api as oslo_db_api @@ -36,6 +37,7 @@ from oslo_utils import timeutils from oslo_utils import uuidutils import sqlalchemy as sa from sqlalchemy import exc as sqla_exc +from sqlalchemy import or_ from sqlalchemy import orm from sqlalchemy import schema from sqlalchemy import sql @@ -4662,6 +4664,98 @@ def purge_shadow_tables(context, before_date, status_fn=None): return total_deleted +def clear_events(context, before_date, max_rows, sleep, + only_migration_events=True, status_fn=None): + """Clear instance events. + + :param context: nova.context.RequestContext for database access + :param before_date: datetime which when specified filters the records + to only delete records before the given date + :param max_rows: Maximum number of rows to archive (required) + :param sleep: The amount of time in seconds to sleep between batches + :param only_migration_events: Delete only live-migration and migration + events + :param status_fn: Logger function if verbose is set + """ + engine = get_engine(context=context) + conn = engine.connect() + metadata = sa.MetaData() + metadata.reflect(bind=engine) + total_deleted = 0 + + if status_fn is None: + status_fn = lambda m: None + + instance_actions = [ + t for t in metadata.sorted_tables if (t.name == "instance_actions") + ][0] + instance_actions_events = [ + t for t in metadata.sorted_tables + if (t.name == "instance_actions_events") + ][0] + + while True: + if only_migration_events: + select = ( + instance_actions.select() + .where(instance_actions.c.created_at < before_date) + .where( + or_( + instance_actions.c.action == "live-migration", + instance_actions.c.action == "migrate", + ) + ) + .limit(max_rows) + ) + else: + select = ( + instance_actions.select() + .where(instance_actions.c.created_at < before_date) + .limit(max_rows) + ) + + with conn.begin(): + results = conn.execute(select).fetchall() + + records = [r.id for r in results] + + with conn.begin(): + conn.execute( + instance_actions_events.delete().where( + instance_actions_events.c.action_id.in_(records) + ) + ) + deleted = conn.execute( + instance_actions.delete().where( + instance_actions.c.id.in_(records) + ) + ) + + if deleted.rowcount > 0: + status_fn( + _( + "Deleted %(rows)i rows from %(ia_table)s based on" + " timestamp column %(col)s and there corresponding" + " %(iae_table)s events" + ) + % { + "rows": deleted.rowcount, + "ia_table": instance_actions.name, + "col": before_date, + "iae_table": instance_actions_events.name, + } + ) + total_deleted += deleted.rowcount + else: + break + # Optionally sleep between batches to throttle the deletion. + time.sleep(sleep) + + conn.close() + + return total_deleted + + #################### -- 2.34.1