These particular imports are no longer needed in a Python 3-only world. Change-Id: I5e9e15556c04871c451f6363380f2a7ac026c968
474 lines
16 KiB
Python
474 lines
16 KiB
Python
# -*- encoding: utf-8 -*-
|
|
# Copyright (c) 2016 b<>com
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
# implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
#
|
|
|
|
import collections
|
|
import datetime
|
|
import itertools
|
|
import sys
|
|
|
|
from oslo_log import log
|
|
from oslo_utils import strutils
|
|
import prettytable as ptable
|
|
|
|
from watcher._i18n import _
|
|
from watcher._i18n import lazy_translation_enabled
|
|
from watcher.common import context
|
|
from watcher.common import exception
|
|
from watcher.common import utils
|
|
from watcher import objects
|
|
|
|
LOG = log.getLogger(__name__)
|
|
|
|
|
|
class WatcherObjectsMap(object):
|
|
"""Wrapper to deal with watcher objects per type
|
|
|
|
This wrapper object contains a list of watcher objects per type.
|
|
Its main use is to simplify the merge of watcher objects by avoiding
|
|
duplicates, but also for representing the relationships between these
|
|
objects.
|
|
"""
|
|
|
|
# This is for generating the .pot translations
|
|
keymap = collections.OrderedDict([
|
|
("goals", _("Goals")),
|
|
("strategies", _("Strategies")),
|
|
("audit_templates", _("Audit Templates")),
|
|
("audits", _("Audits")),
|
|
("action_plans", _("Action Plans")),
|
|
("actions", _("Actions")),
|
|
])
|
|
|
|
def __init__(self):
|
|
for attr_name in self.keys():
|
|
setattr(self, attr_name, [])
|
|
|
|
def values(self):
|
|
return (getattr(self, key) for key in self.keys())
|
|
|
|
@classmethod
|
|
def keys(cls):
|
|
return cls.keymap.keys()
|
|
|
|
def __iter__(self):
|
|
return itertools.chain(*self.values())
|
|
|
|
def __add__(self, other):
|
|
new_map = self.__class__()
|
|
|
|
# Merge the 2 items dicts into a new object (and avoid dupes)
|
|
for attr_name, initials, others in zip(self.keys(), self.values(),
|
|
other.values()):
|
|
# Creates a copy
|
|
merged = initials[:]
|
|
initials_ids = [item.id for item in initials]
|
|
non_dupes = [item for item in others
|
|
if item.id not in initials_ids]
|
|
merged += non_dupes
|
|
|
|
setattr(new_map, attr_name, merged)
|
|
|
|
return new_map
|
|
|
|
def __str__(self):
|
|
out = ""
|
|
for key, vals in zip(self.keys(), self.values()):
|
|
ids = [val.id for val in vals]
|
|
out += "%(key)s: %(val)s" % (dict(key=key, val=ids))
|
|
out += "\n"
|
|
return out
|
|
|
|
def __len__(self):
|
|
return sum(len(getattr(self, key)) for key in self.keys())
|
|
|
|
def get_count_table(self):
|
|
headers = list(self.keymap.values())
|
|
headers.append(_("Total")) # We also add a total count
|
|
translated_headers = [
|
|
h.translate() if lazy_translation_enabled() else h
|
|
for h in headers
|
|
]
|
|
|
|
counters = [len(cat_vals) for cat_vals in self.values()] + [len(self)]
|
|
table = ptable.PrettyTable(field_names=translated_headers)
|
|
table.add_row(counters)
|
|
return table.get_string()
|
|
|
|
|
|
class PurgeCommand(object):
|
|
"""Purges the DB by removing soft deleted entries
|
|
|
|
The workflow for this purge is the following:
|
|
|
|
# Find soft deleted objects which are expired
|
|
# Find orphan objects
|
|
# Find their related objects whether they are expired or not
|
|
# Merge them together
|
|
# If it does not exceed the limit, destroy them all
|
|
"""
|
|
|
|
ctx = context.make_context(show_deleted=True)
|
|
|
|
def __init__(self, age_in_days=None, max_number=None,
|
|
uuid=None, exclude_orphans=False, dry_run=None):
|
|
self.age_in_days = age_in_days
|
|
self.max_number = max_number
|
|
self.uuid = uuid
|
|
self.exclude_orphans = exclude_orphans
|
|
self.dry_run = dry_run
|
|
|
|
self._delete_up_to_max = None
|
|
self._objects_map = WatcherObjectsMap()
|
|
|
|
def get_expiry_date(self):
|
|
if not self.age_in_days:
|
|
return None
|
|
today = datetime.datetime.today()
|
|
expiry_date = today - datetime.timedelta(days=self.age_in_days)
|
|
return expiry_date
|
|
|
|
@classmethod
|
|
def get_goal_uuid(cls, uuid_or_name):
|
|
if uuid_or_name is None:
|
|
return
|
|
|
|
query_func = None
|
|
if not utils.is_uuid_like(uuid_or_name):
|
|
query_func = objects.Goal.get_by_name
|
|
else:
|
|
query_func = objects.Goal.get_by_uuid
|
|
|
|
try:
|
|
goal = query_func(cls.ctx, uuid_or_name)
|
|
except Exception as exc:
|
|
LOG.exception(exc)
|
|
raise exception.GoalNotFound(goal=uuid_or_name)
|
|
|
|
if not goal.deleted_at:
|
|
raise exception.NotSoftDeletedStateError(
|
|
name=_('Goal'), id=uuid_or_name)
|
|
|
|
return goal.uuid
|
|
|
|
def _find_goals(self, filters=None):
|
|
return objects.Goal.list(self.ctx, filters=filters)
|
|
|
|
def _find_strategies(self, filters=None):
|
|
return objects.Strategy.list(self.ctx, filters=filters)
|
|
|
|
def _find_audit_templates(self, filters=None):
|
|
return objects.AuditTemplate.list(self.ctx, filters=filters)
|
|
|
|
def _find_audits(self, filters=None):
|
|
return objects.Audit.list(self.ctx, filters=filters)
|
|
|
|
def _find_action_plans(self, filters=None):
|
|
return objects.ActionPlan.list(self.ctx, filters=filters)
|
|
|
|
def _find_actions(self, filters=None):
|
|
return objects.Action.list(self.ctx, filters=filters)
|
|
|
|
def _find_orphans(self):
|
|
orphans = WatcherObjectsMap()
|
|
|
|
filters = dict(deleted=False)
|
|
goals = objects.Goal.list(self.ctx, filters=filters)
|
|
strategies = objects.Strategy.list(self.ctx, filters=filters)
|
|
audit_templates = objects.AuditTemplate.list(self.ctx, filters=filters)
|
|
audits = objects.Audit.list(self.ctx, filters=filters)
|
|
action_plans = objects.ActionPlan.list(self.ctx, filters=filters)
|
|
actions = objects.Action.list(self.ctx, filters=filters)
|
|
|
|
goal_ids = set(g.id for g in goals)
|
|
orphans.strategies = [
|
|
strategy for strategy in strategies
|
|
if strategy.goal_id not in goal_ids]
|
|
|
|
strategy_ids = [s.id for s in (s for s in strategies
|
|
if s not in orphans.strategies)]
|
|
orphans.audit_templates = [
|
|
audit_template for audit_template in audit_templates
|
|
if audit_template.goal_id not in goal_ids or
|
|
(audit_template.strategy_id and
|
|
audit_template.strategy_id not in strategy_ids)]
|
|
|
|
orphans.audits = [
|
|
audit for audit in audits
|
|
if audit.goal_id not in goal_ids or
|
|
(audit.strategy_id and
|
|
audit.strategy_id not in strategy_ids)]
|
|
|
|
# Objects with orphan parents are themselves orphans
|
|
audit_ids = [audit.id for audit in audits
|
|
if audit not in orphans.audits]
|
|
orphans.action_plans = [
|
|
ap for ap in action_plans
|
|
if ap.audit_id not in audit_ids or
|
|
ap.strategy_id not in strategy_ids]
|
|
|
|
# Objects with orphan parents are themselves orphans
|
|
action_plan_ids = [ap.id for ap in action_plans
|
|
if ap not in orphans.action_plans]
|
|
orphans.actions = [
|
|
action for action in actions
|
|
if action.action_plan_id not in action_plan_ids]
|
|
|
|
LOG.debug("Orphans found:\n%s", orphans)
|
|
LOG.info("Orphans found:\n%s", orphans.get_count_table())
|
|
|
|
return orphans
|
|
|
|
def _find_soft_deleted_objects(self):
|
|
to_be_deleted = WatcherObjectsMap()
|
|
expiry_date = self.get_expiry_date()
|
|
filters = dict(deleted=True)
|
|
|
|
if self.uuid:
|
|
filters["uuid"] = self.uuid
|
|
if expiry_date:
|
|
filters.update(dict(deleted_at__lt=expiry_date))
|
|
|
|
to_be_deleted.goals.extend(self._find_goals(filters))
|
|
to_be_deleted.strategies.extend(self._find_strategies(filters))
|
|
to_be_deleted.audit_templates.extend(
|
|
self._find_audit_templates(filters))
|
|
to_be_deleted.audits.extend(self._find_audits(filters))
|
|
to_be_deleted.action_plans.extend(
|
|
self._find_action_plans(filters))
|
|
to_be_deleted.actions.extend(self._find_actions(filters))
|
|
|
|
soft_deleted_objs = self._find_related_objects(
|
|
to_be_deleted, base_filters=dict(deleted=True))
|
|
|
|
LOG.debug("Soft deleted objects:\n%s", soft_deleted_objs)
|
|
|
|
return soft_deleted_objs
|
|
|
|
def _find_related_objects(self, objects_map, base_filters=None):
|
|
base_filters = base_filters or {}
|
|
|
|
for goal in objects_map.goals:
|
|
filters = {}
|
|
filters.update(base_filters)
|
|
filters.update(dict(goal_id=goal.id))
|
|
related_objs = WatcherObjectsMap()
|
|
related_objs.strategies = self._find_strategies(filters)
|
|
related_objs.audit_templates = self._find_audit_templates(filters)
|
|
related_objs.audits = self._find_audits(filters)
|
|
objects_map += related_objs
|
|
|
|
for strategy in objects_map.strategies:
|
|
filters = {}
|
|
filters.update(base_filters)
|
|
filters.update(dict(strategy_id=strategy.id))
|
|
related_objs = WatcherObjectsMap()
|
|
related_objs.audit_templates = self._find_audit_templates(filters)
|
|
related_objs.audits = self._find_audits(filters)
|
|
objects_map += related_objs
|
|
|
|
for audit in objects_map.audits:
|
|
filters = {}
|
|
filters.update(base_filters)
|
|
filters.update(dict(audit_id=audit.id))
|
|
related_objs = WatcherObjectsMap()
|
|
related_objs.action_plans = self._find_action_plans(filters)
|
|
objects_map += related_objs
|
|
|
|
for action_plan in objects_map.action_plans:
|
|
filters = {}
|
|
filters.update(base_filters)
|
|
filters.update(dict(action_plan_id=action_plan.id))
|
|
related_objs = WatcherObjectsMap()
|
|
related_objs.actions = self._find_actions(filters)
|
|
objects_map += related_objs
|
|
|
|
return objects_map
|
|
|
|
def confirmation_prompt(self):
|
|
print(self._objects_map.get_count_table())
|
|
raw_val = input(
|
|
_("There are %(count)d objects set for deletion. "
|
|
"Continue? [y/N]") % dict(count=len(self._objects_map)))
|
|
|
|
return strutils.bool_from_string(raw_val)
|
|
|
|
def delete_up_to_max_prompt(self, objects_map):
|
|
print(objects_map.get_count_table())
|
|
print(_("The number of objects (%(num)s) to delete from the database "
|
|
"exceeds the maximum number of objects (%(max_number)s) "
|
|
"specified.") % dict(max_number=self.max_number,
|
|
num=len(objects_map)))
|
|
raw_val = input(
|
|
_("Do you want to delete objects up to the specified maximum "
|
|
"number? [y/N]"))
|
|
|
|
self._delete_up_to_max = strutils.bool_from_string(raw_val)
|
|
|
|
return self._delete_up_to_max
|
|
|
|
def _aggregate_objects(self):
|
|
"""Objects aggregated on a 'per goal' basis"""
|
|
# todo: aggregate orphans as well
|
|
aggregate = []
|
|
for goal in self._objects_map.goals:
|
|
related_objs = WatcherObjectsMap()
|
|
|
|
# goals
|
|
related_objs.goals = [goal]
|
|
|
|
# strategies
|
|
goal_ids = [goal.id]
|
|
related_objs.strategies = [
|
|
strategy for strategy in self._objects_map.strategies
|
|
if strategy.goal_id in goal_ids
|
|
]
|
|
|
|
# audit templates
|
|
strategy_ids = [
|
|
strategy.id for strategy in related_objs.strategies]
|
|
related_objs.audit_templates = [
|
|
at for at in self._objects_map.audit_templates
|
|
if at.goal_id in goal_ids or
|
|
(at.strategy_id and at.strategy_id in strategy_ids)
|
|
]
|
|
|
|
# audits
|
|
related_objs.audits = [
|
|
audit for audit in self._objects_map.audits
|
|
if audit.goal_id in goal_ids
|
|
]
|
|
|
|
# action plans
|
|
audit_ids = [audit.id for audit in related_objs.audits]
|
|
related_objs.action_plans = [
|
|
action_plan for action_plan in self._objects_map.action_plans
|
|
if action_plan.audit_id in audit_ids
|
|
]
|
|
|
|
# actions
|
|
action_plan_ids = [
|
|
action_plan.id for action_plan in related_objs.action_plans
|
|
]
|
|
related_objs.actions = [
|
|
action for action in self._objects_map.actions
|
|
if action.action_plan_id in action_plan_ids
|
|
]
|
|
aggregate.append(related_objs)
|
|
|
|
return aggregate
|
|
|
|
def _get_objects_up_to_limit(self):
|
|
aggregated_objects = self._aggregate_objects()
|
|
to_be_deleted_subset = WatcherObjectsMap()
|
|
|
|
for aggregate in aggregated_objects:
|
|
if len(aggregate) + len(to_be_deleted_subset) <= self.max_number:
|
|
to_be_deleted_subset += aggregate
|
|
else:
|
|
break
|
|
|
|
LOG.debug(to_be_deleted_subset)
|
|
return to_be_deleted_subset
|
|
|
|
def find_objects_to_delete(self):
|
|
"""Finds all the objects to be purged
|
|
|
|
:returns: A mapping with all the Watcher objects to purged
|
|
:rtype: :py:class:`~.WatcherObjectsMap` instance
|
|
"""
|
|
to_be_deleted = self._find_soft_deleted_objects()
|
|
|
|
if not self.exclude_orphans:
|
|
to_be_deleted += self._find_orphans()
|
|
|
|
LOG.debug("Objects to be deleted:\n%s", to_be_deleted)
|
|
|
|
return to_be_deleted
|
|
|
|
def do_delete(self):
|
|
LOG.info("Deleting...")
|
|
# Reversed to avoid errors with foreign keys
|
|
for entry in reversed(list(self._objects_map)):
|
|
entry.destroy()
|
|
|
|
def execute(self):
|
|
LOG.info("Starting purge command")
|
|
self._objects_map = self.find_objects_to_delete()
|
|
|
|
if (self.max_number is not None and
|
|
len(self._objects_map) > self.max_number):
|
|
if self.delete_up_to_max_prompt(self._objects_map):
|
|
self._objects_map = self._get_objects_up_to_limit()
|
|
else:
|
|
return
|
|
|
|
_orphans_note = (_(" (orphans excluded)") if self.exclude_orphans
|
|
else _(" (may include orphans)"))
|
|
if not self.dry_run and self.confirmation_prompt():
|
|
self.do_delete()
|
|
print(_("Purge results summary%s:") % _orphans_note)
|
|
LOG.info("Purge results summary%s:", _orphans_note)
|
|
else:
|
|
LOG.debug(self._objects_map)
|
|
print(_("Here below is a table containing the objects "
|
|
"that can be purged%s:") % _orphans_note)
|
|
|
|
LOG.info("\n%s", self._objects_map.get_count_table())
|
|
print(self._objects_map.get_count_table())
|
|
LOG.info("Purge process completed")
|
|
|
|
|
|
def purge(age_in_days, max_number, goal, exclude_orphans, dry_run):
|
|
"""Removes soft deleted objects from the database
|
|
|
|
:param age_in_days: Number of days since deletion (from today)
|
|
to exclude from the purge. If None, everything will be purged.
|
|
:type age_in_days: int
|
|
:param max_number: Max number of objects expected to be deleted.
|
|
Prevents the deletion if exceeded. No limit if set to None.
|
|
:type max_number: int
|
|
:param goal: UUID or name of the goal to purge.
|
|
:type goal: str
|
|
:param exclude_orphans: Flag to indicate whether or not you want to
|
|
exclude orphans from deletion (default: False).
|
|
:type exclude_orphans: bool
|
|
:param dry_run: Flag to indicate whether or not you want to perform
|
|
a dry run (no deletion).
|
|
:type dry_run: bool
|
|
"""
|
|
try:
|
|
if max_number and max_number < 0:
|
|
raise exception.NegativeLimitError
|
|
|
|
LOG.info("[options] age_in_days = %s", age_in_days)
|
|
LOG.info("[options] max_number = %s", max_number)
|
|
LOG.info("[options] goal = %s", goal)
|
|
LOG.info("[options] exclude_orphans = %s", exclude_orphans)
|
|
LOG.info("[options] dry_run = %s", dry_run)
|
|
|
|
uuid = PurgeCommand.get_goal_uuid(goal)
|
|
|
|
cmd = PurgeCommand(age_in_days, max_number, uuid,
|
|
exclude_orphans, dry_run)
|
|
|
|
cmd.execute()
|
|
|
|
except Exception as exc:
|
|
LOG.exception(exc)
|
|
print(exc)
|
|
sys.exit(1)
|