From 8bac4fd42a7f085f71b3fa39d9130f1239adc50b Mon Sep 17 00:00:00 2001 From: Jean-Emile DARTOIS Date: Mon, 11 Jan 2016 16:02:28 +0100 Subject: [PATCH] Add a dynamic loading of Actions handlers in the Watcher Applier In watcher, an audit generates a set of actions which aims at achieving a given goal (lower energy consumption, ...). It is possible to configure different strategies in order to achieve each goal. Each strategy is written as a Python class which produces a set of actions. Today, the set of possible actions is fixed for a given version of Watcher and enables optimization algorithms to include actions such as instance migration, changing hypervisor state, changing power state (ACPI level, ...). The objective of this patchset is to give the ability to load the actions dynamically in order to apply the Action Plan. DocImpact Partially implements: blueprint watcher-add-actions-via-conf Change-Id: Idf295b94dca549ac65d4636e8889c8ab2ecc0df6 --- setup.cfg | 5 ++ watcher/applier/default.py | 21 +++-- watcher/applier/execution/base.py | 62 +++++++++++++++ watcher/applier/execution/default.py | 57 ++++++++++++++ watcher/applier/execution/executor.py | 76 ------------------- watcher/applier/mapping/__init__.py | 0 watcher/applier/mapping/base.py | 33 -------- watcher/applier/mapping/default.py | 47 ------------ watcher/applier/messaging/events.py | 4 +- watcher/applier/primitives/base.py | 27 ++++++- .../primitives/change_nova_service_state.py | 53 ++++++------- watcher/applier/primitives/factory.py | 36 +++++++++ watcher/applier/primitives/migration.py | 58 ++++++-------- watcher/applier/primitives/nop.py | 15 ++-- watcher/applier/primitives/power_state.py | 32 -------- watcher/common/exception.py | 6 +- watcher/decision_engine/model/model_root.py | 2 +- watcher/decision_engine/planner/default.py | 13 +--- watcher/decision_engine/planner/manager.py | 2 +- .../strategies/outlet_temp_control.py | 4 +- watcher/locale/fr/LC_MESSAGES/watcher.po | 23 ++++-- watcher/locale/watcher.pot | 17 +++-- ...y => test_default_action_plan_executor.py} | 30 +++----- watcher/tests/applier/mapping/__init__.py | 0 .../applier/mapping/test_action_mapper.py | 59 -------------- .../mapping/test_default_action_mapper.py | 59 -------------- .../tests/decision_engine/model/test_model.py | 2 +- .../planner/test_default_planner_loader.py | 4 +- .../planner/test_planner_manager.py | 8 +- 29 files changed, 305 insertions(+), 450 deletions(-) create mode 100644 watcher/applier/execution/base.py create mode 100644 watcher/applier/execution/default.py delete mode 100644 watcher/applier/execution/executor.py delete mode 100644 watcher/applier/mapping/__init__.py delete mode 100644 watcher/applier/mapping/base.py delete mode 100644 watcher/applier/mapping/default.py create mode 100644 watcher/applier/primitives/factory.py delete mode 100644 watcher/applier/primitives/power_state.py rename watcher/tests/applier/execution/{test_action_plan_executor.py => test_default_action_plan_executor.py} (65%) delete mode 100644 watcher/tests/applier/mapping/__init__.py delete mode 100644 watcher/tests/applier/mapping/test_action_mapper.py delete mode 100644 watcher/tests/applier/mapping/test_default_action_mapper.py diff --git a/setup.cfg b/setup.cfg index 5c05951ad..aef6ff09f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -46,6 +46,11 @@ watcher_strategies = basic = watcher.decision_engine.strategy.strategies.basic_consolidation:BasicConsolidation outlet_temp_control = watcher.decision_engine.strategy.strategies.outlet_temp_control:OutletTempControl +watcher_actions = + migrate = watcher.applier.primitives.migration:Migrate + nop = watcher.applier.primitives.nop:Nop + change_nova_service_state = watcher.applier.primitives.change_nova_service_state:ChangeNovaServiceState + watcher_planners = default = watcher.decision_engine.planner.default:DefaultPlanner diff --git a/watcher/applier/default.py b/watcher/applier/default.py index 0cbd7cb7b..4d070ca6d 100644 --- a/watcher/applier/default.py +++ b/watcher/applier/default.py @@ -17,24 +17,23 @@ # limitations under the License. # -from watcher.applier.base import BaseApplier -from watcher.applier.execution.executor import ActionPlanExecutor -from watcher.objects import Action -from watcher.objects import ActionPlan +from watcher.applier import base +from watcher.applier.execution import default +from watcher import objects -class DefaultApplier(BaseApplier): +class DefaultApplier(base.BaseApplier): def __init__(self, manager_applier, context): super(DefaultApplier, self).__init__() self.manager_applier = manager_applier self.context = context - self.executor = ActionPlanExecutor(manager_applier, context) + self.executor = default.DefaultActionPlanExecutor(manager_applier, + context) def execute(self, action_plan_uuid): - action_plan = ActionPlan.get_by_uuid(self.context, action_plan_uuid) + action_plan = objects.ActionPlan.get_by_uuid(self.context, + action_plan_uuid) # todo(jed) remove direct access to dbapi need filter in object - actions = Action.dbapi.get_action_list(self.context, - filters={ - 'action_plan_id': - action_plan.id}) + filters = {'action_plan_id': action_plan.id} + actions = objects.Action.dbapi.get_action_list(self.context, filters) return self.executor.execute(actions) diff --git a/watcher/applier/execution/base.py b/watcher/applier/execution/base.py new file mode 100644 index 000000000..bb147bbe7 --- /dev/null +++ b/watcher/applier/execution/base.py @@ -0,0 +1,62 @@ +# -*- encoding: utf-8 -*- +# Copyright (c) 2016 b<>com +# +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import abc + +import six + +from watcher.applier.messaging import events +from watcher.applier.primitives import factory +from watcher.common.messaging.events import event +from watcher import objects + + +@six.add_metaclass(abc.ABCMeta) +class BaseActionPlanExecutor(object): + def __init__(self, manager_applier, context): + self._manager_applier = manager_applier + self._context = context + self._action_factory = factory.ActionFactory() + + @property + def context(self): + return self._context + + @property + def manager_applier(self): + return self._manager_applier + + @property + def action_factory(self): + return self._action_factory + + def notify(self, action, state): + db_action = objects.Action.get_by_uuid(self.context, action.uuid) + db_action.state = state + db_action.save() + ev = event.Event() + ev.type = events.Events.LAUNCH_ACTION + ev.data = {} + payload = {'action_uuid': action.uuid, + 'action_state': state} + self.manager_applier.topic_status.publish_event(ev.type.name, + payload) + + @abc.abstractmethod + def execute(self, actions): + raise NotImplementedError() diff --git a/watcher/applier/execution/default.py b/watcher/applier/execution/default.py new file mode 100644 index 000000000..43b18dcd7 --- /dev/null +++ b/watcher/applier/execution/default.py @@ -0,0 +1,57 @@ +# -*- encoding: utf-8 -*- +# Copyright (c) 2015 b<>com +# +# Authors: Jean-Emile DARTOIS +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +from oslo_log import log + +from watcher._i18n import _LE +from watcher.applier.execution import base +from watcher.applier.execution import deploy_phase +from watcher.objects import action_plan + +LOG = log.getLogger(__name__) + + +class DefaultActionPlanExecutor(base.BaseActionPlanExecutor): + def __init__(self, manager_applier, context): + super(DefaultActionPlanExecutor, self).__init__(manager_applier, + context) + self.deploy = deploy_phase.DeployPhase(self) + + def execute(self, actions): + for action in actions: + try: + self.notify(action, action_plan.Status.ONGOING) + loaded_action = self.action_factory.make_action(action) + result = self.deploy.execute_primitive(loaded_action) + if result is False: + self.notify(action, action_plan.Status.FAILED) + self.deploy.rollback() + return False + else: + self.deploy.populate(loaded_action) + self.notify(action, action_plan.Status.SUCCEEDED) + except Exception as e: + LOG.expection(e) + LOG.debug('The ActionPlanExecutor failed to execute the action' + ' %s ', action) + + LOG.error(_LE("Trigger a rollback")) + self.notify(action, action_plan.Status.FAILED) + self.deploy.rollback() + return False + return True diff --git a/watcher/applier/execution/executor.py b/watcher/applier/execution/executor.py deleted file mode 100644 index 16b977357..000000000 --- a/watcher/applier/execution/executor.py +++ /dev/null @@ -1,76 +0,0 @@ -# -*- encoding: utf-8 -*- -# Copyright (c) 2015 b<>com -# -# Authors: Jean-Emile DARTOIS -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or -# implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -from oslo_log import log -from watcher.applier.execution.deploy_phase import DeployPhase -from watcher.applier.mapping.default import DefaultActionMapper -from watcher.applier.messaging.events import Events -from watcher.common.messaging.events.event import Event -from watcher.objects import Action -from watcher.objects.action_plan import Status - -LOG = log.getLogger(__name__) - - -class ActionPlanExecutor(object): - def __init__(self, manager_applier, context): - self.manager_applier = manager_applier - self.context = context - self.deploy = DeployPhase(self) - self.mapper = DefaultActionMapper() - - def get_primitive(self, action): - return self.mapper.build_primitive_from_action(action) - - def notify(self, action, state): - db_action = Action.get_by_uuid(self.context, action.uuid) - db_action.state = state - db_action.save() - event = Event() - event.type = Events.LAUNCH_ACTION - event.data = {} - payload = {'action_uuid': action.uuid, - 'action_state': state} - self.manager_applier.topic_status.publish_event(event.type.name, - payload) - - def execute(self, actions): - for action in actions: - try: - self.notify(action, Status.ONGOING) - primitive = self.get_primitive(action) - result = self.deploy.execute_primitive(primitive) - if result is False: - self.notify(action, Status.FAILED) - self.deploy.rollback() - return False - else: - self.deploy.populate(primitive) - self.notify(action, Status.SUCCEEDED) - except Exception as e: - LOG.debug( - 'The applier module failed to execute the action{0} with ' - 'the exception {1} '.format( - action, - unicode(e))) - - LOG.error("Trigger a rollback") - self.notify(action, Status.FAILED) - self.deploy.rollback() - return False - return True diff --git a/watcher/applier/mapping/__init__.py b/watcher/applier/mapping/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/watcher/applier/mapping/base.py b/watcher/applier/mapping/base.py deleted file mode 100644 index 99f92e70b..000000000 --- a/watcher/applier/mapping/base.py +++ /dev/null @@ -1,33 +0,0 @@ -# -*- encoding: utf-8 -*- -# Copyright (c) 2015 b<>com -# -# Authors: Jean-Emile DARTOIS -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or -# implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -import abc -import six - - -@six.add_metaclass(abc.ABCMeta) -class BaseActionMapper(object): - @abc.abstractmethod - def build_primitive_from_action(self, action): - """Transform an action to a primitive - - :type action: watcher.decision_engine.action.BaseAction - :return: the associated Primitive - """ - raise NotImplementedError() diff --git a/watcher/applier/mapping/default.py b/watcher/applier/mapping/default.py deleted file mode 100644 index 65bd7b259..000000000 --- a/watcher/applier/mapping/default.py +++ /dev/null @@ -1,47 +0,0 @@ -# -*- encoding: utf-8 -*- -# Copyright (c) 2015 b<>com -# -# Authors: Jean-Emile DARTOIS -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or -# implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -from watcher.applier.mapping.base import BaseActionMapper -from watcher.applier.primitives.change_nova_service_state import \ - ChangeNovaServiceState -from watcher.applier.primitives.migration import Migrate -from watcher.applier.primitives.nop import Nop -from watcher.applier.primitives.power_state import ChangePowerState -from watcher.common.exception import ActionNotFound -from watcher.decision_engine.planner.default import Primitives - - -class DefaultActionMapper(BaseActionMapper): - def build_primitive_from_action(self, action): - if action.action_type == Primitives.COLD_MIGRATE.value: - return Migrate(action.applies_to, Primitives.COLD_MIGRATE, - action.src, - action.dst) - elif action.action_type == Primitives.LIVE_MIGRATE.value: - return Migrate(action.applies_to, Primitives.COLD_MIGRATE, - action.src, - action.dst) - elif action.action_type == Primitives.HYPERVISOR_STATE.value: - return ChangeNovaServiceState(action.applies_to, action.parameter) - elif action.action_type == Primitives.POWER_STATE.value: - return ChangePowerState() - elif action.action_type == Primitives.NOP.value: - return Nop() - else: - raise ActionNotFound() diff --git a/watcher/applier/messaging/events.py b/watcher/applier/messaging/events.py index 2e186bf21..eb6de5c74 100644 --- a/watcher/applier/messaging/events.py +++ b/watcher/applier/messaging/events.py @@ -17,9 +17,9 @@ # limitations under the License. # -from enum import Enum +import enum -class Events(Enum): +class Events(enum.Enum): LAUNCH_ACTION_PLAN = "launch_action_plan" LAUNCH_ACTION = "launch_action" diff --git a/watcher/applier/primitives/base.py b/watcher/applier/primitives/base.py index 64eadcc7a..16507ba11 100644 --- a/watcher/applier/primitives/base.py +++ b/watcher/applier/primitives/base.py @@ -18,17 +18,38 @@ # import abc import six -from watcher.applier.promise import Promise + +from watcher.applier import promise @six.add_metaclass(abc.ABCMeta) class BasePrimitive(object): - @Promise + def __init__(self): + self._input_parameters = None + self._applies_to = None + + @property + def input_parameters(self): + return self._input_parameters + + @input_parameters.setter + def input_parameters(self, p): + self._input_parameters = p + + @property + def applies_to(self): + return self._applies_to + + @applies_to.setter + def applies_to(self, a): + self._applies_to = a + + @promise.Promise @abc.abstractmethod def execute(self): raise NotImplementedError() - @Promise + @promise.Promise @abc.abstractmethod def undo(self): raise NotImplementedError() diff --git a/watcher/applier/primitives/change_nova_service_state.py b/watcher/applier/primitives/change_nova_service_state.py index 6773a927c..c82a828f6 100644 --- a/watcher/applier/primitives/change_nova_service_state.py +++ b/watcher/applier/primitives/change_nova_service_state.py @@ -18,30 +18,21 @@ # -from oslo_config import cfg - - from watcher._i18n import _ -from watcher.applier.primitives.base import BasePrimitive -from watcher.applier.promise import Promise -from watcher.common.exception import IllegalArgumentException -from watcher.common.keystone import KeystoneClient -from watcher.common.nova import NovaClient -from watcher.decision_engine.model.hypervisor_state import HypervisorState - -CONF = cfg.CONF +from watcher.applier.primitives import base +from watcher.applier import promise +from watcher.common import exception +from watcher.common import keystone as kclient +from watcher.common import nova as nclient +from watcher.decision_engine.model import hypervisor_state as hstate -class ChangeNovaServiceState(BasePrimitive): - def __init__(self, host, state): - """This class allows us to change the state of nova-compute service. - - :param host: the uuid of the host - :param state: (enabled/disabled) - """ - super(BasePrimitive, self).__init__() - self._host = host - self._state = state +class ChangeNovaServiceState(base.BasePrimitive): + def __init__(self): + """This class allows us to change the state of nova-compute service.""" + super(ChangeNovaServiceState, self).__init__() + self._host = self.applies_to + self._state = self.input_parameters.get('state') @property def host(self): @@ -51,32 +42,32 @@ class ChangeNovaServiceState(BasePrimitive): def state(self): return self._state - @Promise + @promise.Promise def execute(self): target_state = None - if self.state == HypervisorState.OFFLINE.value: + if self.state == hstate.HypervisorState.OFFLINE.value: target_state = False - elif self.status == HypervisorState.ONLINE.value: + elif self.status == hstate.HypervisorState.ONLINE.value: target_state = True return self.nova_manage_service(target_state) - @Promise + @promise.Promise def undo(self): target_state = None - if self.state == HypervisorState.OFFLINE.value: + if self.state == hstate.HypervisorState.OFFLINE.value: target_state = True - elif self.state == HypervisorState.ONLINE.value: + elif self.state == hstate.HypervisorState.ONLINE.value: target_state = False return self.nova_manage_service(target_state) def nova_manage_service(self, state): if state is None: - raise IllegalArgumentException( + raise exception.IllegalArgumentException( _("The target state is not defined")) - keystone = KeystoneClient() - wrapper = NovaClient(keystone.get_credentials(), - session=keystone.get_session()) + keystone = kclient.KeystoneClient() + wrapper = nclient.NovaClient(keystone.get_credentials(), + session=keystone.get_session()) if state is True: return wrapper.enable_service_nova_compute(self.host) else: diff --git a/watcher/applier/primitives/factory.py b/watcher/applier/primitives/factory.py new file mode 100644 index 000000000..40c67d163 --- /dev/null +++ b/watcher/applier/primitives/factory.py @@ -0,0 +1,36 @@ +# -*- encoding: utf-8 -*- +# Copyright (c) 2016 b<>com +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +from __future__ import unicode_literals + +from oslo_log import log + +from watcher.applier.primitives.loading import default + +LOG = log.getLogger(__name__) + + +class ActionFactory(object): + def __init__(self): + self.action_loader = default.DefaultActionLoader() + + def make_action(self, object_action): + LOG.debug("Creating instance of %s", object_action.action_type) + loaded_action = self.action_loader.load(name=object_action.action_type) + loaded_action.input_parameters = object_action.input_parameters + loaded_action.applies_to = object_action.applies_to + return loaded_action diff --git a/watcher/applier/primitives/migration.py b/watcher/applier/primitives/migration.py index 34e54b8a6..17f07f974 100644 --- a/watcher/applier/primitives/migration.py +++ b/watcher/applier/primitives/migration.py @@ -17,50 +17,38 @@ # limitations under the License. # -from oslo_config import cfg -from watcher.applier.primitives.base import BasePrimitive -from watcher.applier.promise import Promise -from watcher.common.keystone import KeystoneClient -from watcher.common.nova import NovaClient -from watcher.decision_engine.planner.default import Primitives - -CONF = cfg.CONF +from watcher.applier.primitives import base +from watcher.applier import promise +from watcher.common import exception +from watcher.common import keystone as kclient +from watcher.common import nova as nclient -class Migrate(BasePrimitive): - def __init__(self, vm_uuid=None, - migration_type=None, - source_hypervisor=None, - destination_hypervisor=None): - super(BasePrimitive, self).__init__() - self.instance_uuid = vm_uuid - self.migration_type = migration_type - self.source_hypervisor = source_hypervisor - self.destination_hypervisor = destination_hypervisor +class Migrate(base.BasePrimitive): + def __init__(self): + super(Migrate, self).__init__() + self.instance_uuid = self.applies_to + self.migration_type = self.input_parameters.get('migration_type') def migrate(self, destination): - keystone = KeystoneClient() - wrapper = NovaClient(keystone.get_credentials(), - session=keystone.get_session()) + keystone = kclient.KeystoneClient() + wrapper = nclient.NovaClient(keystone.get_credentials(), + session=keystone.get_session()) instance = wrapper.find_instance(self.instance_uuid) if instance: - # todo(jed) remove Primitves - if self.migration_type is Primitives.COLD_MIGRATE: + if self.migration_type is 'live': return wrapper.live_migrate_instance( - instance_id=self.instance_uuid, - dest_hostname=destination, - block_migration=True) - elif self.migration_type is Primitives.LIVE_MIGRATE: - return wrapper.live_migrate_instance( - instance_id=self.instance_uuid, - dest_hostname=destination, - block_migration=False) + instance_id=self.instance_uuid, dest_hostname=destination) + else: + raise exception.InvalidParameterValue(err=self.migration_type) + else: + raise exception.InstanceNotFound(name=self.instance_uuid) - @Promise + @promise.Promise def execute(self): - return self.migrate(self.destination_hypervisor) + return self.migrate(self.input_parameters.get('dst_hypervisor_uuid')) - @Promise + @promise.Promise def undo(self): - return self.migrate(self.source_hypervisor) + return self.migrate(self.input_parameters.get('src_hypervisor_uuid')) diff --git a/watcher/applier/primitives/nop.py b/watcher/applier/primitives/nop.py index 000dd2d3a..d0e3ee4ff 100644 --- a/watcher/applier/primitives/nop.py +++ b/watcher/applier/primitives/nop.py @@ -20,21 +20,22 @@ from oslo_log import log -from watcher.applier.primitives.base import BasePrimitive -from watcher.applier.promise import Promise +from watcher.applier.primitives import base +from watcher.applier import promise LOG = log.getLogger(__name__) -class Nop(BasePrimitive): +class Nop(base.BasePrimitive): - @Promise + @promise.Promise def execute(self): - LOG.debug("executing NOP command") + LOG.debug("executing action NOP message:%s ", + self.input_parameters.get('message')) return True - @Promise + @promise.Promise def undo(self): - LOG.debug("undo NOP command") + LOG.debug("undo action NOP") return True diff --git a/watcher/applier/primitives/power_state.py b/watcher/applier/primitives/power_state.py deleted file mode 100644 index 178a7c941..000000000 --- a/watcher/applier/primitives/power_state.py +++ /dev/null @@ -1,32 +0,0 @@ -# -*- encoding: utf-8 -*- -# Copyright (c) 2015 b<>com -# -# Authors: Jean-Emile DARTOIS -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or -# implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -from watcher.applier.primitives.base import BasePrimitive -from watcher.applier.promise import Promise - - -class ChangePowerState(BasePrimitive): - - @Promise - def execute(self): - raise NotImplementedError # pragma:no cover - - @Promise - def undo(self): - raise NotImplementedError # pragma:no cover diff --git a/watcher/common/exception.py b/watcher/common/exception.py index 651e595a2..2eaa82413 100644 --- a/watcher/common/exception.py +++ b/watcher/common/exception.py @@ -291,12 +291,12 @@ class ClusterStateNotDefined(WatcherException): # Model -class VMNotFound(WatcherException): - message = _("The VM could not be found") +class InstanceNotFound(WatcherException): + message = _("The instance '%(name)s' is not found") class HypervisorNotFound(WatcherException): - message = _("The hypervisor could not be found") + message = _("The hypervisor is not found") class LoadingError(WatcherException): diff --git a/watcher/decision_engine/model/model_root.py b/watcher/decision_engine/model/model_root.py index f9aaa49e8..7ae764328 100644 --- a/watcher/decision_engine/model/model_root.py +++ b/watcher/decision_engine/model/model_root.py @@ -66,7 +66,7 @@ class ModelRoot(object): def get_vm_from_id(self, uuid): if str(uuid) not in self._vms.keys(): - raise exception.VMNotFound(uuid) + raise exception.InstanceNotFound(name=uuid) return self._vms[str(uuid)] def get_all_vms(self): diff --git a/watcher/decision_engine/planner/default.py b/watcher/decision_engine/planner/default.py index d372591d5..1d37ca426 100644 --- a/watcher/decision_engine/planner/default.py +++ b/watcher/decision_engine/planner/default.py @@ -17,9 +17,6 @@ # limitations under the License. # -import json - -import enum from oslo_log import log from watcher._i18n import _LW @@ -30,14 +27,6 @@ from watcher import objects LOG = log.getLogger(__name__) -class Primitives(enum.Enum): - LIVE_MIGRATE = 'MIGRATE' - COLD_MIGRATE = 'MIGRATE' - POWER_STATE = 'POWERSTATE' - HYPERVISOR_STATE = 'HYPERVISOR_STATE' - NOP = 'NOP' - - class DefaultPlanner(base.BasePlanner): priorities = { 'nop': 0, @@ -56,7 +45,7 @@ class DefaultPlanner(base.BasePlanner): 'action_plan_id': int(action_plan_id), 'action_type': action_type, 'applies_to': applies_to, - 'input_parameters': json.dumps(input_parameters), + 'input_parameters': input_parameters, 'state': objects.action.Status.PENDING, 'alarm': None, 'next': None, diff --git a/watcher/decision_engine/planner/manager.py b/watcher/decision_engine/planner/manager.py index b266718e1..c7946f35e 100644 --- a/watcher/decision_engine/planner/manager.py +++ b/watcher/decision_engine/planner/manager.py @@ -49,5 +49,5 @@ class PlannerManager(object): def load(self): selected_planner = CONF.watcher_planner.planner - LOG.debug("Loading {0}".format(selected_planner)) + LOG.debug("Loading %s", selected_planner) return self.loader.load(name=selected_planner) diff --git a/watcher/decision_engine/strategy/strategies/outlet_temp_control.py b/watcher/decision_engine/strategy/strategies/outlet_temp_control.py index c8d62825d..00f9bf483 100644 --- a/watcher/decision_engine/strategy/strategies/outlet_temp_control.py +++ b/watcher/decision_engine/strategy/strategies/outlet_temp_control.py @@ -149,8 +149,8 @@ class OutletTempControl(BaseStrategy): LOG.info(_LE("VM not active, skipped: %s"), vm.uuid) continue - return (mig_src_hypervisor, vm) - except wexc.VMNotFound as e: + return mig_src_hypervisor, vm + except wexc.InstanceNotFound as e: LOG.info("VM not found Error: %s" % e.message) pass diff --git a/watcher/locale/fr/LC_MESSAGES/watcher.po b/watcher/locale/fr/LC_MESSAGES/watcher.po index 0fd8df22c..54ca4e7c6 100644 --- a/watcher/locale/fr/LC_MESSAGES/watcher.po +++ b/watcher/locale/fr/LC_MESSAGES/watcher.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: python-watcher 0.21.1.dev32\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2016-01-14 14:51+0100\n" +"POT-Creation-Date: 2016-01-15 10:25+0100\n" "PO-Revision-Date: 2015-12-11 15:42+0100\n" "Last-Translator: FULL NAME \n" "Language: fr\n" @@ -71,7 +71,11 @@ msgstr "" msgid "Error parsing HTTP response: %s" msgstr "" -#: watcher/applier/primitives/change_nova_service_state.py:75 +#: watcher/applier/execution/default.py:52 +msgid "Trigger a rollback" +msgstr "" + +#: watcher/applier/primitives/change_nova_service_state.py:66 msgid "The target state is not defined" msgstr "" @@ -261,11 +265,12 @@ msgid "the cluster state is not defined" msgstr "" #: watcher/common/exception.py:295 -msgid "The VM could not be found" -msgstr "" +#, python-format +msgid "The instance '%(name)s' is not found" +msgstr "L'instance '%(name)s' n'a pas été trouvée" #: watcher/common/exception.py:299 -msgid "The hypervisor could not be found" +msgid "The hypervisor is not found" msgstr "" #: watcher/common/exception.py:303 @@ -348,7 +353,7 @@ msgstr "" msgid "'obj' argument type is not valid" msgstr "" -#: watcher/decision_engine/planner/default.py:86 +#: watcher/decision_engine/planner/default.py:75 msgid "The action plan is empty" msgstr "" @@ -536,3 +541,9 @@ msgstr "" #~ msgid "The Meta-Action could not be found" #~ msgstr "" +#~ msgid "The VM could not be found" +#~ msgstr "" + +#~ msgid "The hypervisor could not be found" +#~ msgstr "" + diff --git a/watcher/locale/watcher.pot b/watcher/locale/watcher.pot index 719dbacf0..b51c39576 100644 --- a/watcher/locale/watcher.pot +++ b/watcher/locale/watcher.pot @@ -7,9 +7,9 @@ #, fuzzy msgid "" msgstr "" -"Project-Id-Version: python-watcher 0.22.1.dev16\n" +"Project-Id-Version: python-watcher 0.22.1.dev19\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2016-01-14 14:51+0100\n" +"POT-Creation-Date: 2016-01-15 10:25+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -70,7 +70,11 @@ msgstr "" msgid "Error parsing HTTP response: %s" msgstr "" -#: watcher/applier/primitives/change_nova_service_state.py:75 +#: watcher/applier/execution/default.py:52 +msgid "Trigger a rollback" +msgstr "" + +#: watcher/applier/primitives/change_nova_service_state.py:66 msgid "The target state is not defined" msgstr "" @@ -259,11 +263,12 @@ msgid "the cluster state is not defined" msgstr "" #: watcher/common/exception.py:295 -msgid "The VM could not be found" +#, python-format +msgid "The instance '%(name)s' is not found" msgstr "" #: watcher/common/exception.py:299 -msgid "The hypervisor could not be found" +msgid "The hypervisor is not found" msgstr "" #: watcher/common/exception.py:303 @@ -346,7 +351,7 @@ msgstr "" msgid "'obj' argument type is not valid" msgstr "" -#: watcher/decision_engine/planner/default.py:86 +#: watcher/decision_engine/planner/default.py:75 msgid "The action plan is empty" msgstr "" diff --git a/watcher/tests/applier/execution/test_action_plan_executor.py b/watcher/tests/applier/execution/test_default_action_plan_executor.py similarity index 65% rename from watcher/tests/applier/execution/test_action_plan_executor.py rename to watcher/tests/applier/execution/test_default_action_plan_executor.py index b503dd7ce..99588d70c 100644 --- a/watcher/tests/applier/execution/test_action_plan_executor.py +++ b/watcher/tests/applier/execution/test_default_action_plan_executor.py @@ -18,21 +18,17 @@ # import mock -from watcher.applier.execution.executor import ActionPlanExecutor -from watcher import objects - +from watcher.applier.execution import default from watcher.common import utils -from watcher.decision_engine.planner.default import Primitives -from watcher.objects.action import Action -from watcher.objects.action import Status -from watcher.tests.db.base import DbTestCase +from watcher import objects +from watcher.tests.db import base -class TestCommandExecutor(DbTestCase): +class TestDefaultActionPlanExecutor(base.DbTestCase): def setUp(self): - super(TestCommandExecutor, self).setUp() - self.applier = mock.MagicMock() - self.executor = ActionPlanExecutor(self.applier, self.context) + super(TestDefaultActionPlanExecutor, self).setUp() + self.executor = default.DefaultActionPlanExecutor(mock.MagicMock(), + self.context) def test_execute(self): actions = mock.MagicMock() @@ -44,19 +40,17 @@ class TestCommandExecutor(DbTestCase): action = { 'uuid': utils.generate_uuid(), 'action_plan_id': 0, - 'action_type': Primitives.NOP.value, + 'action_type': "nop", 'applies_to': '', - 'src': '', - 'dst': '', - 'parameter': '', - 'description': '', - 'state': Status.PENDING, + 'input_parameters': {'state': 'OFFLINE'}, + 'state': objects.action.Status.PENDING, 'alarm': None, 'next': None, } new_action = objects.Action(self.context, **action) new_action.create(self.context) new_action.save() - actions.append(Action.get_by_uuid(self.context, action['uuid'])) + actions.append(objects.Action.get_by_uuid(self.context, + action['uuid'])) result = self.executor.execute(actions) self.assertEqual(result, True) diff --git a/watcher/tests/applier/mapping/__init__.py b/watcher/tests/applier/mapping/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/watcher/tests/applier/mapping/test_action_mapper.py b/watcher/tests/applier/mapping/test_action_mapper.py deleted file mode 100644 index 4f97c2947..000000000 --- a/watcher/tests/applier/mapping/test_action_mapper.py +++ /dev/null @@ -1,59 +0,0 @@ -# -*- encoding: utf-8 -*- -# Copyright (c) 2015 b<>com -# -# Authors: Jean-Emile DARTOIS -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or -# implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -import mock - -from watcher.applier.mapping.default import DefaultActionMapper -from watcher.decision_engine.planner.default import Primitives -from watcher.tests import base - - -class TestDefaultActionMapper(base.TestCase): - def setUp(self): - super(TestDefaultActionMapper, self).setUp() - self.mapper = DefaultActionMapper() - - def test_build_command_cold(self): - action = mock.MagicMock() - action.action_type = Primitives.COLD_MIGRATE.value - cmd = self.mapper.build_primitive_from_action(action) - self.assertIsNotNone(cmd) - - def test_build_command_live(self): - action = mock.MagicMock() - action.action_type = Primitives.LIVE_MIGRATE.value - cmd = self.mapper.build_primitive_from_action(action) - self.assertIsNotNone(cmd) - - def test_build_command_h_s(self): - action = mock.MagicMock() - action.action_type = Primitives.HYPERVISOR_STATE.value - cmd = self.mapper.build_primitive_from_action(action) - self.assertIsNotNone(cmd) - - def test_build_command_p_s(self): - action = mock.MagicMock() - action.action_type = Primitives.POWER_STATE.value - cmd = self.mapper.build_primitive_from_action(action) - self.assertIsNotNone(cmd) - - def test_build_command_exception_attribute(self): - action = mock.MagicMock - self.assertRaises(AttributeError, - self.mapper.build_primitive_from_action, - action) diff --git a/watcher/tests/applier/mapping/test_default_action_mapper.py b/watcher/tests/applier/mapping/test_default_action_mapper.py deleted file mode 100644 index 4f97c2947..000000000 --- a/watcher/tests/applier/mapping/test_default_action_mapper.py +++ /dev/null @@ -1,59 +0,0 @@ -# -*- encoding: utf-8 -*- -# Copyright (c) 2015 b<>com -# -# Authors: Jean-Emile DARTOIS -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or -# implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -import mock - -from watcher.applier.mapping.default import DefaultActionMapper -from watcher.decision_engine.planner.default import Primitives -from watcher.tests import base - - -class TestDefaultActionMapper(base.TestCase): - def setUp(self): - super(TestDefaultActionMapper, self).setUp() - self.mapper = DefaultActionMapper() - - def test_build_command_cold(self): - action = mock.MagicMock() - action.action_type = Primitives.COLD_MIGRATE.value - cmd = self.mapper.build_primitive_from_action(action) - self.assertIsNotNone(cmd) - - def test_build_command_live(self): - action = mock.MagicMock() - action.action_type = Primitives.LIVE_MIGRATE.value - cmd = self.mapper.build_primitive_from_action(action) - self.assertIsNotNone(cmd) - - def test_build_command_h_s(self): - action = mock.MagicMock() - action.action_type = Primitives.HYPERVISOR_STATE.value - cmd = self.mapper.build_primitive_from_action(action) - self.assertIsNotNone(cmd) - - def test_build_command_p_s(self): - action = mock.MagicMock() - action.action_type = Primitives.POWER_STATE.value - cmd = self.mapper.build_primitive_from_action(action) - self.assertIsNotNone(cmd) - - def test_build_command_exception_attribute(self): - action = mock.MagicMock - self.assertRaises(AttributeError, - self.mapper.build_primitive_from_action, - action) diff --git a/watcher/tests/decision_engine/model/test_model.py b/watcher/tests/decision_engine/model/test_model.py index fb6e43bf7..8bbfce7af 100644 --- a/watcher/tests/decision_engine/model/test_model.py +++ b/watcher/tests/decision_engine/model/test_model.py @@ -129,7 +129,7 @@ class TestModel(base.BaseTestCase): def test_vm_from_id_raise(self): fake_cluster = FakerModelCollector() model = fake_cluster.generate_scenario_1() - self.assertRaises(exception.VMNotFound, + self.assertRaises(exception.InstanceNotFound, model.get_vm_from_id, "valeur_qcq") def test_assert_vm_raise(self): diff --git a/watcher/tests/decision_engine/planner/test_default_planner_loader.py b/watcher/tests/decision_engine/planner/test_default_planner_loader.py index 955fbdcbf..1b9e9bd3c 100644 --- a/watcher/tests/decision_engine/planner/test_default_planner_loader.py +++ b/watcher/tests/decision_engine/planner/test_default_planner_loader.py @@ -20,7 +20,9 @@ from watcher.tests import base class TestDefaultPlannerLoader(base.TestCase): - loader = default.DefaultPlannerLoader() + def setUp(self): + super(TestDefaultPlannerLoader, self).setUp() + self.loader = default.DefaultPlannerLoader() def test_endpoints(self): for endpoint in self.loader.list_available(): diff --git a/watcher/tests/decision_engine/planner/test_planner_manager.py b/watcher/tests/decision_engine/planner/test_planner_manager.py index 516502afb..845627237 100644 --- a/watcher/tests/decision_engine/planner/test_planner_manager.py +++ b/watcher/tests/decision_engine/planner/test_planner_manager.py @@ -16,13 +16,13 @@ from oslo_config import cfg -from watcher.decision_engine.planner.default import DefaultPlanner -from watcher.decision_engine.planner.manager import PlannerManager +from watcher.decision_engine.planner import default +from watcher.decision_engine.planner import manager as planner from watcher.tests import base class TestPlannerManager(base.TestCase): def test_load(self): cfg.CONF.set_override('planner', "default", group='watcher_planner') - manager = PlannerManager() - self.assertIsInstance(manager.load(), DefaultPlanner) + manager = planner.PlannerManager() + self.assertIsInstance(manager.load(), default.DefaultPlanner)