diff --git a/watcher/api/controllers/v1/action_plan.py b/watcher/api/controllers/v1/action_plan.py index 079d44caa..cabbf64fb 100644 --- a/watcher/api/controllers/v1/action_plan.py +++ b/watcher/api/controllers/v1/action_plan.py @@ -49,13 +49,14 @@ standard workflow model description formats such as `Business Process Model and Notation 2.0 (BPMN 2.0) `_ or `Unified Modeling Language (UML) `_. -To see the life-cycle and description of +To see the life-cycle and description of :ref:`Action Plan ` states, visit :ref:`the Action Plan state machine `. """ # noqa import datetime +from oslo_log import log import pecan from pecan import rest import wsme @@ -66,6 +67,7 @@ from watcher._i18n import _ from watcher.api.controllers import base from watcher.api.controllers import link from watcher.api.controllers.v1 import collection +from watcher.api.controllers.v1 import efficacy_indicator as efficacyindicator from watcher.api.controllers.v1 import types from watcher.api.controllers.v1 import utils as api_utils from watcher.applier import rpcapi @@ -73,6 +75,8 @@ from watcher.common import exception from watcher import objects from watcher.objects import action_plan as ap_objects +LOG = log.getLogger(__name__) + class ActionPlanPatchType(types.JsonPatchType): @@ -113,6 +117,7 @@ class ActionPlan(base.APIBase): _audit_uuid = None _first_action_uuid = None + _efficacy_indicators = None def _get_audit_uuid(self): return self._audit_uuid @@ -143,6 +148,34 @@ class ActionPlan(base.APIBase): except exception.ActionNotFound: self._first_action_uuid = None + def _get_efficacy_indicators(self): + if self._efficacy_indicators is None: + self._set_efficacy_indicators(wtypes.Unset) + return self._efficacy_indicators + + def _set_efficacy_indicators(self, value): + efficacy_indicators = [] + if value == wtypes.Unset and not self._efficacy_indicators: + try: + _efficacy_indicators = objects.EfficacyIndicator.list( + pecan.request.context, + filters={"action_plan_uuid": self.uuid}) + + for indicator in _efficacy_indicators: + efficacy_indicator = efficacyindicator.EfficacyIndicator( + context=pecan.request.context, + name=indicator.name, + description=indicator.description, + unit=indicator.unit, + value=indicator.value, + ) + efficacy_indicators.append(efficacy_indicator.as_dict()) + self._efficacy_indicators = efficacy_indicators + except exception.EfficacyIndicatorNotFound as exc: + LOG.exception(exc) + elif value and self._efficacy_indicators != value: + self._efficacy_indicators = value + uuid = wtypes.wsattr(types.uuid, readonly=True) """Unique UUID for this action plan""" @@ -155,6 +188,14 @@ class ActionPlan(base.APIBase): mandatory=True) """The UUID of the audit this port belongs to""" + efficacy_indicators = wsme.wsproperty( + types.jsontype, _get_efficacy_indicators, _set_efficacy_indicators, + mandatory=True) + """The list of efficacy indicators associated to this action plan""" + + global_efficacy = wtypes.wsattr(types.jsontype, readonly=True) + """The global efficacy of this action plan""" + state = wtypes.text """This action plan state""" @@ -163,7 +204,6 @@ class ActionPlan(base.APIBase): def __init__(self, **kwargs): super(ActionPlan, self).__init__() - self.fields = [] fields = list(objects.ActionPlan.fields) for field in fields: @@ -175,6 +215,7 @@ class ActionPlan(base.APIBase): self.fields.append('audit_uuid') self.fields.append('first_action_uuid') + self.fields.append('efficacy_indicators') setattr(self, 'audit_uuid', kwargs.get('audit_id', wtypes.Unset)) setattr(self, 'first_action_uuid', @@ -184,12 +225,13 @@ class ActionPlan(base.APIBase): def _convert_with_links(action_plan, url, expand=True): if not expand: action_plan.unset_fields_except( - ['uuid', 'state', 'updated_at', - 'audit_uuid', 'first_action_uuid']) + ['uuid', 'state', 'efficacy_indicators', 'global_efficacy', + 'updated_at', 'audit_uuid', 'first_action_uuid']) - action_plan.links = [link.Link.make_link( - 'self', url, - 'action_plans', action_plan.uuid), + action_plan.links = [ + link.Link.make_link( + 'self', url, + 'action_plans', action_plan.uuid), link.Link.make_link( 'bookmark', url, 'action_plans', action_plan.uuid, @@ -211,6 +253,12 @@ class ActionPlan(base.APIBase): updated_at=datetime.datetime.utcnow()) sample._first_action_uuid = '57eaf9ab-5aaa-4f7e-bdf7-9a140ac7a720' sample._audit_uuid = 'abcee106-14d3-4515-b744-5a26885cf6f6' + sample._efficacy_indicators = [{'description': 'Test indicator', + 'name': 'test_indicator', + 'unit': '%'}] + sample._global_efficacy = {'description': 'Global efficacy', + 'name': 'test_global_efficacy', + 'unit': '%'} return cls._convert_with_links(sample, 'http://localhost:9322', expand) diff --git a/watcher/common/exception.py b/watcher/common/exception.py index 7fb131eab..8c517150f 100644 --- a/watcher/common/exception.py +++ b/watcher/common/exception.py @@ -311,6 +311,11 @@ class InvalidIndicatorValue(WatcherException): "and spec type '%(spec_type)s' is invalid.") +class GlobalEfficacyComputationError(WatcherException): + msg_fmt = _("Could not compute the global efficacy for the '%(goal)s' " + "goal using the '%(strategy)s' strategy.") + + class NoMetricValuesForVM(WatcherException): msg_fmt = _("No values returned by %(resource_id)s for %(metric_name)s.") diff --git a/watcher/decision_engine/planner/default.py b/watcher/decision_engine/planner/default.py index db55f804d..9d568ecc5 100644 --- a/watcher/decision_engine/planner/default.py +++ b/watcher/decision_engine/planner/default.py @@ -59,19 +59,21 @@ class DefaultPlanner(base.BasePlanner): def schedule(self, context, audit_id, solution): LOG.debug('Create an action plan for the audit uuid: %s ', audit_id) - action_plan = self._create_action_plan(context, audit_id) + action_plan = self._create_action_plan(context, audit_id, solution) actions = list(solution.actions) to_schedule = [] for action in actions: - json_action = self.create_action(action_plan_id=action_plan.id, - action_type=action.get( - 'action_type'), - input_parameters=action.get( - 'input_parameters')) + json_action = self.create_action( + action_plan_id=action_plan.id, + action_type=action.get('action_type'), + input_parameters=action.get('input_parameters')) to_schedule.append((self.priorities[action.get('action_type')], json_action)) + self._create_efficacy_indicators( + context, action_plan.id, solution.efficacy_indicators) + # scheduling scheduled = sorted(to_schedule, key=lambda x: (x[0])) if len(scheduled) == 0: @@ -96,19 +98,38 @@ class DefaultPlanner(base.BasePlanner): return action_plan - def _create_action_plan(self, context, audit_id): + def _create_action_plan(self, context, audit_id, solution): action_plan_dict = { 'uuid': utils.generate_uuid(), 'audit_id': audit_id, 'first_action_id': None, - 'state': objects.action_plan.State.RECOMMENDED + 'state': objects.action_plan.State.RECOMMENDED, + 'global_efficacy': solution.global_efficacy, } new_action_plan = objects.ActionPlan(context, **action_plan_dict) new_action_plan.create(context) - new_action_plan.save() + return new_action_plan + def _create_efficacy_indicators(self, context, action_plan_id, indicators): + efficacy_indicators = [] + for indicator in indicators: + efficacy_indicator_dict = { + 'uuid': utils.generate_uuid(), + 'name': indicator.name, + 'description': indicator.description, + 'unit': indicator.unit, + 'value': indicator.value, + 'action_plan_id': action_plan_id, + } + new_efficacy_indicator = objects.EfficacyIndicator( + context, **efficacy_indicator_dict) + new_efficacy_indicator.create(context) + + efficacy_indicators.append(new_efficacy_indicator) + return efficacy_indicators + def _create_action(self, context, _action, parent_action): try: LOG.debug("Creating the %s in watcher db", diff --git a/watcher/decision_engine/solution/base.py b/watcher/decision_engine/solution/base.py index cb0b5fcc9..22f56e4be 100644 --- a/watcher/decision_engine/solution/base.py +++ b/watcher/decision_engine/solution/base.py @@ -37,59 +37,62 @@ applied. Two approaches to dealing with this can be envisaged: -- **fully automated mode**: only the :ref:`Solution ` - with the highest ranking (i.e., the highest - :ref:`Optimization Efficiency `) - will be sent to the :ref:`Watcher Planner ` and - translated into concrete :ref:`Actions `. -- **manual mode**: several :ref:`Solutions ` are proposed - to the :ref:`Administrator ` with a detailed - measurement of the estimated - :ref:`Optimization Efficiency ` and he/she decides - which one will be launched. +- **fully automated mode**: only the :ref:`Solution ` + with the highest ranking (i.e., the highest + :ref:`Optimization Efficacy `) will be sent to the + :ref:`Watcher Planner ` and translated into + concrete :ref:`Actions `. +- **manual mode**: several :ref:`Solutions ` are proposed + to the :ref:`Administrator ` with a detailed + measurement of the estimated :ref:`Optimization Efficacy + ` and he/she decides which one will be launched. """ import abc import six +from watcher.decision_engine.solution import efficacy + @six.add_metaclass(abc.ABCMeta) class BaseSolution(object): - def __init__(self): - self._origin = None - self._model = None - self._efficacy = 0 + def __init__(self, goal, strategy): + """Base Solution constructor + + :param goal: Goal associated to this solution + :type goal: :py:class:`~.base.Goal` instance + :param strategy: Strategy associated to this solution + :type strategy: :py:class:`~.BaseStrategy` instance + """ + self.goal = goal + self.strategy = strategy + self.origin = None + self.model = None + self.efficacy = efficacy.Efficacy(self.goal, self.strategy) @property - def efficacy(self): - return self._efficacy - - @efficacy.setter - def efficacy(self, e): - self._efficacy = e + def global_efficacy(self): + return self.efficacy.global_efficacy @property - def model(self): - return self._model + def efficacy_indicators(self): + return self.efficacy.indicators - @model.setter - def model(self, m): - self._model = m + def compute_global_efficacy(self): + """Compute the global efficacy given a map of efficacy indicators""" + self.efficacy.compute_global_efficacy() - @property - def origin(self): - return self._origin + def set_efficacy_indicators(self, **indicators_map): + """Set the efficacy indicators mapping (no validation) - @origin.setter - def origin(self, m): - self._origin = m + :param indicators_map: mapping between the indicator name and its value + :type indicators_map: dict {`str`: `object`} + """ + self.efficacy.set_efficacy_indicators(**indicators_map) @abc.abstractmethod - def add_action(self, - action_type, - resource_id, - input_parameters=None): - """Add a new Action in the Action Plan + def add_action(self, action_type, resource_id, input_parameters=None): + """Add a new Action in the Solution :param action_type: the unique id of an action type defined in entry point 'watcher_actions' diff --git a/watcher/decision_engine/solution/default.py b/watcher/decision_engine/solution/default.py index 9784c3525..a64950cb5 100644 --- a/watcher/decision_engine/solution/default.py +++ b/watcher/decision_engine/solution/default.py @@ -22,19 +22,21 @@ from watcher.decision_engine.solution import base class DefaultSolution(base.BaseSolution): - def __init__(self): + def __init__(self, goal, strategy): """Stores a set of actions generated by a strategy The DefaultSolution class store a set of actions generated by a strategy in order to achieve the goal. + + :param goal: Goal associated to this solution + :type goal: :py:class:`~.base.Goal` instance + :param strategy: Strategy associated to this solution + :type strategy: :py:class:`~.BaseStrategy` instance """ - super(DefaultSolution, self).__init__() + super(DefaultSolution, self).__init__(goal, strategy) self._actions = [] - def add_action(self, action_type, - input_parameters=None, - resource_id=None): - + def add_action(self, action_type, input_parameters=None, resource_id=None): if input_parameters is not None: if baction.BaseAction.RESOURCE_ID in input_parameters.keys(): raise exception.ReservedWord(name=baction.BaseAction. diff --git a/watcher/decision_engine/solution/efficacy.py b/watcher/decision_engine/solution/efficacy.py index 827a0d87d..108e78fe3 100644 --- a/watcher/decision_engine/solution/efficacy.py +++ b/watcher/decision_engine/solution/efficacy.py @@ -14,8 +14,20 @@ # See the License for the specific language governing permissions and # limitations under the License. +import numbers + +from oslo_log import log as logging + +from watcher._i18n import _ +from watcher.common import exception from watcher.common import utils +LOG = logging.getLogger(__name__) + + +class IndicatorsMap(utils.Struct): + pass + class Indicator(utils.Struct): @@ -24,4 +36,70 @@ class Indicator(utils.Struct): self.name = name self.description = description self.unit = unit + if not isinstance(value, numbers.Number): + raise exception.InvalidIndicatorValue( + _("An indicator value should be a number")) self.value = value + + +class Efficacy(object): + """Solution efficacy""" + + def __init__(self, goal, strategy): + """Solution efficacy + + :param goal: Goal associated to this solution + :type goal: :py:class:`~.base.Goal` instance + :param strategy: Strategy associated to this solution + :type strategy: :py:class:`~.BaseStrategy` instance + """ + self.goal = goal + self.strategy = strategy + + self._efficacy_spec = self.goal.efficacy_specification + + # Used to store in DB the info related to the efficacy indicators + self.indicators = [] + # Used to compute the global efficacy + self._indicators_mapping = IndicatorsMap() + self.global_efficacy = None + + def set_efficacy_indicators(self, **indicators_map): + """Set the efficacy indicators + + :param indicators_map: kwargs where the key is the name of the efficacy + indicator as defined in the associated + :py:class:`~.IndicatorSpecification` and the + value is a number. + :type indicators_map: dict {str: numerical value} + """ + self._indicators_mapping.update(indicators_map) + + def compute_global_efficacy(self): + self._efficacy_spec.validate_efficacy_indicators( + self._indicators_mapping) + try: + self.global_efficacy = ( + self._efficacy_spec.get_global_efficacy_indicator( + self._indicators_mapping)) + + indicators_specs_map = { + indicator_spec.name: indicator_spec + for indicator_spec in self._efficacy_spec.indicators_specs} + + indicators = [] + for indicator_name, value in self._indicators_mapping.items(): + related_indicator_spec = indicators_specs_map[indicator_name] + indicators.append( + Indicator( + name=related_indicator_spec.name, + description=related_indicator_spec.description, + unit=related_indicator_spec.unit, + value=value)) + + self.indicators = indicators + except Exception as exc: + LOG.exception(exc) + raise exception.GlobalEfficacyComputationError( + goal=self.goal.name, + strategy=self.strategy.name) diff --git a/watcher/decision_engine/strategy/strategies/base.py b/watcher/decision_engine/strategy/strategies/base.py index 92863193c..86c8bc7c2 100644 --- a/watcher/decision_engine/strategy/strategies/base.py +++ b/watcher/decision_engine/strategy/strategies/base.py @@ -66,11 +66,12 @@ class BaseStrategy(loadable.Loadable): super(BaseStrategy, self).__init__(config) self._name = self.get_name() self._display_name = self.get_display_name() + self._goal = self.get_goal() # default strategy level self._strategy_level = level.StrategyLevel.conservative self._cluster_state_collector = None # the solution given by the strategy - self._solution = default.DefaultSolution() + self._solution = default.DefaultSolution(goal=self.goal, strategy=self) self._osc = osc self._collector_manager = None self._model = None @@ -99,7 +100,7 @@ class BaseStrategy(loadable.Loadable): @classmethod @abc.abstractmethod def get_goal_name(cls): - """The goal name for the strategy""" + """The goal name the strategy achieves""" raise NotImplementedError() @classmethod @@ -151,6 +152,8 @@ class BaseStrategy(loadable.Loadable): self.do_execute() self.post_execute() + self.solution.compute_global_efficacy() + return self.solution @property diff --git a/watcher/decision_engine/strategy/strategies/basic_consolidation.py b/watcher/decision_engine/strategy/strategies/basic_consolidation.py index b88f20f9d..07a82a97a 100644 --- a/watcher/decision_engine/strategy/strategies/basic_consolidation.py +++ b/watcher/decision_engine/strategy/strategies/basic_consolidation.py @@ -141,7 +141,6 @@ class BasicConsolidation(base.ServerConsolidationBaseStrategy): def check_migration(self, src_hypervisor, dest_hypervisor, vm_to_mig): """Check if the migration is possible - :param self.model: the current state of the cluster :param src_hypervisor: the current node of the virtual machine :param dest_hypervisor: the destination of the virtual machine :param vm_to_mig: the virtual machine @@ -185,7 +184,6 @@ class BasicConsolidation(base.ServerConsolidationBaseStrategy): check the threshold value defined by the ratio of aggregated CPU capacity of VMs on one node to CPU capacity of this node must not exceed the threshold value. - :param self.model: the current state of the cluster :param dest_hypervisor: the destination of the virtual machine :param total_cores :param total_disk @@ -216,7 +214,6 @@ class BasicConsolidation(base.ServerConsolidationBaseStrategy): total_memory_used): """Calculate weight of every resource - :param self.model: :param element: :param total_cores_used: :param total_disk_used: @@ -482,4 +479,7 @@ class BasicConsolidation(base.ServerConsolidationBaseStrategy): LOG.debug(infos) def post_execute(self): - pass + self.solution.set_efficacy_indicators( + released_compute_nodes_count=self.number_of_migrations, + vm_migrations_count=self.number_of_released_nodes, + ) diff --git a/watcher/decision_engine/strategy/strategies/vm_workload_consolidation.py b/watcher/decision_engine/strategy/strategies/vm_workload_consolidation.py index ca4ae6d58..bca5ed178 100644 --- a/watcher/decision_engine/strategy/strategies/vm_workload_consolidation.py +++ b/watcher/decision_engine/strategy/strategies/vm_workload_consolidation.py @@ -332,7 +332,7 @@ class VMWorkloadConsolidation(base.ServerConsolidationBaseStrategy): :param model: model_root object :return: {'cpu': <0,1>, 'ram': <0,1>, 'disk': <0,1>} """ - hypervisors = self.model.get_all_hypervisors().values() + hypervisors = model.get_all_hypervisors().values() rcu = {} counters = {} for hypervisor in hypervisors: @@ -517,7 +517,7 @@ class VMWorkloadConsolidation(base.ServerConsolidationBaseStrategy): :param original_model: root_model object """ LOG.info(_LI('Executing Smart Strategy')) - model = self.get_prediction_model(self.model) + model = self.get_prediction_model() rcu = self.get_relative_cluster_utilization(model) self.ceilometer_vm_data_cache = dict() @@ -546,9 +546,9 @@ class VMWorkloadConsolidation(base.ServerConsolidationBaseStrategy): LOG.debug(info) - self.solution.model = model - self.solution.efficacy = rcu_after['cpu'] - def post_execute(self): - # TODO(v-francoise): Add the indicators to the solution - pass + # self.solution.efficacy = rcu_after['cpu'] + self.solution.set_efficacy_indicators( + released_compute_nodes_count=self.number_of_migrations, + vm_migrations_count=self.number_of_released_hypervisors, + ) diff --git a/watcher/decision_engine/strategy/strategies/workload_stabilization.py b/watcher/decision_engine/strategy/strategies/workload_stabilization.py index b7a2a16e6..bd47565d9 100644 --- a/watcher/decision_engine/strategy/strategies/workload_stabilization.py +++ b/watcher/decision_engine/strategy/strategies/workload_stabilization.py @@ -362,7 +362,6 @@ class WorkloadStabilization(base.WorkloadStabilizationBaseStrategy): def fill_solution(self): self.solution.model = self.model - self.solution.efficacy = 100 return self.solution def pre_execute(self): @@ -403,11 +402,10 @@ class WorkloadStabilization(base.WorkloadStabilizationBaseStrategy): break if balanced: break - return self.fill_solution() def post_execute(self): """Post-execution phase This can be used to compute the global efficacy """ - self.solution.model = self.model + self.fill_solution() diff --git a/watcher/decision_engine/sync.py b/watcher/decision_engine/sync.py index 092baf06d..cd7676ec5 100644 --- a/watcher/decision_engine/sync.py +++ b/watcher/decision_engine/sync.py @@ -277,8 +277,8 @@ class Syncer(object): display_name=goal_cls.get_translatable_display_name(), efficacy_specification=tuple( IndicatorSpec(**indicator.to_dict()) - for indicator in goal_cls.get_efficacy_specification() - .get_indicators_specifications())) + for indicator in goal_cls.get_efficacy_specification( + ).get_indicators_specifications())) for _, strategy_cls in implemented_strategies.items(): strategies_map[strategy_cls.get_name()] = StrategyMapping( diff --git a/watcher/objects/action_plan.py b/watcher/objects/action_plan.py index 4386eae2f..52aece120 100644 --- a/watcher/objects/action_plan.py +++ b/watcher/objects/action_plan.py @@ -73,6 +73,7 @@ from watcher.common import utils from watcher.db import api as dbapi from watcher.objects import action as action_objects from watcher.objects import base +from watcher.objects import efficacy_indicator as indicator_objects from watcher.objects import utils as obj_utils @@ -98,6 +99,7 @@ class ActionPlan(base.WatcherObject): 'audit_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, } @staticmethod @@ -192,7 +194,7 @@ class ActionPlan(base.WatcherObject): self._from_db_object(self, db_action_plan) def destroy(self, context=None): - """Delete the Action from the DB. + """Delete the action plan from the DB. :param context: Security context. NOTE: This should only be used internally by the indirection_api. @@ -201,6 +203,14 @@ class ActionPlan(base.WatcherObject): A context should be set when instantiating the object, e.g.: Action(context) """ + related_efficacy_indicators = indicator_objects.EfficacyIndicator.list( + context=self._context, + filters={"action_plan_uuid": self.uuid}) + + # Cascade soft_delete of related efficacy indicators + for related_efficacy_indicator in related_efficacy_indicators: + related_efficacy_indicator.destroy() + self.dbapi.destroy_action_plan(self.uuid) self.obj_reset_changes() @@ -260,6 +270,14 @@ class ActionPlan(base.WatcherObject): for related_action in related_actions: related_action.soft_delete() + related_efficacy_indicators = indicator_objects.EfficacyIndicator.list( + context=self._context, + filters={"action_plan_uuid": self.uuid}) + + # Cascade soft_delete of related efficacy indicators + for related_efficacy_indicator in related_efficacy_indicators: + related_efficacy_indicator.soft_delete() + self.dbapi.soft_delete_action_plan(self.uuid) self.state = State.DELETED self.save() diff --git a/watcher/objects/efficacy_indicator.py b/watcher/objects/efficacy_indicator.py index 39de4eb45..2275f0017 100644 --- a/watcher/objects/efficacy_indicator.py +++ b/watcher/objects/efficacy_indicator.py @@ -195,5 +195,3 @@ class EfficacyIndicator(base.WatcherObject): object, e.g.: Audit(context) """ self.dbapi.soft_delete_efficacy_indicator(self.uuid) - self.state = "DELETED" - self.save() diff --git a/watcher/tests/api/v1/test_actions_plans.py b/watcher/tests/api/v1/test_actions_plans.py index ee0682f32..a82158575 100644 --- a/watcher/tests/api/v1/test_actions_plans.py +++ b/watcher/tests/api/v1/test_actions_plans.py @@ -13,13 +13,14 @@ import datetime import itertools import mock - +import pecan from oslo_config import cfg 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 @@ -31,7 +32,11 @@ from watcher.tests.objects import utils as obj_utils class TestActionPlanObject(base.TestCase): - def test_action_plan_init(self): + @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'] @@ -47,7 +52,8 @@ class TestListActionPlan(api_base.FunctionalTest): self.assertEqual([], response['action_plans']) def _assert_action_plans_fields(self, action_plan): - action_plan_fields = ['state'] + action_plan_fields = ['uuid', 'audit_uuid', 'state', 'global_efficacy', + 'efficacy_indicators'] for field in action_plan_fields: self.assertIn(field, action_plan) @@ -71,10 +77,18 @@ class TestListActionPlan(api_base.FunctionalTest): self.assertEqual([], response['action_plans']) def test_get_one_ok(self): - action_plan = obj_utils.create_action_plan_without_audit(self.context) + action_plan = obj_utils.create_test_action_plan(self.context) + obj_utils.create_test_efficacy_indicator( + self.context, action_plan_id=action_plan['id']) response = self.get_json('/action_plans/%s' % action_plan['uuid']) self.assertEqual(action_plan.uuid, response['uuid']) self._assert_action_plans_fields(response) + self.assertEqual( + [{'description': 'Test indicator', + 'name': 'test_indicator', + 'value': 0.0, + 'unit': '%'}], + response['efficacy_indicators']) def test_get_one_with_first_action(self): action_plan = obj_utils.create_test_action_plan(self.context) diff --git a/watcher/tests/db/utils.py b/watcher/tests/db/utils.py index a76980a2f..55702636a 100644 --- a/watcher/tests/db/utils.py +++ b/watcher/tests/db/utils.py @@ -120,6 +120,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), + 'global_efficacy': kwargs.get('global_efficacy', {}), 'first_action_id': kwargs.get('first_action_id', 1), 'created_at': kwargs.get('created_at'), 'updated_at': kwargs.get('updated_at'), diff --git a/watcher/tests/decision_engine/fake_strategies.py b/watcher/tests/decision_engine/fake_strategies.py index 369d4fe61..7d78a2507 100644 --- a/watcher/tests/decision_engine/fake_strategies.py +++ b/watcher/tests/decision_engine/fake_strategies.py @@ -47,7 +47,13 @@ class FakeStrategy(base_strategy.BaseStrategy): def get_config_opts(cls): return [] - def execute(self, original_model): + def pre_execute(self): + pass + + def do_execute(self): + pass + + def post_execute(self): pass diff --git a/watcher/tests/decision_engine/planner/test_default_planner.py b/watcher/tests/decision_engine/planner/test_default_planner.py index d27ab7999..edcfa7656 100644 --- a/watcher/tests/decision_engine/planner/test_default_planner.py +++ b/watcher/tests/decision_engine/planner/test_default_planner.py @@ -53,14 +53,17 @@ class SolutionFakerSingleHyp(object): current_state_cluster.generate_scenario_3_with_2_hypervisors()) sercon.ceilometer = mock.MagicMock( get_statistics=metrics.mock_get_statistics) + return sercon.execute() class TestActionScheduling(base.DbTestCase): + def test_schedule_actions(self): default_planner = pbase.DefaultPlanner(mock.Mock()) audit = db_utils.create_test_audit(uuid=utils.generate_uuid()) - solution = dsol.DefaultSolution() + solution = dsol.DefaultSolution( + goal=mock.Mock(), strategy=mock.Mock()) parameters = { "src_uuid_hypervisor": "server1", @@ -86,7 +89,8 @@ class TestActionScheduling(base.DbTestCase): 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() + solution = dsol.DefaultSolution( + goal=mock.Mock(), strategy=mock.Mock()) parameters = { "src_uuid_hypervisor": "server1", diff --git a/watcher/tests/decision_engine/solution/test_default_solution.py b/watcher/tests/decision_engine/solution/test_default_solution.py index 0c67333ad..41ee13a1b 100644 --- a/watcher/tests/decision_engine/solution/test_default_solution.py +++ b/watcher/tests/decision_engine/solution/test_default_solution.py @@ -14,13 +14,16 @@ # See the License for the specific language governing permissions and # limitations under the License. +import mock + from watcher.decision_engine.solution import default from watcher.tests import base class TestDefaultSolution(base.BaseTestCase): def test_default_solution(self): - solution = default.DefaultSolution() + solution = default.DefaultSolution( + goal=mock.Mock(), strategy=mock.Mock()) parameters = { "src_uuid_hypervisor": "server1", "dst_uuid_hypervisor": "server2", diff --git a/watcher/tests/decision_engine/strategy/selector/test_strategy_selector.py b/watcher/tests/decision_engine/strategy/selector/test_strategy_selector.py index e80f1499a..a1ada2899 100644 --- a/watcher/tests/decision_engine/strategy/selector/test_strategy_selector.py +++ b/watcher/tests/decision_engine/strategy/selector/test_strategy_selector.py @@ -60,7 +60,6 @@ class TestStrategySelector(base.TestCase): @mock.patch.object(default_loader.DefaultStrategyLoader, 'list_available') def test_select_no_available_strategy_for_goal(self, m_list_available): m_list_available.return_value = {} - strategy_selector = default_selector.DefaultStrategySelector( - "dummy") + strategy_selector = default_selector.DefaultStrategySelector("dummy") self.assertRaises(exception.NoAvailableStrategyForGoal, strategy_selector.select) diff --git a/watcher/tests/decision_engine/strategy/strategies/test_basic_consolidation.py b/watcher/tests/decision_engine/strategy/strategies/test_basic_consolidation.py index aefca9e20..a028dd1af 100644 --- a/watcher/tests/decision_engine/strategy/strategies/test_basic_consolidation.py +++ b/watcher/tests/decision_engine/strategy/strategies/test_basic_consolidation.py @@ -180,7 +180,7 @@ class TestBasicConsolidation(base.BaseTestCase): ) as mock_score_call: mock_score_call.return_value = 0 solution = self.strategy.execute() - self.assertEqual(0, solution.efficacy) + self.assertEqual(0, solution.efficacy.global_efficacy.value) def test_check_parameters(self): model = self.fake_cluster.generate_scenario_3_with_2_hypervisors() diff --git a/watcher/tests/decision_engine/strategy/strategies/test_outlet_temp_control.py b/watcher/tests/decision_engine/strategy/strategies/test_outlet_temp_control.py index ccb9bf8e6..c6796467d 100644 --- a/watcher/tests/decision_engine/strategy/strategies/test_outlet_temp_control.py +++ b/watcher/tests/decision_engine/strategy/strategies/test_outlet_temp_control.py @@ -37,7 +37,6 @@ class TestOutletTempControl(base.BaseTestCase): super(TestOutletTempControl, self).setUp() # fake metrics self.fake_metrics = faker_metrics_collector.FakerMetricsCollector() - # fake cluster self.fake_cluster = faker_cluster_state.FakerModelCollector() diff --git a/watcher/tests/objects/test_action_plan.py b/watcher/tests/objects/test_action_plan.py index df10880bc..9ed9980da 100644 --- a/watcher/tests/objects/test_action_plan.py +++ b/watcher/tests/objects/test_action_plan.py @@ -74,19 +74,69 @@ class TestActionPlanObject(base.DbTestCase): self.assertEqual(self.context, action_plan._context) def test_destroy(self): + efficacy_indicator = utils.get_test_efficacy_indicator( + action_plan_id=self.fake_action_plan['id']) uuid = self.fake_action_plan['uuid'] - with mock.patch.object(self.dbapi, 'get_action_plan_by_uuid', - autospec=True) as mock_get_action_plan: - mock_get_action_plan.return_value = self.fake_action_plan - with mock.patch.object(self.dbapi, 'destroy_action_plan', - autospec=True) as mock_destroy_action_plan: - action_plan = objects.ActionPlan.get_by_uuid( - self.context, uuid) - action_plan.destroy() - mock_get_action_plan.assert_called_once_with( - self.context, uuid) - mock_destroy_action_plan.assert_called_once_with(uuid) - self.assertEqual(self.context, action_plan._context) + + with mock.patch.multiple( + self.dbapi, autospec=True, + get_action_plan_by_uuid=mock.DEFAULT, + destroy_action_plan=mock.DEFAULT, + get_efficacy_indicator_list=mock.DEFAULT, + destroy_efficacy_indicator=mock.DEFAULT, + ) as m_dict: + m_get_action_plan = m_dict['get_action_plan_by_uuid'] + m_destroy_action_plan = m_dict['destroy_action_plan'] + m_get_efficacy_indicator_list = ( + m_dict['get_efficacy_indicator_list']) + m_destroy_efficacy_indicator = m_dict['destroy_efficacy_indicator'] + m_get_action_plan.return_value = self.fake_action_plan + m_get_efficacy_indicator_list.return_value = [efficacy_indicator] + action_plan = objects.ActionPlan.get_by_uuid(self.context, uuid) + action_plan.destroy() + m_get_action_plan.assert_called_once_with(self.context, uuid) + m_get_efficacy_indicator_list.assert_called_once_with( + self.context, filters={"action_plan_uuid": uuid}, + limit=None, marker=None, sort_dir=None, sort_key=None) + m_destroy_action_plan.assert_called_once_with(uuid) + m_destroy_efficacy_indicator.assert_called_once_with( + efficacy_indicator['uuid']) + self.assertEqual(self.context, action_plan._context) + + def test_soft_delete(self): + efficacy_indicator = utils.get_test_efficacy_indicator( + action_plan_id=self.fake_action_plan['id']) + uuid = self.fake_action_plan['uuid'] + + with mock.patch.multiple( + self.dbapi, autospec=True, + get_action_plan_by_uuid=mock.DEFAULT, + soft_delete_action_plan=mock.DEFAULT, + update_action_plan=mock.DEFAULT, + get_efficacy_indicator_list=mock.DEFAULT, + soft_delete_efficacy_indicator=mock.DEFAULT, + ) as m_dict: + m_get_action_plan = m_dict['get_action_plan_by_uuid'] + m_soft_delete_action_plan = m_dict['soft_delete_action_plan'] + m_get_efficacy_indicator_list = ( + m_dict['get_efficacy_indicator_list']) + m_soft_delete_efficacy_indicator = ( + m_dict['soft_delete_efficacy_indicator']) + m_update_action_plan = m_dict['update_action_plan'] + m_get_action_plan.return_value = self.fake_action_plan + m_get_efficacy_indicator_list.return_value = [efficacy_indicator] + action_plan = objects.ActionPlan.get_by_uuid(self.context, uuid) + action_plan.soft_delete() + m_get_action_plan.assert_called_once_with(self.context, uuid) + m_get_efficacy_indicator_list.assert_called_once_with( + self.context, filters={"action_plan_uuid": uuid}, + limit=None, marker=None, sort_dir=None, sort_key=None) + m_soft_delete_action_plan.assert_called_once_with(uuid) + m_soft_delete_efficacy_indicator.assert_called_once_with( + efficacy_indicator['uuid']) + m_update_action_plan.assert_called_once_with( + uuid, {'state': 'DELETED'}) + self.assertEqual(self.context, action_plan._context) def test_save(self): uuid = self.fake_action_plan['uuid'] diff --git a/watcher/tests/objects/utils.py b/watcher/tests/objects/utils.py index 176d4e749..9ee6082a7 100644 --- a/watcher/tests/objects/utils.py +++ b/watcher/tests/objects/utils.py @@ -189,3 +189,30 @@ def create_test_strategy(context, **kw): strategy = get_test_strategy(context, **kw) strategy.create() return strategy + + +def get_test_efficacy_indicator(context, **kw): + """Return a EfficacyIndicator object with appropriate attributes. + + NOTE: The object leaves the attributes marked as changed, such + that a create() could be used to commit it to the DB. + """ + db_efficacy_indicator = db_utils.get_test_efficacy_indicator(**kw) + # Let DB generate ID if it isn't specified explicitly + if 'id' not in kw: + del db_efficacy_indicator['id'] + efficacy_indicator = objects.EfficacyIndicator(context) + for key in db_efficacy_indicator: + setattr(efficacy_indicator, key, db_efficacy_indicator[key]) + return efficacy_indicator + + +def create_test_efficacy_indicator(context, **kw): + """Create and return a test efficacy indicator object. + + Create a efficacy indicator in the DB and return a EfficacyIndicator object + with appropriate attributes. + """ + efficacy_indicator = get_test_efficacy_indicator(context, **kw) + efficacy_indicator.create() + return efficacy_indicator