diff --git a/watcher/api/controllers/v1/action_plan.py b/watcher/api/controllers/v1/action_plan.py index 7165e0f42..968f9f439 100644 --- a/watcher/api/controllers/v1/action_plan.py +++ b/watcher/api/controllers/v1/action_plan.py @@ -73,6 +73,7 @@ from watcher.api.controllers.v1 import utils as api_utils from watcher.applier import rpcapi from watcher.common import exception from watcher.common import policy +from watcher.common import utils from watcher import objects from watcher.objects import action_plan as ap_objects @@ -117,6 +118,8 @@ class ActionPlan(base.APIBase): """ _audit_uuid = None + _strategy_uuid = None + _strategy_name = None _first_action_uuid = None _efficacy_indicators = None @@ -177,6 +180,43 @@ class ActionPlan(base.APIBase): elif value and self._efficacy_indicators != value: self._efficacy_indicators = value + def _get_strategy(self, value): + if value == wtypes.Unset: + return None + strategy = None + try: + if utils.is_uuid_like(value) or utils.is_int_like(value): + strategy = objects.Strategy.get( + pecan.request.context, value) + else: + strategy = objects.Strategy.get_by_name( + pecan.request.context, value) + except exception.StrategyNotFound: + pass + if strategy: + self.strategy_id = strategy.id + return strategy + + def _get_strategy_uuid(self): + return self._strategy_uuid + + def _set_strategy_uuid(self, value): + if value and self._strategy_uuid != value: + self._strategy_uuid = None + strategy = self._get_strategy(value) + if strategy: + self._strategy_uuid = strategy.uuid + + def _get_strategy_name(self): + return self._strategy_name + + def _set_strategy_name(self, value): + if value and self._strategy_name != value: + self._strategy_name = None + strategy = self._get_strategy(value) + if strategy: + self._strategy_name = strategy.name + uuid = wtypes.wsattr(types.uuid, readonly=True) """Unique UUID for this action plan""" @@ -189,6 +229,14 @@ class ActionPlan(base.APIBase): mandatory=True) """The UUID of the audit this port belongs to""" + strategy_uuid = wsme.wsproperty( + wtypes.text, _get_strategy_uuid, _set_strategy_uuid, mandatory=False) + """Strategy UUID the action plan refers to""" + + strategy_name = wsme.wsproperty( + wtypes.text, _get_strategy_name, _set_strategy_name, mandatory=False) + """The name of the strategy this action plan refers to""" + efficacy_indicators = wsme.wsproperty( types.jsontype, _get_efficacy_indicators, _set_efficacy_indicators, mandatory=True) @@ -219,6 +267,10 @@ class ActionPlan(base.APIBase): self.fields.append('efficacy_indicators') setattr(self, 'audit_uuid', kwargs.get('audit_id', wtypes.Unset)) + fields.append('strategy_uuid') + setattr(self, 'strategy_uuid', kwargs.get('strategy_id', wtypes.Unset)) + fields.append('strategy_name') + setattr(self, 'strategy_name', kwargs.get('strategy_id', wtypes.Unset)) setattr(self, 'first_action_uuid', kwargs.get('first_action_id', wtypes.Unset)) @@ -227,7 +279,8 @@ class ActionPlan(base.APIBase): if not expand: action_plan.unset_fields_except( ['uuid', 'state', 'efficacy_indicators', 'global_efficacy', - 'updated_at', 'audit_uuid', 'first_action_uuid']) + 'updated_at', 'audit_uuid', 'strategy_uuid', 'strategy_name', + 'first_action_uuid']) action_plan.links = [ link.Link.make_link( @@ -275,8 +328,8 @@ class ActionPlanCollection(collection.Collection): @staticmethod def convert_with_links(rpc_action_plans, limit, url=None, expand=False, **kwargs): - collection = ActionPlanCollection() - collection.action_plans = [ActionPlan.convert_with_links( + ap_collection = ActionPlanCollection() + ap_collection.action_plans = [ActionPlan.convert_with_links( p, expand) for p in rpc_action_plans] if 'sort_key' in kwargs: @@ -284,13 +337,13 @@ class ActionPlanCollection(collection.Collection): if kwargs['sort_key'] == 'audit_uuid': if 'sort_dir' in kwargs: reverse = True if kwargs['sort_dir'] == 'desc' else False - collection.action_plans = sorted( - collection.action_plans, + ap_collection.action_plans = sorted( + ap_collection.action_plans, key=lambda action_plan: action_plan.audit_uuid, reverse=reverse) - collection.next = collection.get_next(limit, url=url, **kwargs) - return collection + ap_collection.next = ap_collection.get_next(limit, url=url, **kwargs) + return ap_collection @classmethod def sample(cls): @@ -301,6 +354,7 @@ class ActionPlanCollection(collection.Collection): class ActionPlansController(rest.RestController): """REST controller for Actions.""" + def __init__(self): super(ActionPlansController, self).__init__() @@ -314,7 +368,8 @@ class ActionPlansController(rest.RestController): def _get_action_plans_collection(self, marker, limit, sort_key, sort_dir, expand=False, - resource_url=None, audit_uuid=None): + resource_url=None, audit_uuid=None, + strategy=None): limit = api_utils.validate_limit(limit) api_utils.validate_sort_dir(sort_dir) @@ -328,6 +383,12 @@ class ActionPlansController(rest.RestController): if audit_uuid: filters['audit_uuid'] = audit_uuid + if strategy: + if utils.is_uuid_like(strategy): + filters['strategy_uuid'] = strategy + else: + filters['strategy_name'] = strategy + if sort_key == 'audit_uuid': sort_db_key = None else: @@ -347,9 +408,9 @@ class ActionPlansController(rest.RestController): sort_dir=sort_dir) @wsme_pecan.wsexpose(ActionPlanCollection, types.uuid, int, wtypes.text, - wtypes.text, types.uuid) + wtypes.text, types.uuid, wtypes.text) def get_all(self, marker=None, limit=None, - sort_key='id', sort_dir='asc', audit_uuid=None): + sort_key='id', sort_dir='asc', audit_uuid=None, strategy=None): """Retrieve a list of action plans. :param marker: pagination marker for large data sets. @@ -358,18 +419,20 @@ class ActionPlansController(rest.RestController): :param sort_dir: direction to sort. "asc" or "desc". Default: asc. :param audit_uuid: Optional UUID of an audit, to get only actions for that audit. + :param strategy: strategy UUID or name to filter by """ context = pecan.request.context policy.enforce(context, 'action_plan:get_all', action='action_plan:get_all') return self._get_action_plans_collection( - marker, limit, sort_key, sort_dir, audit_uuid=audit_uuid) + marker, limit, sort_key, sort_dir, + audit_uuid=audit_uuid, strategy=strategy) @wsme_pecan.wsexpose(ActionPlanCollection, types.uuid, int, wtypes.text, - wtypes.text, types.uuid) + wtypes.text, types.uuid, wtypes.text) def detail(self, marker=None, limit=None, - sort_key='id', sort_dir='asc', audit_uuid=None): + sort_key='id', sort_dir='asc', audit_uuid=None, strategy=None): """Retrieve a list of action_plans with detail. :param marker: pagination marker for large data sets. @@ -378,6 +441,7 @@ class ActionPlansController(rest.RestController): :param sort_dir: direction to sort. "asc" or "desc". Default: asc. :param audit_uuid: Optional UUID of an audit, to get only actions for that audit. + :param strategy: strategy UUID or name to filter by """ context = pecan.request.context policy.enforce(context, 'action_plan:detail', @@ -391,9 +455,8 @@ class ActionPlansController(rest.RestController): expand = True resource_url = '/'.join(['action_plans', 'detail']) return self._get_action_plans_collection( - marker, limit, - sort_key, sort_dir, expand, - resource_url, audit_uuid=audit_uuid) + marker, limit, sort_key, sort_dir, expand, + resource_url, audit_uuid=audit_uuid, strategy=strategy) @wsme_pecan.wsexpose(ActionPlan, types.uuid) def get_one(self, action_plan_uuid): @@ -491,8 +554,8 @@ class ActionPlansController(rest.RestController): if action_plan_to_update[field] != patch_val: action_plan_to_update[field] = patch_val - if (field == 'state' - and patch_val == objects.action_plan.State.PENDING): + if (field == 'state'and + patch_val == objects.action_plan.State.PENDING): launch_action_plan = True action_plan_to_update.save() diff --git a/watcher/api/controllers/v1/audit.py b/watcher/api/controllers/v1/audit.py index 77754738b..8b724cc90 100644 --- a/watcher/api/controllers/v1/audit.py +++ b/watcher/api/controllers/v1/audit.py @@ -198,7 +198,7 @@ class Audit(base.APIBase): else: strategy = objects.Strategy.get_by_name( pecan.request.context, value) - except exception.GoalNotFound: + except exception.StrategyNotFound: pass if strategy: self.strategy_id = strategy.id diff --git a/watcher/db/purge.py b/watcher/db/purge.py index 975d307a1..e3e09a0a0 100644 --- a/watcher/db/purge.py +++ b/watcher/db/purge.py @@ -220,7 +220,8 @@ class PurgeCommand(object): if audit not in orphans.audits] orphans.action_plans = [ ap for ap in action_plans - if ap.audit_id not in audit_ids] + 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 diff --git a/watcher/db/sqlalchemy/api.py b/watcher/db/sqlalchemy/api.py index d3b2ca4ef..394726bd3 100644 --- a/watcher/db/sqlalchemy/api.py +++ b/watcher/db/sqlalchemy/api.py @@ -347,10 +347,15 @@ class Connection(api.BaseConnection): if filters is None: filters = {} - plain_fields = ['uuid', 'state', 'audit_id'] - join_fieldmap = { - 'audit_uuid': ("uuid", models.Audit), - } + plain_fields = ['uuid', 'state', 'audit_id', 'strategy_id'] + join_fieldmap = JoinMap( + audit_uuid=NaturalJoinFilter( + join_fieldname="uuid", join_model=models.Audit), + strategy_uuid=NaturalJoinFilter( + join_fieldname="uuid", join_model=models.Strategy), + strategy_name=NaturalJoinFilter( + join_fieldname="name", join_model=models.Strategy), + ) return self._add_filters( query=query, model=models.ActionPlan, filters=filters, diff --git a/watcher/db/sqlalchemy/models.py b/watcher/db/sqlalchemy/models.py index ddce604c6..be210ce04 100644 --- a/watcher/db/sqlalchemy/models.py +++ b/watcher/db/sqlalchemy/models.py @@ -211,7 +211,8 @@ class ActionPlan(Base): id = Column(Integer, primary_key=True) uuid = Column(String(36)) first_action_id = Column(Integer) - audit_id = Column(Integer, ForeignKey('audits.id'), nullable=True) + audit_id = Column(Integer, ForeignKey('audits.id'), nullable=False) + strategy_id = Column(Integer, ForeignKey('strategies.id'), nullable=False) state = Column(String(20), nullable=True) global_efficacy = Column(JSONEncodedDict, nullable=True) diff --git a/watcher/decision_engine/audit/continuous.py b/watcher/decision_engine/audit/continuous.py index 1711aa798..be2cc2d44 100644 --- a/watcher/decision_engine/audit/continuous.py +++ b/watcher/decision_engine/audit/continuous.py @@ -74,8 +74,8 @@ class ContinuousAuditHandler(base.AuditHandler): def do_execute(self, audit, request_context): # execute the strategy - solution = self.strategy_context.execute_strategy(audit.uuid, - request_context) + solution = self.strategy_context.execute_strategy( + audit, request_context) if audit.audit_type == audit_objects.AuditType.CONTINUOUS.value: a_plan_filters = {'audit_uuid': audit.uuid, diff --git a/watcher/decision_engine/audit/oneshot.py b/watcher/decision_engine/audit/oneshot.py index d7b68d926..c689d6f13 100644 --- a/watcher/decision_engine/audit/oneshot.py +++ b/watcher/decision_engine/audit/oneshot.py @@ -20,7 +20,7 @@ from watcher.decision_engine.audit import base class OneShotAuditHandler(base.AuditHandler): def do_execute(self, audit, request_context): # execute the strategy - solution = self.strategy_context.execute_strategy(audit.uuid, - request_context) + solution = self.strategy_context.execute_strategy( + audit, request_context) return solution diff --git a/watcher/decision_engine/planner/default.py b/watcher/decision_engine/planner/default.py index a645f710b..7662d61be 100644 --- a/watcher/decision_engine/planner/default.py +++ b/watcher/decision_engine/planner/default.py @@ -48,10 +48,11 @@ class DefaultPlanner(base.BasePlanner): @classmethod def get_config_opts(cls): - return [cfg.DictOpt( - 'weights', - help="These weights are used to schedule the actions", - default=cls.weights_dict), + return [ + cfg.DictOpt( + 'weights', + help="These weights are used to schedule the actions", + default=cls.weights_dict), ] def create_action(self, @@ -113,9 +114,13 @@ class DefaultPlanner(base.BasePlanner): return action_plan def _create_action_plan(self, context, audit_id, solution): + strategy = objects.Strategy.get_by_name( + context, solution.strategy.name) + action_plan_dict = { 'uuid': utils.generate_uuid(), 'audit_id': audit_id, + 'strategy_id': strategy.id, 'first_action_id': None, 'state': objects.action_plan.State.RECOMMENDED, 'global_efficacy': solution.global_efficacy, diff --git a/watcher/decision_engine/strategy/context/base.py b/watcher/decision_engine/strategy/context/base.py index 28f8aec11..ca89d85d0 100644 --- a/watcher/decision_engine/strategy/context/base.py +++ b/watcher/decision_engine/strategy/context/base.py @@ -22,6 +22,16 @@ import six @six.add_metaclass(abc.ABCMeta) class BaseStrategyContext(object): + @abc.abstractmethod - def execute_strategy(self, audit_uuid, request_context): + def execute_strategy(self, audit, request_context): + """Execute the strategy for the given an audit + + :param audit: Audit object + :type audit: :py:class:`~.objects.audit.Audit` instance + :param request_context: Current request context + :type request_context: :py:class:`~.RequestContext` instance + :returns: The computed solution + :rtype: :py:class:`~.BaseSolution` instance + """ raise NotImplementedError() diff --git a/watcher/decision_engine/strategy/context/default.py b/watcher/decision_engine/strategy/context/default.py index 4ac263a81..6ab2e6723 100644 --- a/watcher/decision_engine/strategy/context/default.py +++ b/watcher/decision_engine/strategy/context/default.py @@ -30,9 +30,7 @@ class DefaultStrategyContext(base.BaseStrategyContext): super(DefaultStrategyContext, self).__init__() LOG.debug("Initializing Strategy Context") - def execute_strategy(self, audit_uuid, request_context): - audit = objects.Audit.get_by_uuid(request_context, audit_uuid) - + def execute_strategy(self, audit, request_context): osc = clients.OpenStackClients() # todo(jed) retrieve in audit parameters (threshold,...) # todo(jed) create ActionPlan diff --git a/watcher/decision_engine/sync.py b/watcher/decision_engine/sync.py index 0f292b672..1bb6951c7 100644 --- a/watcher/decision_engine/sync.py +++ b/watcher/decision_engine/sync.py @@ -22,6 +22,8 @@ from watcher._i18n import _LI, _LW from watcher.common import context from watcher.decision_engine.loading import default from watcher import objects +from watcher.objects import action_plan as apobjects +from watcher.objects import audit as auditobjects LOG = log.getLogger(__name__) @@ -54,6 +56,8 @@ class Syncer(object): self.strategy_mapping = dict() self.stale_audit_templates_map = {} + self.stale_audits_map = {} + self.stale_action_plans_map = {} @property def available_goals(self): @@ -118,7 +122,7 @@ class Syncer(object): self.strategy_mapping.update(self._sync_strategy(strategy_map)) - self._sync_audit_templates() + self._sync_objects() def _sync_goal(self, goal_map): goal_name = goal_map.name @@ -177,25 +181,45 @@ class Syncer(object): return strategy_mapping - def _sync_audit_templates(self): - # First we find audit templates that are stale because their associated - # goal or strategy has been modified and we update them in-memory + def _sync_objects(self): + # First we find audit templates, audits and action plans that are stale + # because their associated goal or strategy has been modified and we + # update them in-memory self._find_stale_audit_templates_due_to_goal() self._find_stale_audit_templates_due_to_strategy() - # Then we handle the case where an audit template became - # stale because its related goal does not exist anymore. + self._find_stale_audits_due_to_goal() + self._find_stale_audits_due_to_strategy() + + self._find_stale_action_plans_due_to_strategy() + self._find_stale_action_plans_due_to_audit() + + # Then we handle the case where an audit template, an audit or an + # action plan becomes stale because its related goal does not + # exist anymore. self._soft_delete_removed_goals() - # Then we handle the case where an audit template became - # stale because its related strategy does not exist anymore. + # Then we handle the case where an audit template, an audit or an + # action plan becomes stale because its related strategy does not + # exist anymore. self._soft_delete_removed_strategies() # Finally, we save into the DB the updated stale audit templates + # and soft delete stale audits and action plans for stale_audit_template in self.stale_audit_templates_map.values(): stale_audit_template.save() LOG.info(_LI("Audit Template '%s' synced"), stale_audit_template.name) + for stale_audit in self.stale_audits_map.values(): + stale_audit.save() + LOG.info(_LI("Stale audit '%s' synced and cancelled"), + stale_audit.uuid) + + for stale_action_plan in self.stale_action_plans_map.values(): + stale_action_plan.save() + LOG.info(_LI("Stale action plan '%s' synced and cancelled"), + stale_action_plan.uuid) + def _find_stale_audit_templates_due_to_goal(self): for goal_id, synced_goal in self.goal_mapping.items(): filters = {"goal_id": goal_id} @@ -228,6 +252,72 @@ class Syncer(object): self.stale_audit_templates_map[ audit_template.id].strategy_id = synced_strategy.id + def _find_stale_audits_due_to_goal(self): + for goal_id, synced_goal in self.goal_mapping.items(): + filters = {"goal_id": goal_id} + stale_audits = objects.Audit.list( + self.ctx, filters=filters) + + # Update the goal ID for the stale audits (w/o saving) + for audit in stale_audits: + if audit.id not in self.stale_audits_map: + audit.goal_id = synced_goal.id + self.stale_audits_map[audit.id] = audit + else: + self.stale_audits_map[audit.id].goal_id = synced_goal.id + + def _find_stale_audits_due_to_strategy(self): + for strategy_id, synced_strategy in self.strategy_mapping.items(): + filters = {"strategy_id": strategy_id} + stale_audits = objects.Audit.list(self.ctx, filters=filters) + # Update strategy IDs for all stale audits (w/o saving) + for audit in stale_audits: + if audit.id not in self.stale_audits_map: + audit.strategy_id = synced_strategy.id + audit.state = auditobjects.State.CANCELLED + self.stale_audits_map[audit.id] = audit + else: + self.stale_audits_map[ + audit.id].strategy_id = synced_strategy.id + self.stale_audits_map[ + audit.id].state = auditobjects.State.CANCELLED + + def _find_stale_action_plans_due_to_strategy(self): + for strategy_id, synced_strategy in self.strategy_mapping.items(): + filters = {"strategy_id": strategy_id} + stale_action_plans = objects.ActionPlan.list( + self.ctx, filters=filters) + + # Update strategy IDs for all stale action plans (w/o saving) + for action_plan in stale_action_plans: + if action_plan.id not in self.stale_action_plans_map: + action_plan.strategy_id = synced_strategy.id + action_plan.state = apobjects.State.CANCELLED + self.stale_action_plans_map[action_plan.id] = action_plan + else: + self.stale_action_plans_map[ + action_plan.id].strategy_id = synced_strategy.id + self.stale_action_plans_map[ + action_plan.id].state = apobjects.State.CANCELLED + + def _find_stale_action_plans_due_to_audit(self): + for audit_id, synced_audit in self.stale_audits_map.items(): + filters = {"audit_id": audit_id} + stale_action_plans = objects.ActionPlan.list( + self.ctx, filters=filters) + + # Update audit IDs for all stale action plans (w/o saving) + for action_plan in stale_action_plans: + if action_plan.id not in self.stale_action_plans_map: + action_plan.audit_id = synced_audit.id + action_plan.state = apobjects.State.CANCELLED + self.stale_action_plans_map[action_plan.id] = action_plan + else: + self.stale_action_plans_map[ + action_plan.id].audit_id = synced_audit.id + self.stale_action_plans_map[ + action_plan.id].state = apobjects.State.CANCELLED + def _soft_delete_removed_goals(self): removed_goals = [ g for g in self.available_goals @@ -235,12 +325,24 @@ class Syncer(object): for removed_goal in removed_goals: removed_goal.soft_delete() filters = {"goal_id": removed_goal.id} + invalid_ats = objects.AuditTemplate.list(self.ctx, filters=filters) for at in invalid_ats: LOG.warning( _LW("Audit Template '%(audit_template)s' references a " - "goal that does not exist"), - audit_template=at.uuid) + "goal that does not exist"), audit_template=at.uuid) + + stale_audits = objects.Audit.list(self.ctx, filters=filters) + for audit in stale_audits: + LOG.warning( + _LW("Audit '%(audit)s' references a " + "goal that does not exist"), audit=audit.uuid) + if audit.id not in self.stale_audits_map: + audit.state = auditobjects.State.CANCELLED + self.stale_audits_map[audit.id] = audit + else: + self.stale_audits_map[ + audit.id].state = auditobjects.State.CANCELLED def _soft_delete_removed_strategies(self): removed_strategies = [ @@ -265,6 +367,32 @@ class Syncer(object): else: self.stale_audit_templates_map[at.id].strategy_id = None + stale_audits = objects.Audit.list(self.ctx, filters=filters) + for audit in stale_audits: + LOG.warning( + _LW("Audit '%(audit)s' references a " + "strategy that does not exist"), audit=audit.uuid) + if audit.id not in self.stale_audits_map: + audit.state = auditobjects.State.CANCELLED + self.stale_audits_map[audit.id] = audit + else: + self.stale_audits_map[ + audit.id].state = auditobjects.State.CANCELLED + + stale_action_plans = objects.ActionPlan.list( + self.ctx, filters=filters) + for action_plan in stale_action_plans: + LOG.warning( + _LW("Action Plan '%(action_plan)s' references a " + "strategy that does not exist"), + action_plan=action_plan.uuid) + if action_plan.id not in self.stale_action_plans_map: + action_plan.state = apobjects.State.CANCELLED + self.stale_action_plans_map[action_plan.id] = action_plan + else: + self.stale_action_plans_map[ + action_plan.id].state = apobjects.State.CANCELLED + def _discover(self): strategies_map = {} goals_map = {} diff --git a/watcher/objects/action_plan.py b/watcher/objects/action_plan.py index 52aece120..f6f63d984 100644 --- a/watcher/objects/action_plan.py +++ b/watcher/objects/action_plan.py @@ -97,6 +97,7 @@ class ActionPlan(base.WatcherObject): 'id': int, 'uuid': obj_utils.str_or_none, 'audit_id': obj_utils.int_or_none, + 'strategy_id': obj_utils.int_or_none, 'first_action_id': obj_utils.int_or_none, 'state': obj_utils.str_or_none, 'global_efficacy': obj_utils.dict_or_none, @@ -253,7 +254,7 @@ class ActionPlan(base.WatcherObject): self[field] = current[field] def soft_delete(self, context=None): - """soft Delete the Action plan from the DB. + """Soft Delete the Action plan from the DB. :param context: Security context. NOTE: This should only be used internally by the indirection_api. diff --git a/watcher/tests/api/v1/test_actions_plans.py b/watcher/tests/api/v1/test_actions_plans.py index ff3e694a4..7c492bfcc 100644 --- a/watcher/tests/api/v1/test_actions_plans.py +++ b/watcher/tests/api/v1/test_actions_plans.py @@ -13,39 +13,18 @@ import datetime import itertools import mock -import pecan from oslo_config import cfg from oslo_serialization import jsonutils -from wsme import types as wtypes -from watcher.api.controllers.v1 import action_plan as api_action_plan from watcher.applier import rpcapi as aapi -from watcher.common import context from watcher.common import utils from watcher.db import api as db_api from watcher import objects from watcher.tests.api import base as api_base -from watcher.tests.api import utils as api_utils -from watcher.tests import base from watcher.tests.objects import utils as obj_utils -class TestActionPlanObject(base.TestCase): - - @mock.patch.object(objects.EfficacyIndicator, - 'list', mock.Mock(return_value=[])) - @mock.patch.object(pecan, 'request') - def test_action_plan_init(self, m_request): - m_request.context = context.make_context() - act_plan_dict = api_utils.action_plan_post_data() - del act_plan_dict['state'] - del act_plan_dict['audit_id'] - del act_plan_dict['first_action_id'] - act_plan = api_action_plan.ActionPlan(**act_plan_dict) - self.assertEqual(wtypes.Unset, act_plan.state) - - class TestListActionPlan(api_base.FunctionalTest): def test_empty(self): @@ -53,20 +32,21 @@ class TestListActionPlan(api_base.FunctionalTest): self.assertEqual([], response['action_plans']) def _assert_action_plans_fields(self, action_plan): - action_plan_fields = ['uuid', 'audit_uuid', 'state', 'global_efficacy', - 'efficacy_indicators'] + action_plan_fields = [ + 'uuid', 'audit_uuid', 'strategy_uuid', 'strategy_name', + 'state', 'global_efficacy', 'efficacy_indicators'] for field in action_plan_fields: self.assertIn(field, action_plan) def test_one(self): - action_plan = obj_utils.create_action_plan_without_audit(self.context) + action_plan = obj_utils.create_test_action_plan(self.context) response = self.get_json('/action_plans') self.assertEqual(action_plan.uuid, response['action_plans'][0]["uuid"]) self._assert_action_plans_fields(response['action_plans'][0]) def test_one_soft_deleted(self): - action_plan = obj_utils.create_action_plan_without_audit(self.context) + action_plan = obj_utils.create_test_action_plan(self.context) action_plan.soft_delete() response = self.get_json('/action_plans', headers={'X-Show-Deleted': 'True'}) @@ -100,7 +80,7 @@ class TestListActionPlan(api_base.FunctionalTest): self._assert_action_plans_fields(response) def test_get_one_soft_deleted(self): - action_plan = obj_utils.create_action_plan_without_audit(self.context) + action_plan = obj_utils.create_test_action_plan(self.context) action_plan.soft_delete() response = self.get_json('/action_plans/%s' % action_plan['uuid'], headers={'X-Show-Deleted': 'True'}) @@ -112,15 +92,14 @@ class TestListActionPlan(api_base.FunctionalTest): self.assertEqual(404, response.status_int) def test_detail(self): - action_plan = obj_utils.create_test_action_plan(self.context, - audit_id=None) + action_plan = obj_utils.create_test_action_plan(self.context) response = self.get_json('/action_plans/detail') self.assertEqual(action_plan.uuid, response['action_plans'][0]["uuid"]) self._assert_action_plans_fields(response['action_plans'][0]) def test_detail_soft_deleted(self): - action_plan = obj_utils.create_action_plan_without_audit(self.context) + action_plan = obj_utils.create_test_action_plan(self.context) action_plan.soft_delete() response = self.get_json('/action_plans/detail', headers={'X-Show-Deleted': 'True'}) @@ -141,7 +120,7 @@ class TestListActionPlan(api_base.FunctionalTest): def test_many(self): action_plan_list = [] for id_ in range(5): - action_plan = obj_utils.create_action_plan_without_audit( + action_plan = obj_utils.create_test_action_plan( self.context, id=id_, uuid=utils.generate_uuid()) action_plan_list.append(action_plan.uuid) response = self.get_json('/action_plans') @@ -225,7 +204,7 @@ class TestListActionPlan(api_base.FunctionalTest): def test_many_without_soft_deleted(self): action_plan_list = [] for id_ in [1, 2, 3]: - action_plan = obj_utils.create_action_plan_without_audit( + action_plan = obj_utils.create_test_action_plan( self.context, id=id_, uuid=utils.generate_uuid()) action_plan_list.append(action_plan.uuid) for id_ in [4, 5]: @@ -240,11 +219,11 @@ class TestListActionPlan(api_base.FunctionalTest): def test_many_with_soft_deleted(self): action_plan_list = [] for id_ in [1, 2, 3]: - action_plan = obj_utils.create_action_plan_without_audit( + action_plan = obj_utils.create_test_action_plan( self.context, id=id_, uuid=utils.generate_uuid()) action_plan_list.append(action_plan.uuid) for id_ in [4, 5]: - action_plan = obj_utils.create_action_plan_without_audit( + action_plan = obj_utils.create_test_action_plan( self.context, id=id_, uuid=utils.generate_uuid()) action_plan.soft_delete() action_plan_list.append(action_plan.uuid) @@ -272,8 +251,7 @@ class TestListActionPlan(api_base.FunctionalTest): def test_links(self): uuid = utils.generate_uuid() - obj_utils.create_action_plan_without_audit(self.context, - id=1, uuid=uuid) + obj_utils.create_test_action_plan(self.context, id=1, uuid=uuid) response = self.get_json('/action_plans/%s' % uuid) self.assertIn('links', response.keys()) self.assertEqual(2, len(response['links'])) @@ -284,7 +262,7 @@ class TestListActionPlan(api_base.FunctionalTest): def test_collection_links(self): for id_ in range(5): - obj_utils.create_action_plan_without_audit( + obj_utils.create_test_action_plan( self.context, id=id_, uuid=utils.generate_uuid()) response = self.get_json('/action_plans/?limit=3') self.assertEqual(3, len(response['action_plans'])) @@ -296,9 +274,8 @@ class TestListActionPlan(api_base.FunctionalTest): cfg.CONF.set_override('max_limit', 3, 'api', enforce_type=True) for id_ in range(5): - obj_utils.create_action_plan_without_audit( - self.context, id=id_, uuid=utils.generate_uuid(), - audit_id=None) + obj_utils.create_test_action_plan( + self.context, id=id_, uuid=utils.generate_uuid()) response = self.get_json('/action_plans') self.assertEqual(3, len(response['action_plans'])) @@ -310,7 +287,7 @@ class TestDelete(api_base.FunctionalTest): def setUp(self): super(TestDelete, self).setUp() - self.action_plan = obj_utils.create_action_plan_without_audit( + self.action_plan = obj_utils.create_test_action_plan( self.context) p = mock.patch.object(db_api.BaseConnection, 'destroy_action_plan') self.mock_action_plan_delete = p.start() @@ -366,7 +343,7 @@ class TestPatch(api_base.FunctionalTest): def setUp(self): super(TestPatch, self).setUp() - self.action_plan = obj_utils.create_action_plan_without_audit( + self.action_plan = obj_utils.create_test_action_plan( self.context, state=objects.action_plan.State.RECOMMENDED) p = mock.patch.object(db_api.BaseConnection, 'update_action_plan') self.mock_action_plan_update = p.start() @@ -459,7 +436,7 @@ class TestPatch(api_base.FunctionalTest): response = self.patch_json( '/action_plans/%s' % self.action_plan.uuid, [{'path': '/state', 'value': new_state, - 'op': 'replace'}]) + 'op': 'replace'}]) self.assertEqual('application/json', response.content_type) self.assertEqual(200, response.status_code) applier_mock.assert_called_once_with(mock.ANY, @@ -509,7 +486,7 @@ class TestPatchStateTransitionDenied(api_base.FunctionalTest): db_api.BaseConnection, 'update_action_plan', mock.Mock(side_effect=lambda ap: ap.save() or ap)) def test_replace_state_pending_denied(self): - action_plan = obj_utils.create_action_plan_without_audit( + action_plan = obj_utils.create_test_action_plan( self.context, state=self.original_state) initial_ap = self.get_json('/action_plans/%s' % action_plan.uuid) @@ -533,7 +510,7 @@ class TestPatchStateDeletedNotFound(api_base.FunctionalTest): db_api.BaseConnection, 'update_action_plan', mock.Mock(side_effect=lambda ap: ap.save() or ap)) def test_replace_state_pending_not_found(self): - action_plan = obj_utils.create_action_plan_without_audit( + action_plan = obj_utils.create_test_action_plan( self.context, state=objects.action_plan.State.DELETED) response = self.get_json( @@ -561,15 +538,14 @@ class TestPatchStateTransitionOk(api_base.FunctionalTest): mock.Mock(side_effect=lambda ap: ap.save() or ap)) @mock.patch.object(aapi.ApplierAPI, 'launch_action_plan', mock.Mock()) def test_replace_state_pending_ok(self): - action_plan = obj_utils.create_action_plan_without_audit( + action_plan = obj_utils.create_test_action_plan( self.context, state=self.original_state) initial_ap = self.get_json('/action_plans/%s' % action_plan.uuid) response = self.patch_json( '/action_plans/%s' % action_plan.uuid, - [{'path': '/state', 'value': self.new_state, - 'op': 'replace'}]) + [{'path': '/state', 'value': self.new_state, 'op': 'replace'}]) updated_ap = self.get_json('/action_plans/%s' % action_plan.uuid) self.assertNotEqual(self.new_state, initial_ap['state']) diff --git a/watcher/tests/db/test_purge.py b/watcher/tests/db/test_purge.py index f94b2668e..dfa128198 100644 --- a/watcher/tests/db/test_purge.py +++ b/watcher/tests/db/test_purge.py @@ -155,14 +155,14 @@ class TestPurgeCommand(base.DbTestCase): with freezegun.freeze_time(self.expired_date): self.action_plan1 = obj_utils.create_test_action_plan( - self.context, audit_id=self.audit1.id, - id=self._generate_id(), uuid=None) + self.context, id=self._generate_id(), uuid=None, + audit_id=self.audit1.id, strategy_id=self.strategy1.id) self.action_plan2 = obj_utils.create_test_action_plan( - self.context, audit_id=self.audit2.id, - id=self._generate_id(), uuid=None) + self.context, id=self._generate_id(), uuid=None, + audit_id=self.audit2.id, strategy_id=self.strategy2.id) self.action_plan3 = obj_utils.create_test_action_plan( - self.context, audit_id=self.audit3.id, - id=self._generate_id(), uuid=None) + self.context, id=self._generate_id(), uuid=None, + audit_id=self.audit3.id, strategy_id=self.strategy3.id) self.action1 = obj_utils.create_test_action( self.context, action_plan_id=self.action_plan1.id, diff --git a/watcher/tests/db/utils.py b/watcher/tests/db/utils.py index a3b20a249..f78c4cee5 100644 --- a/watcher/tests/db/utils.py +++ b/watcher/tests/db/utils.py @@ -124,6 +124,7 @@ def get_test_action_plan(**kwargs): 'uuid': kwargs.get('uuid', '76be87bd-3422-43f9-93a0-e85a577e3061'), 'state': kwargs.get('state', 'ONGOING'), 'audit_id': kwargs.get('audit_id', 1), + 'strategy_id': kwargs.get('strategy_id', 1), 'global_efficacy': kwargs.get('global_efficacy', {}), 'first_action_id': kwargs.get('first_action_id', 1), 'created_at': kwargs.get('created_at'), diff --git a/watcher/tests/decision_engine/audit/test_audit_handlers.py b/watcher/tests/decision_engine/audit/test_audit_handlers.py index 4a7c5c248..98c693b10 100644 --- a/watcher/tests/decision_engine/audit/test_audit_handlers.py +++ b/watcher/tests/decision_engine/audit/test_audit_handlers.py @@ -13,10 +13,11 @@ # implied. # See the License for the specific language governing permissions and # limitations under the License. -import mock + import uuid from apscheduler.schedulers import background +import mock from watcher.decision_engine.audit import continuous from watcher.decision_engine.audit import oneshot @@ -30,13 +31,17 @@ from watcher.tests.objects import utils as obj_utils class TestOneShotAuditHandler(base.DbTestCase): + def setUp(self): super(TestOneShotAuditHandler, self).setUp() obj_utils.create_test_goal(self.context, id=1, name="dummy") + self.strategy = obj_utils.create_test_strategy( + self.context, name='dummy') audit_template = obj_utils.create_test_audit_template( - self.context) + self.context, strategy_id=self.strategy.id) self.audit = obj_utils.create_test_audit( self.context, + strategy_id=self.strategy.id, audit_template_id=audit_template.id) @mock.patch.object(manager.CollectorManager, "get_cluster_model_collector") @@ -79,11 +84,12 @@ class TestContinuousAuditHandler(base.DbTestCase): obj_utils.create_test_goal(self.context, id=1, name="DUMMY") audit_template = obj_utils.create_test_audit_template( self.context) - self.audits = [obj_utils.create_test_audit( - self.context, - uuid=uuid.uuid4(), - audit_template_id=audit_template.id, - audit_type=audit_objects.AuditType.CONTINUOUS.value) + self.audits = [ + obj_utils.create_test_audit( + self.context, + uuid=uuid.uuid4(), + audit_template_id=audit_template.id, + audit_type=audit_objects.AuditType.CONTINUOUS.value) for i in range(2)] @mock.patch.object(background.BackgroundScheduler, 'add_job') diff --git a/watcher/tests/decision_engine/fake_goals.py b/watcher/tests/decision_engine/fake_goals.py index 3e5618b07..435253daf 100644 --- a/watcher/tests/decision_engine/fake_goals.py +++ b/watcher/tests/decision_engine/fake_goals.py @@ -81,8 +81,3 @@ class FakeDummy1(FakeGoal): class FakeDummy2(FakeGoal): NAME = "dummy_2" DISPLAY_NAME = "Dummy 2" - - -class FakeOtherDummy2(FakeGoal): - NAME = "dummy_2" - DISPLAY_NAME = "Other Dummy 2" diff --git a/watcher/tests/decision_engine/planner/test_default_planner.py b/watcher/tests/decision_engine/planner/test_default_planner.py index 341c33b3c..effd5269d 100644 --- a/watcher/tests/decision_engine/planner/test_default_planner.py +++ b/watcher/tests/decision_engine/planner/test_default_planner.py @@ -59,11 +59,16 @@ class SolutionFakerSingleHyp(object): class TestActionScheduling(base.DbTestCase): + def setUp(self): + super(TestActionScheduling, self).setUp() + self.strategy = db_utils.create_test_strategy(name="dummy") + self.audit = db_utils.create_test_audit( + uuid=utils.generate_uuid(), strategy_id=self.strategy.id) + self.default_planner = pbase.DefaultPlanner(mock.Mock()) + def test_schedule_actions(self): - default_planner = pbase.DefaultPlanner(mock.Mock()) - audit = db_utils.create_test_audit(uuid=utils.generate_uuid()) solution = dsol.DefaultSolution( - goal=mock.Mock(), strategy=mock.Mock()) + goal=mock.Mock(), strategy=self.strategy) parameters = { "source_node": "server1", @@ -74,11 +79,12 @@ class TestActionScheduling(base.DbTestCase): input_parameters=parameters) with mock.patch.object( - pbase.DefaultPlanner, "create_action", - wraps=default_planner.create_action) as m_create_action: - default_planner.config.weights = {'migrate': 3} - action_plan = default_planner.schedule(self.context, - audit.id, solution) + pbase.DefaultPlanner, "create_action", + wraps=self.default_planner.create_action + ) as m_create_action: + self.default_planner.config.weights = {'migrate': 3} + action_plan = self.default_planner.schedule( + self.context, self.audit.id, solution) self.assertIsNotNone(action_plan.uuid) self.assertEqual(1, m_create_action.call_count) @@ -87,10 +93,8 @@ class TestActionScheduling(base.DbTestCase): self.assertEqual("migrate", actions[0].action_type) def test_schedule_two_actions(self): - default_planner = pbase.DefaultPlanner(mock.Mock()) - audit = db_utils.create_test_audit(uuid=utils.generate_uuid()) solution = dsol.DefaultSolution( - goal=mock.Mock(), strategy=mock.Mock()) + goal=mock.Mock(), strategy=self.strategy) parameters = { "source_node": "server1", @@ -105,11 +109,12 @@ class TestActionScheduling(base.DbTestCase): input_parameters={}) with mock.patch.object( - pbase.DefaultPlanner, "create_action", - wraps=default_planner.create_action) as m_create_action: - default_planner.config.weights = {'migrate': 3, 'nop': 0} - action_plan = default_planner.schedule(self.context, - audit.id, solution) + pbase.DefaultPlanner, "create_action", + wraps=self.default_planner.create_action + ) as m_create_action: + self.default_planner.config.weights = {'migrate': 3, 'nop': 0} + action_plan = self.default_planner.schedule( + self.context, self.audit.id, solution) self.assertIsNotNone(action_plan.uuid) self.assertEqual(2, m_create_action.call_count) # check order @@ -119,10 +124,8 @@ class TestActionScheduling(base.DbTestCase): self.assertEqual("migrate", actions[1].action_type) def test_schedule_actions_with_unknown_action(self): - default_planner = pbase.DefaultPlanner(mock.Mock()) - audit = db_utils.create_test_audit(uuid=utils.generate_uuid()) solution = dsol.DefaultSolution( - goal=mock.Mock(), strategy=mock.Mock()) + goal=mock.Mock(), strategy=self.strategy) parameters = { "src_uuid_node": "server1", @@ -137,11 +140,12 @@ class TestActionScheduling(base.DbTestCase): input_parameters={}) with mock.patch.object( - pbase.DefaultPlanner, "create_action", - wraps=default_planner.create_action) as m_create_action: - default_planner.config.weights = {'migrate': 0} - self.assertRaises(KeyError, default_planner.schedule, - self.context, audit.id, solution) + pbase.DefaultPlanner, "create_action", + wraps=self.default_planner.create_action + ) as m_create_action: + self.default_planner.config.weights = {'migrate': 0} + self.assertRaises(KeyError, self.default_planner.schedule, + self.context, self.audit.id, solution) self.assertEqual(2, m_create_action.call_count) @@ -158,6 +162,7 @@ class TestDefaultPlanner(base.DbTestCase): } obj_utils.create_test_audit_template(self.context) + self.strategy = obj_utils.create_test_strategy(self.context) p = mock.patch.object(db_api.BaseConnection, 'create_action_plan') self.mock_create_action_plan = p.start() @@ -179,14 +184,18 @@ class TestDefaultPlanner(base.DbTestCase): action.create() return action - def test_schedule_scheduled_empty(self): + @mock.patch.object(objects.Strategy, 'get_by_name') + def test_schedule_scheduled_empty(self, m_get_by_name): + m_get_by_name.return_value = self.strategy audit = db_utils.create_test_audit(uuid=utils.generate_uuid()) fake_solution = SolutionFakerSingleHyp.build() action_plan = self.default_planner.schedule(self.context, audit.id, fake_solution) self.assertIsNotNone(action_plan.uuid) - def test_scheduler_warning_empty_action_plan(self): + @mock.patch.object(objects.Strategy, 'get_by_name') + def test_scheduler_warning_empty_action_plan(self, m_get_by_name): + m_get_by_name.return_value = self.strategy audit = db_utils.create_test_audit(uuid=utils.generate_uuid()) fake_solution = SolutionFaker.build() action_plan = self.default_planner.schedule(self.context, diff --git a/watcher/tests/decision_engine/strategy/context/test_strategy_context.py b/watcher/tests/decision_engine/strategy/context/test_strategy_context.py index 65d5f8783..3f376042c 100644 --- a/watcher/tests/decision_engine/strategy/context/test_strategy_context.py +++ b/watcher/tests/decision_engine/strategy/context/test_strategy_context.py @@ -44,7 +44,7 @@ class TestStrategyContext(base.DbTestCase): mock_call.return_value = strategies.DummyStrategy( config=mock.Mock()) solution = self.strategy_context.execute_strategy( - self.audit.uuid, self.context) + self.audit, self.context) self.assertIsInstance(solution, default.DefaultSolution) @mock.patch.object(manager.CollectorManager, "get_cluster_model_collector", @@ -65,8 +65,7 @@ class TestStrategyContext(base.DbTestCase): uuid=utils.generate_uuid(), ) - solution = self.strategy_context.execute_strategy( - audit.uuid, self.context) + solution = self.strategy_context.execute_strategy(audit, self.context) self.assertEqual(len(solution.actions), 3) @@ -92,7 +91,6 @@ class TestStrategyContext(base.DbTestCase): uuid=utils.generate_uuid(), ) - solution = self.strategy_context.execute_strategy( - audit.uuid, self.context) + solution = self.strategy_context.execute_strategy(audit, self.context) self.assertEqual(solution, expected_strategy) diff --git a/watcher/tests/decision_engine/test_sync.py b/watcher/tests/decision_engine/test_sync.py index 3f5a0cbe2..704fd7205 100644 --- a/watcher/tests/decision_engine/test_sync.py +++ b/watcher/tests/decision_engine/test_sync.py @@ -21,6 +21,7 @@ from watcher.common import utils from watcher.decision_engine.loading import default from watcher.decision_engine import sync from watcher import objects +from watcher.objects import action_plan as ap_objects from watcher.tests.db import base from watcher.tests.decision_engine import fake_goals from watcher.tests.decision_engine import fake_strategies @@ -73,6 +74,27 @@ class TestSyncer(base.DbTestCase): self.addCleanup(p_goals_load.stop) self.addCleanup(p_strategies.stop) + @staticmethod + def _find_created_modified_unmodified_ids(befores, afters): + created = { + a_item.id: a_item for a_item in afters + if a_item.uuid not in (b_item.uuid for b_item in befores) + } + + modified = { + a_item.id: a_item for a_item in afters + if a_item.as_dict() not in ( + b_items.as_dict() for b_items in befores) + } + + unmodified = { + a_item.id: a_item for a_item in afters + if a_item.as_dict() in ( + b_items.as_dict() for b_items in befores) + } + + return created, modified, unmodified + @mock.patch.object(objects.Strategy, "soft_delete") @mock.patch.object(objects.Strategy, "save") @mock.patch.object(objects.Strategy, "create") @@ -257,15 +279,18 @@ class TestSyncer(base.DbTestCase): strategy1 = objects.Strategy( self.ctx, id=1, name="strategy_1", uuid=utils.generate_uuid(), display_name="Strategy 1", goal_id=goal1.id) - # Should stay unmodified after sync() + # Should be modified after sync() because its related goal has been + # modified strategy2 = objects.Strategy( self.ctx, id=2, name="strategy_2", uuid=utils.generate_uuid(), display_name="Strategy 2", goal_id=goal2.id) - # Should be modified by the sync() + # Should be modified after sync() because its strategy name has been + # modified strategy3 = objects.Strategy( self.ctx, id=3, name="strategy_3", uuid=utils.generate_uuid(), - display_name="Original", goal_id=goal2.id) - # Should be modified by the sync() + display_name="Original", goal_id=goal1.id) + # Should be modified after sync() because both its related goal + # and its strategy name have been modified strategy4 = objects.Strategy( self.ctx, id=4, name="strategy_4", uuid=utils.generate_uuid(), display_name="Original", goal_id=goal2.id) @@ -279,18 +304,18 @@ class TestSyncer(base.DbTestCase): # Should stay unmodified after sync() audit_template1 = objects.AuditTemplate( - self.ctx, id=1, uuid=utils.generate_uuid(), - name="Synced AT1", goal_id=goal1.id, strategy_id=strategy1.id) + self.ctx, id=1, name="Synced AT1", uuid=utils.generate_uuid(), + goal_id=goal1.id, strategy_id=strategy1.id) # Should be modified by the sync() because its associated goal - # should be modified + # has been modified (compared to the defined fake goals) audit_template2 = objects.AuditTemplate( self.ctx, id=2, name="Synced AT2", uuid=utils.generate_uuid(), goal_id=goal2.id, strategy_id=strategy2.id) # Should be modified by the sync() because its associated strategy - # should be modified + # has been modified (compared to the defined fake strategies) audit_template3 = objects.AuditTemplate( self.ctx, id=3, name="Synced AT3", uuid=utils.generate_uuid(), - goal_id=goal2.id, strategy_id=strategy3.id) + goal_id=goal1.id, strategy_id=strategy3.id) # Modified because of both because its associated goal and associated # strategy should be modified audit_template4 = objects.AuditTemplate( @@ -301,9 +326,70 @@ class TestSyncer(base.DbTestCase): audit_template3.create() audit_template4.create() - before_audit_templates = objects.AuditTemplate.list(self.ctx) + # Should stay unmodified after sync() + audit1 = objects.Audit( + self.ctx, id=1, uuid=utils.generate_uuid(), + goal_id=goal1.id, strategy_id=strategy1.id) + # Should be modified by the sync() because its associated goal + # has been modified (compared to the defined fake goals) + audit2 = objects.Audit( + self.ctx, id=2, uuid=utils.generate_uuid(), + goal_id=goal2.id, strategy_id=strategy2.id) + # Should be modified by the sync() because its associated strategy + # has been modified (compared to the defined fake strategies) + audit3 = objects.Audit( + self.ctx, id=3, uuid=utils.generate_uuid(), + goal_id=goal1.id, strategy_id=strategy3.id) + # Modified because of both because its associated goal and associated + # strategy should be modified (compared to the defined fake + # goals/strategies) + audit4 = objects.Audit( + self.ctx, id=4, uuid=utils.generate_uuid(), + goal_id=goal2.id, strategy_id=strategy4.id) + + audit1.create() + audit2.create() + audit3.create() + audit4.create() + + # Should stay unmodified after sync() + action_plan1 = objects.ActionPlan( + self.ctx, id=1, uuid=utils.generate_uuid(), + audit_id=audit1.id, strategy_id=strategy1.id, + first_action_id=None, state='DOESNOTMATTER', + global_efficacy={}) + # Stale after syncing because the goal of the audit has been modified + # (compared to the defined fake goals) + action_plan2 = objects.ActionPlan( + self.ctx, id=2, uuid=utils.generate_uuid(), + audit_id=audit2.id, strategy_id=strategy2.id, + first_action_id=None, state='DOESNOTMATTER', + global_efficacy={}) + # Stale after syncing because the strategy has been modified + # (compared to the defined fake strategies) + action_plan3 = objects.ActionPlan( + self.ctx, id=3, uuid=utils.generate_uuid(), + audit_id=audit3.id, strategy_id=strategy3.id, + first_action_id=None, state='DOESNOTMATTER', + global_efficacy={}) + # Stale after syncing because both the strategy and the related audit + # have been modified (compared to the defined fake goals/strategies) + action_plan4 = objects.ActionPlan( + self.ctx, id=4, uuid=utils.generate_uuid(), + audit_id=audit4.id, strategy_id=strategy4.id, + first_action_id=None, state='DOESNOTMATTER', + global_efficacy={}) + + action_plan1.create() + action_plan2.create() + action_plan3.create() + action_plan4.create() + before_goals = objects.Goal.list(self.ctx) before_strategies = objects.Strategy.list(self.ctx) + before_audit_templates = objects.AuditTemplate.list(self.ctx) + before_audits = objects.Audit.list(self.ctx) + before_action_plans = objects.ActionPlan.list(self.ctx) # ### Action under test ### # @@ -314,30 +400,51 @@ class TestSyncer(base.DbTestCase): # ### Assertions ### # - after_audit_templates = objects.AuditTemplate.list(self.ctx) after_goals = objects.Goal.list(self.ctx) after_strategies = objects.Strategy.list(self.ctx) + after_audit_templates = objects.AuditTemplate.list(self.ctx) + after_audits = objects.Audit.list(self.ctx) + after_action_plans = objects.ActionPlan.list(self.ctx) self.assertEqual(2, len(before_goals)) self.assertEqual(4, len(before_strategies)) self.assertEqual(4, len(before_audit_templates)) + self.assertEqual(4, len(before_audits)) + self.assertEqual(4, len(before_action_plans)) self.assertEqual(2, len(after_goals)) self.assertEqual(4, len(after_strategies)) self.assertEqual(4, len(after_audit_templates)) + self.assertEqual(4, len(after_audits)) + self.assertEqual(4, len(after_action_plans)) + self.assertEqual( {"dummy_1", "dummy_2"}, set([g.name for g in after_goals])) self.assertEqual( {"strategy_1", "strategy_2", "strategy_3", "strategy_4"}, set([s.name for s in after_strategies])) - created_goals = { - ag.name: ag for ag in after_goals - if ag.uuid not in [bg.uuid for bg in before_goals] - } - created_strategies = { - a_s.name: a_s for a_s in after_strategies - if a_s.uuid not in [b_s.uuid for b_s in before_strategies] - } + + created_goals, modified_goals, unmodified_goals = ( + self._find_created_modified_unmodified_ids( + before_goals, after_goals)) + + created_strategies, modified_strategies, unmodified_strategies = ( + self._find_created_modified_unmodified_ids( + before_strategies, after_strategies)) + + (created_audit_templates, modified_audit_templates, + unmodified_audit_templates) = ( + self._find_created_modified_unmodified_ids( + before_audit_templates, after_audit_templates)) + + created_audits, modified_audits, unmodified_audits = ( + self._find_created_modified_unmodified_ids( + before_audits, after_audits)) + + (created_action_plans, modified_action_plans, + unmodified_action_plans) = ( + self._find_created_modified_unmodified_ids( + before_action_plans, after_action_plans)) dummy_1_spec = [ {'description': 'Dummy indicator', 'name': 'dummy', @@ -351,40 +458,34 @@ class TestSyncer(base.DbTestCase): self.assertEqual(1, len(created_goals)) self.assertEqual(3, len(created_strategies)) - - modified_audit_templates = { - a_at.id for a_at in after_audit_templates - if a_at.goal_id not in ( - # initial goal IDs - b_at.goal_id for b_at in before_audit_templates) or - a_at.strategy_id not in ( - # initial strategy IDs - b_at.strategy_id for b_at in before_audit_templates - if b_at.strategy_id is not None) - } - - unmodified_audit_templates = { - a_at.id for a_at in after_audit_templates - if a_at.goal_id in ( - # initial goal IDs - b_at.goal_id for b_at in before_audit_templates) and - a_at.strategy_id in ( - # initial strategy IDs - b_at.strategy_id for b_at in before_audit_templates - if b_at.strategy_id is not None) - } + self.assertEqual(0, len(created_audits)) + self.assertEqual(0, len(created_action_plans)) self.assertEqual(2, strategy2.goal_id) - self.assertIn(strategy2.name, created_strategies) - self.assertNotEqual(strategy2.id, - created_strategies[strategy2.name].id) - self.assertEqual(set([audit_template2.id, - audit_template3.id, - audit_template4.id]), - modified_audit_templates) + self.assertNotEqual( + set([strategy2.id, strategy3.id, strategy4.id]), + set(modified_strategies)) + self.assertEqual(set([strategy1.id]), set(unmodified_strategies)) + + self.assertEqual( + set([audit_template2.id, audit_template3.id, audit_template4.id]), + set(modified_audit_templates)) self.assertEqual(set([audit_template1.id]), - unmodified_audit_templates) + set(unmodified_audit_templates)) + + self.assertEqual( + set([audit2.id, audit3.id, audit4.id]), + set(modified_audits)) + self.assertEqual(set([audit1.id]), set(unmodified_audits)) + + self.assertEqual( + set([action_plan2.id, action_plan3.id, action_plan4.id]), + set(modified_action_plans)) + self.assertTrue( + all(ap.state == ap_objects.State.CANCELLED + for ap in modified_action_plans.values())) + self.assertEqual(set([action_plan1.id]), set(unmodified_action_plans)) def test_end2end_sync_goals_with_removed_goal_and_strategy(self): # ### Setup ### # @@ -417,11 +518,13 @@ class TestSyncer(base.DbTestCase): strategy1 = objects.Strategy( self.ctx, id=1, name="strategy_1", uuid=utils.generate_uuid(), display_name="Strategy 1", goal_id=goal1.id) - # To be removed by the sync() + # To be removed by the sync() because strategy entry point does not + # exist anymore strategy2 = objects.Strategy( self.ctx, id=2, name="strategy_2", uuid=utils.generate_uuid(), display_name="Strategy 2", goal_id=goal1.id) - # To be removed by the sync() + # To be removed by the sync() because the goal has been soft deleted + # and because the strategy entry point does not exist anymore strategy3 = objects.Strategy( self.ctx, id=3, name="strategy_3", uuid=utils.generate_uuid(), display_name="Original", goal_id=goal2.id) @@ -435,9 +538,9 @@ class TestSyncer(base.DbTestCase): # The strategy of this audit template will be dereferenced # as it does not exist anymore audit_template1 = objects.AuditTemplate( - self.ctx, id=1, uuid=utils.generate_uuid(), - name="Synced AT1", goal_id=goal1.id, strategy_id=strategy1.id) - # Stale even after syncing because the goal has been soft deleted + self.ctx, id=1, name="Synced AT1", uuid=utils.generate_uuid(), + goal_id=goal1.id, strategy_id=strategy1.id) + # Stale after syncing because the goal has been soft deleted audit_template2 = objects.AuditTemplate( self.ctx, id=2, name="Synced AT2", uuid=utils.generate_uuid(), goal_id=goal2.id, strategy_id=strategy2.id) @@ -445,9 +548,39 @@ class TestSyncer(base.DbTestCase): audit_template1.create() audit_template2.create() - before_audit_templates = objects.AuditTemplate.list(self.ctx) + # Should stay unmodified after sync() + audit1 = objects.Audit( + self.ctx, id=1, uuid=utils.generate_uuid(), + goal_id=goal1.id, strategy_id=strategy1.id) + # Stale after syncing because the goal has been soft deleted + audit2 = objects.Audit( + self.ctx, id=2, uuid=utils.generate_uuid(), + goal_id=goal2.id, strategy_id=strategy2.id) + audit1.create() + audit2.create() + + # Stale after syncing because its related strategy has been be + # soft deleted + action_plan1 = objects.ActionPlan( + self.ctx, id=1, uuid=utils.generate_uuid(), + audit_id=audit1.id, strategy_id=strategy1.id, + first_action_id=None, state='DOESNOTMATTER', + global_efficacy={}) + # Stale after syncing because its related goal has been soft deleted + action_plan2 = objects.ActionPlan( + self.ctx, id=2, uuid=utils.generate_uuid(), + audit_id=audit2.id, strategy_id=strategy2.id, + first_action_id=None, state='DOESNOTMATTER', + global_efficacy={}) + + action_plan1.create() + action_plan2.create() + before_goals = objects.Goal.list(self.ctx) before_strategies = objects.Strategy.list(self.ctx) + before_audit_templates = objects.AuditTemplate.list(self.ctx) + before_audits = objects.Audit.list(self.ctx) + before_action_plans = objects.ActionPlan.list(self.ctx) # ### Action under test ### # @@ -458,54 +591,66 @@ class TestSyncer(base.DbTestCase): # ### Assertions ### # - after_audit_templates = objects.AuditTemplate.list(self.ctx) after_goals = objects.Goal.list(self.ctx) after_strategies = objects.Strategy.list(self.ctx) + after_audit_templates = objects.AuditTemplate.list(self.ctx) + after_audits = objects.Audit.list(self.ctx) + after_action_plans = objects.ActionPlan.list(self.ctx) self.assertEqual(2, len(before_goals)) self.assertEqual(3, len(before_strategies)) self.assertEqual(2, len(before_audit_templates)) + self.assertEqual(2, len(before_audits)) + self.assertEqual(2, len(before_action_plans)) self.assertEqual(1, len(after_goals)) self.assertEqual(1, len(after_strategies)) self.assertEqual(2, len(after_audit_templates)) + self.assertEqual(2, len(after_audits)) + self.assertEqual(2, len(after_action_plans)) self.assertEqual( {"dummy_1"}, set([g.name for g in after_goals])) self.assertEqual( {"strategy_1"}, set([s.name for s in after_strategies])) - created_goals = [ag for ag in after_goals - if ag.uuid not in [bg.uuid for bg in before_goals]] - created_strategies = [ - a_s for a_s in after_strategies - if a_s.uuid not in [b_s.uuid for b_s in before_strategies]] + + created_goals, modified_goals, unmodified_goals = ( + self._find_created_modified_unmodified_ids( + before_goals, after_goals)) + + created_strategies, modified_strategies, unmodified_strategies = ( + self._find_created_modified_unmodified_ids( + before_strategies, after_strategies)) + + (created_audit_templates, modified_audit_templates, + unmodified_audit_templates) = ( + self._find_created_modified_unmodified_ids( + before_audit_templates, after_audit_templates)) + + created_audits, modified_audits, unmodified_audits = ( + self._find_created_modified_unmodified_ids( + before_audits, after_audits)) + + (created_action_plans, modified_action_plans, + unmodified_action_plans) = ( + self._find_created_modified_unmodified_ids( + before_action_plans, after_action_plans)) self.assertEqual(0, len(created_goals)) self.assertEqual(0, len(created_strategies)) - - modified_audit_templates = { - a_at.id for a_at in after_audit_templates - if a_at.goal_id not in ( - # initial goal IDs - b_at.goal_id for b_at in before_audit_templates) or - a_at.strategy_id not in ( - # initial strategy IDs - b_at.strategy_id for b_at in before_audit_templates - if b_at.strategy_id is not None) - } - - unmodified_audit_templates = { - a_at.id for a_at in after_audit_templates - if a_at.goal_id in ( - # initial goal IDs - b_at.goal_id for b_at in before_audit_templates) and - a_at.strategy_id in ( - # initial strategy IDs - b_at.strategy_id for b_at in before_audit_templates - if b_at.strategy_id is not None) - } + self.assertEqual(0, len(created_audits)) + self.assertEqual(0, len(created_action_plans)) self.assertEqual(set([audit_template2.id]), - modified_audit_templates) + set(modified_audit_templates)) self.assertEqual(set([audit_template1.id]), - unmodified_audit_templates) + set(unmodified_audit_templates)) + + self.assertEqual(set([audit2.id]), set(modified_audits)) + self.assertEqual(set([audit1.id]), set(unmodified_audits)) + + self.assertEqual(set([action_plan2.id]), set(modified_action_plans)) + self.assertTrue( + all(ap.state == ap_objects.State.CANCELLED + for ap in modified_action_plans.values())) + self.assertEqual(set([action_plan1.id]), set(unmodified_action_plans)) diff --git a/watcher/tests/objects/utils.py b/watcher/tests/objects/utils.py index 0c1da4c03..9143e5e12 100644 --- a/watcher/tests/objects/utils.py +++ b/watcher/tests/objects/utils.py @@ -100,16 +100,6 @@ def create_test_action_plan(context, **kw): return action_plan -def create_action_plan_without_audit(context, **kw): - """Create and return a test action_plan object. - - Create a action plan in the DB and return a ActionPlan object with - appropriate attributes. - """ - kw['audit_id'] = None - return create_test_action_plan(context, **kw) - - def get_test_action(context, **kw): """Return a Action object with appropriate attributes.