diff --git a/setup.cfg b/setup.cfg index ac5d502e3..3f63aa86f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -45,6 +45,13 @@ tempest.test_plugins = watcher.database.migration_backend = sqlalchemy = watcher.db.sqlalchemy.migration +watcher_goals = + unclassified = watcher.decision_engine.goal.goals:Unclassified + dummy = watcher.decision_engine.goal.goals:Dummy + server_consolidation = watcher.decision_engine.goal.goals:ServerConsolidation + thermal_optimization = watcher.decision_engine.goal.goals:ThermalOptimization + workload_balancing = watcher.decision_engine.goal.goals:WorkloadBalancing + watcher_strategies = dummy = watcher.decision_engine.strategy.strategies.dummy_strategy:DummyStrategy basic = watcher.decision_engine.strategy.strategies.basic_consolidation:BasicConsolidation diff --git a/watcher/decision_engine/goal/__init__.py b/watcher/decision_engine/goal/__init__.py new file mode 100644 index 000000000..69c63d6da --- /dev/null +++ b/watcher/decision_engine/goal/__init__.py @@ -0,0 +1,26 @@ +# -*- 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 watcher.decision_engine.goal import goals + +Dummy = goals.Dummy +ServerConsolidation = goals.ServerConsolidation +ThermalOptimization = goals.ThermalOptimization +Unclassified = goals.Unclassified +WorkloadBalancing = goals.WorkloadBalancing + +__all__ = ("Dummy", "ServerConsolidation", "ThermalOptimization", + "Unclassified", "WorkloadBalancing", ) diff --git a/watcher/decision_engine/goal/base.py b/watcher/decision_engine/goal/base.py new file mode 100644 index 000000000..54c5a672a --- /dev/null +++ b/watcher/decision_engine/goal/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 oslo_log import log + +from watcher.common.loader import loadable + +LOG = log.getLogger(__name__) + + +@six.add_metaclass(abc.ABCMeta) +class Goal(loadable.Loadable): + + def __init__(self, config): + super(Goal, self).__init__(config) + self.name = self.get_name() + self.display_name = self.get_display_name() + + @classmethod + @abc.abstractmethod + def get_name(cls): + """Name of the goal: should be identical to the related entry point""" + raise NotImplementedError() + + @classmethod + @abc.abstractmethod + def get_display_name(cls): + """The goal display name for the goal""" + raise NotImplementedError() + + @classmethod + @abc.abstractmethod + def get_translatable_display_name(cls): + """The translatable msgid of the goal""" + # Note(v-francoise): Defined here to be used as the translation key for + # other services + raise NotImplementedError() + + @classmethod + def get_config_opts(cls): + """Defines the configuration options to be associated to this loadable + + :return: A list of configuration options relative to this Loadable + :rtype: list of :class:`oslo_config.cfg.Opt` instances + """ + return [] diff --git a/watcher/decision_engine/goal/goals.py b/watcher/decision_engine/goal/goals.py new file mode 100644 index 000000000..8ea0bb573 --- /dev/null +++ b/watcher/decision_engine/goal/goals.py @@ -0,0 +1,93 @@ +# -*- 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 watcher._i18n import _ +from watcher.decision_engine.goal import base + + +class Dummy(base.Goal): + + @classmethod + def get_name(cls): + return "dummy" + + @classmethod + def get_display_name(cls): + return _("Dummy goal") + + @classmethod + def get_translatable_display_name(cls): + return "Dummy goal" + + +class Unclassified(base.Goal): + + @classmethod + def get_name(cls): + return "unclassified" + + @classmethod + def get_display_name(cls): + return _("Unclassified") + + @classmethod + def get_translatable_display_name(cls): + return "Unclassified" + + +class ServerConsolidation(base.Goal): + + @classmethod + def get_name(cls): + return "server_consolidation" + + @classmethod + def get_display_name(cls): + return _("Server consolidation") + + @classmethod + def get_translatable_display_name(cls): + return "Server consolidation" + + +class ThermalOptimization(base.Goal): + + @classmethod + def get_name(cls): + return "thermal_optimization" + + @classmethod + def get_display_name(cls): + return _("Thermal optimization") + + @classmethod + def get_translatable_display_name(cls): + return "Thermal optimization" + + +class WorkloadBalancing(base.Goal): + + @classmethod + def get_name(cls): + return "workload_balancing" + + @classmethod + def get_display_name(cls): + return _("Workload balancing") + + @classmethod + def get_translatable_display_name(cls): + return "Workload balancing" diff --git a/watcher/decision_engine/strategy/loading/__init__.py b/watcher/decision_engine/loading/__init__.py similarity index 100% rename from watcher/decision_engine/strategy/loading/__init__.py rename to watcher/decision_engine/loading/__init__.py diff --git a/watcher/decision_engine/strategy/loading/default.py b/watcher/decision_engine/loading/default.py similarity index 85% rename from watcher/decision_engine/strategy/loading/default.py rename to watcher/decision_engine/loading/default.py index d44b893da..f7f29ca55 100644 --- a/watcher/decision_engine/strategy/loading/default.py +++ b/watcher/decision_engine/loading/default.py @@ -27,3 +27,9 @@ class DefaultStrategyLoader(default.DefaultLoader): def __init__(self): super(DefaultStrategyLoader, self).__init__( namespace='watcher_strategies') + + +class DefaultGoalLoader(default.DefaultLoader): + def __init__(self): + super(DefaultGoalLoader, self).__init__( + namespace='watcher_goals') diff --git a/watcher/decision_engine/strategy/selection/default.py b/watcher/decision_engine/strategy/selection/default.py index 213789482..505347879 100644 --- a/watcher/decision_engine/strategy/selection/default.py +++ b/watcher/decision_engine/strategy/selection/default.py @@ -19,7 +19,7 @@ from oslo_log import log from watcher._i18n import _ from watcher.common import exception -from watcher.decision_engine.strategy.loading import default +from watcher.decision_engine.loading import default from watcher.decision_engine.strategy.selection import base LOG = log.getLogger(__name__) diff --git a/watcher/decision_engine/strategy/strategies/base.py b/watcher/decision_engine/strategy/strategies/base.py index 583a243bd..680e0e470 100644 --- a/watcher/decision_engine/strategy/strategies/base.py +++ b/watcher/decision_engine/strategy/strategies/base.py @@ -42,6 +42,7 @@ import six from watcher._i18n import _ from watcher.common import clients from watcher.common.loader import loadable +from watcher.decision_engine.loading import default as loading from watcher.decision_engine.solution import default from watcher.decision_engine.strategy.common import level @@ -93,18 +94,10 @@ class BaseStrategy(loadable.Loadable): raise NotImplementedError() @classmethod - @abc.abstractmethod - def get_goal_display_name(cls): - """The translated display name related to the goal of the strategy""" - raise NotImplementedError() - - @classmethod - @abc.abstractmethod - def get_translatable_goal_display_name(cls): - """The translatable msgid related to the goal of the strategy""" - # Note(v-francoise): Defined here to be used as the translation key for - # other services - raise NotImplementedError() + def get_goal(cls): + """The goal the strategy achieves""" + goal_loader = loading.DefaultGoalLoader() + return goal_loader.load(cls.get_goal_name()) @classmethod def get_config_opts(cls): @@ -140,7 +133,7 @@ class BaseStrategy(loadable.Loadable): self._solution = s @property - def id(self): + def name(self): return self._name @property @@ -169,15 +162,7 @@ class DummyBaseStrategy(BaseStrategy): @classmethod def get_goal_name(cls): - return "DUMMY" - - @classmethod - def get_goal_display_name(cls): - return _("Dummy goal") - - @classmethod - def get_translatable_goal_display_name(cls): - return "Dummy goal" + return "dummy" @six.add_metaclass(abc.ABCMeta) @@ -192,15 +177,7 @@ class UnclassifiedStrategy(BaseStrategy): @classmethod def get_goal_name(cls): - return "UNCLASSIFIED" - - @classmethod - def get_goal_display_name(cls): - return _("Unclassified") - - @classmethod - def get_translatable_goal_display_name(cls): - return "Unclassified" + return "unclassified" @six.add_metaclass(abc.ABCMeta) @@ -208,15 +185,7 @@ class ServerConsolidationBaseStrategy(BaseStrategy): @classmethod def get_goal_name(cls): - return "SERVER_CONSOLIDATION" - - @classmethod - def get_goal_display_name(cls): - return _("Server consolidation") - - @classmethod - def get_translatable_goal_display_name(cls): - return "Server consolidation" + return "server_consolidation" @six.add_metaclass(abc.ABCMeta) @@ -224,15 +193,7 @@ class ThermalOptimizationBaseStrategy(BaseStrategy): @classmethod def get_goal_name(cls): - return "THERMAL_OPTIMIZATION" - - @classmethod - def get_goal_display_name(cls): - return _("Thermal optimization") - - @classmethod - def get_translatable_goal_display_name(cls): - return "Thermal optimization" + return "thermal_optimization" @six.add_metaclass(abc.ABCMeta) @@ -240,7 +201,7 @@ class WorkloadStabilizationBaseStrategy(BaseStrategy): @classmethod def get_goal_name(cls): - return "WORKLOAD_BALANCING" + return "workload_balancing" @classmethod def get_goal_display_name(cls): diff --git a/watcher/decision_engine/strategy/strategies/workload_balance.py b/watcher/decision_engine/strategy/strategies/workload_balance.py index 4f0338cdc..efaab1c86 100644 --- a/watcher/decision_engine/strategy/strategies/workload_balance.py +++ b/watcher/decision_engine/strategy/strategies/workload_balance.py @@ -18,7 +18,7 @@ # from oslo_log import log -from watcher._i18n import _LE, _LI, _LW +from watcher._i18n import _, _LE, _LI, _LW from watcher.common import exception as wexc from watcher.decision_engine.model import resource from watcher.decision_engine.model import vm_state diff --git a/watcher/decision_engine/strategy/strategies/workload_stabilization.py b/watcher/decision_engine/strategy/strategies/workload_stabilization.py index a26069ac2..b10fe8385 100644 --- a/watcher/decision_engine/strategy/strategies/workload_stabilization.py +++ b/watcher/decision_engine/strategy/strategies/workload_stabilization.py @@ -123,15 +123,15 @@ class WorkloadStabilization(base.WorkloadStabilizationBaseStrategy): @classmethod def get_name(cls): - return "WORKLOAD_BALANCING" + return "workload_stabilization" @classmethod def get_display_name(cls): - return _("Workload balancing") + return _("Workload stabilization") @classmethod def get_translatable_display_name(cls): - return "Workload balancing" + return "Workload stabilization" @property def ceilometer(self): diff --git a/watcher/decision_engine/sync.py b/watcher/decision_engine/sync.py index 3bb2a903b..9ee6d92c2 100644 --- a/watcher/decision_engine/sync.py +++ b/watcher/decision_engine/sync.py @@ -20,15 +20,19 @@ from oslo_log import log from watcher._i18n import _LE, _LI from watcher.common import context -from watcher.decision_engine.strategy.loading import default +from watcher.decision_engine.loading import default from watcher import objects LOG = log.getLogger(__name__) -GoalMapping = collections.namedtuple('GoalMapping', ['name', 'display_name']) +GoalMapping = collections.namedtuple( + 'GoalMapping', ['name', 'display_name']) StrategyMapping = collections.namedtuple( 'StrategyMapping', ['name', 'goal_name', 'display_name']) +IndicatorSpec = collections.namedtuple( + 'IndicatorSpec', ['name', 'description', 'unit', 'schema']) + class Syncer(object): """Syncs all available goals and strategies with the Watcher DB""" @@ -52,22 +56,26 @@ class Syncer(object): @property def available_goals(self): + """Goals loaded from DB""" if self._available_goals is None: self._available_goals = objects.Goal.list(self.ctx) return self._available_goals @property def available_strategies(self): + """Strategies loaded from DB""" if self._available_strategies is None: self._available_strategies = objects.Strategy.list(self.ctx) return self._available_strategies @property def available_goals_map(self): + """Mapping of goals loaded from DB""" if self._available_goals_map is None: self._available_goals_map = { GoalMapping( - name=g.name, display_name=g.display_name): g + name=g.name, + display_name=g.display_name): g for g in self.available_goals } return self._available_goals_map @@ -109,9 +117,7 @@ class Syncer(object): def _sync_goal(self, goal_map): goal_name = goal_map.name - goal_display_name = goal_map.display_name goal_mapping = dict() - # Goals that are matching by name with the given discovered goal name matching_goals = [g for g in self.available_goals if g.name == goal_name] @@ -120,7 +126,7 @@ class Syncer(object): if stale_goals or not matching_goals: goal = objects.Goal(self.ctx) goal.name = goal_name - goal.display_name = goal_display_name + goal.display_name = goal_map.display_name goal.create() LOG.info(_LI("Goal %s created"), goal_name) @@ -134,7 +140,6 @@ class Syncer(object): def _sync_strategy(self, strategy_map): strategy_name = strategy_map.name - strategy_display_name = strategy_map.display_name goal_name = strategy_map.goal_name strategy_mapping = dict() @@ -148,7 +153,7 @@ class Syncer(object): if stale_strategies or not matching_strategies: strategy = objects.Strategy(self.ctx) strategy.name = strategy_name - strategy.display_name = strategy_display_name + strategy.display_name = strategy_map.display_name strategy.goal_id = objects.Goal.get_by_name(self.ctx, goal_name).id strategy.create() LOG.info(_LI("Strategy %s created"), strategy_name) @@ -253,14 +258,18 @@ class Syncer(object): strategies_map = {} goals_map = {} discovered_map = {"goals": goals_map, "strategies": strategies_map} + goal_loader = default.DefaultGoalLoader() + implemented_goals = goal_loader.list_available() + strategy_loader = default.DefaultStrategyLoader() implemented_strategies = strategy_loader.list_available() - for _, strategy_cls in implemented_strategies.items(): - goals_map[strategy_cls.get_goal_name()] = GoalMapping( - name=strategy_cls.get_goal_name(), - display_name=strategy_cls.get_translatable_goal_display_name()) + for _, goal_cls in implemented_goals.items(): + goals_map[goal_cls.get_name()] = GoalMapping( + name=goal_cls.get_name(), + display_name=goal_cls.get_translatable_display_name()) + for _, strategy_cls in implemented_strategies.items(): strategies_map[strategy_cls.get_name()] = StrategyMapping( name=strategy_cls.get_name(), goal_name=strategy_cls.get_goal_name(), @@ -269,13 +278,21 @@ class Syncer(object): return discovered_map def _soft_delete_stale_goals(self, goal_map, matching_goals): - goal_name = goal_map.name + """Soft delete the stale goals + + :param goal_map: discovered goal map + :type goal_map: :py:class:`~.GoalMapping` instance + :param matching_goals: list of DB goals matching the goal_map + :type matching_goals: list of :py:class:`~.objects.Goal` instances + :returns: A list of soft deleted DB goals (subset of matching goals) + :rtype: list of :py:class:`~.objects.Goal` instances + """ goal_display_name = goal_map.display_name + goal_name = goal_map.name stale_goals = [] for matching_goal in matching_goals: - if (matching_goal.display_name == goal_display_name and - matching_goal.strategy_id not in self.strategy_mapping): + if matching_goal.display_name == goal_display_name: LOG.info(_LI("Goal %s unchanged"), goal_name) else: LOG.info(_LI("Goal %s modified"), goal_name) diff --git a/watcher/opts.py b/watcher/opts.py index e280523c4..37df1f3d4 100644 --- a/watcher/opts.py +++ b/watcher/opts.py @@ -25,10 +25,10 @@ from watcher.applier.workflow_engine.loading import default as \ workflow_engine_loader from watcher.common import clients from watcher.common import utils +from watcher.decision_engine.loading import default as strategy_loader from watcher.decision_engine import manager as decision_engine_manger from watcher.decision_engine.planner.loading import default as planner_loader from watcher.decision_engine.planner import manager as planner_manager -from watcher.decision_engine.strategy.loading import default as strategy_loader PLUGIN_LOADERS = ( diff --git a/watcher/tests/api/v1/test_audit_templates.py b/watcher/tests/api/v1/test_audit_templates.py index 2e2e5df41..60109561f 100644 --- a/watcher/tests/api/v1/test_audit_templates.py +++ b/watcher/tests/api/v1/test_audit_templates.py @@ -57,16 +57,16 @@ class FunctionalTestWithSetup(api_base.FunctionalTest): def setUp(self): super(FunctionalTestWithSetup, self).setUp() self.fake_goal1 = obj_utils.get_test_goal( - self.context, id=1, uuid=utils.generate_uuid(), name="DUMMY_1") + self.context, id=1, uuid=utils.generate_uuid(), name="dummy_1") self.fake_goal2 = obj_utils.get_test_goal( - self.context, id=2, uuid=utils.generate_uuid(), name="DUMMY_2") + self.context, id=2, uuid=utils.generate_uuid(), name="dummy_2") self.fake_goal1.create() self.fake_goal2.create() self.fake_strategy1 = obj_utils.get_test_strategy( - self.context, id=1, uuid=utils.generate_uuid(), name="STRATEGY_1", + self.context, id=1, uuid=utils.generate_uuid(), name="strategy_1", goal_id=self.fake_goal1.id) self.fake_strategy2 = obj_utils.get_test_strategy( - self.context, id=2, uuid=utils.generate_uuid(), name="STRATEGY_2", + self.context, id=2, uuid=utils.generate_uuid(), name="strategy_2", goal_id=self.fake_goal2.id) self.fake_strategy1.create() self.fake_strategy2.create() diff --git a/watcher/tests/decision_engine/audit/test_default_audit_handler.py b/watcher/tests/decision_engine/audit/test_default_audit_handler.py index 4ba20aa4c..005849822 100644 --- a/watcher/tests/decision_engine/audit/test_default_audit_handler.py +++ b/watcher/tests/decision_engine/audit/test_default_audit_handler.py @@ -28,7 +28,7 @@ from watcher.tests.objects import utils as obj_utils class TestDefaultAuditHandler(base.DbTestCase): def setUp(self): super(TestDefaultAuditHandler, self).setUp() - obj_utils.create_test_goal(self.context, id=1, name="DUMMY") + obj_utils.create_test_goal(self.context, id=1, name="dummy") audit_template = obj_utils.create_test_audit_template( self.context) self.audit = obj_utils.create_test_audit( diff --git a/watcher/tests/decision_engine/fake_goals.py b/watcher/tests/decision_engine/fake_goals.py new file mode 100644 index 000000000..e96bf26da --- /dev/null +++ b/watcher/tests/decision_engine/fake_goals.py @@ -0,0 +1,50 @@ +# -*- 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 watcher.decision_engine.goal import base as base_goal + + +class FakeGoal(base_goal.Goal): + + NAME = NotImplemented + DISPLAY_NAME = NotImplemented + + @classmethod + def get_name(cls): + return cls.NAME + + @classmethod + def get_display_name(cls): + return cls.DISPLAY_NAME + + @classmethod + def get_translatable_display_name(cls): + return cls.DISPLAY_NAME + + +class FakeDummy1(FakeGoal): + NAME = "dummy_1" + DISPLAY_NAME = "Dummy 1" + + +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/fake_strategies.py b/watcher/tests/decision_engine/fake_strategies.py index 7d24f055e..369d4fe61 100644 --- a/watcher/tests/decision_engine/fake_strategies.py +++ b/watcher/tests/decision_engine/fake_strategies.py @@ -23,10 +23,9 @@ CONF = cfg.CONF class FakeStrategy(base_strategy.BaseStrategy): - GOAL_NAME = NotImplemented - GOAL_DISPLAY_NAME = NotImplemented NAME = NotImplemented DISPLAY_NAME = NotImplemented + GOAL_NAME = NotImplemented @classmethod def get_name(cls): @@ -44,14 +43,6 @@ class FakeStrategy(base_strategy.BaseStrategy): def get_goal_name(cls): return cls.GOAL_NAME - @classmethod - def get_goal_display_name(cls): - return cls.GOAL_DISPLAY_NAME - - @classmethod - def get_translatable_goal_display_name(cls): - return cls.GOAL_DISPLAY_NAME - @classmethod def get_config_opts(cls): return [] @@ -61,9 +52,8 @@ class FakeStrategy(base_strategy.BaseStrategy): class FakeDummy1Strategy1(FakeStrategy): - GOAL_NAME = "DUMMY_1" - GOAL_DISPLAY_NAME = "Dummy 1" - NAME = "STRATEGY_1" + GOAL_NAME = "dummy_1" + NAME = "strategy_1" DISPLAY_NAME = "Strategy 1" @classmethod @@ -74,21 +64,18 @@ class FakeDummy1Strategy1(FakeStrategy): class FakeDummy1Strategy2(FakeStrategy): - GOAL_NAME = "DUMMY_1" - GOAL_DISPLAY_NAME = "Dummy 1" - NAME = "STRATEGY_2" + GOAL_NAME = "dummy_1" + NAME = "strategy_2" DISPLAY_NAME = "Strategy 2" class FakeDummy2Strategy3(FakeStrategy): - GOAL_NAME = "DUMMY_2" - GOAL_DISPLAY_NAME = "Dummy 2" - NAME = "STRATEGY_3" + GOAL_NAME = "dummy_2" + NAME = "strategy_3" DISPLAY_NAME = "Strategy 3" class FakeDummy2Strategy4(FakeStrategy): - GOAL_NAME = "DUMMY_2" - GOAL_DISPLAY_NAME = "Other Dummy 2" - NAME = "STRATEGY_4" + GOAL_NAME = "dummy_2" + NAME = "strategy_4" DISPLAY_NAME = "Strategy 4" diff --git a/watcher/tests/decision_engine/goal/__init__.py b/watcher/tests/decision_engine/goal/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/watcher/tests/decision_engine/goal/test_goal_loader.py b/watcher/tests/decision_engine/goal/test_goal_loader.py new file mode 100644 index 000000000..877be9231 --- /dev/null +++ b/watcher/tests/decision_engine/goal/test_goal_loader.py @@ -0,0 +1,78 @@ +# -*- encoding: utf-8 -*- +# Copyright (c) 2015 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 mock +from stevedore import extension + +from watcher.common import exception +from watcher.decision_engine.goal import goals +from watcher.decision_engine.loading import default as default_loading +from watcher.tests import base + + +class TestDefaultGoalLoader(base.TestCase): + + def setUp(self): + super(TestDefaultGoalLoader, self).setUp() + self.goal_loader = default_loading.DefaultGoalLoader() + + def test_load_goal_with_empty_model(self): + self.assertRaises( + exception.LoadingError, self.goal_loader.load, None) + + def test_goal_loader(self): + dummy_goal_name = "dummy" + # Set up the fake Stevedore extensions + fake_extmanager_call = extension.ExtensionManager.make_test_instance( + extensions=[extension.Extension( + name=dummy_goal_name, + entry_point="%s:%s" % ( + goals.Dummy.__module__, + goals.Dummy.__name__), + plugin=goals.Dummy, + obj=None, + )], + namespace="watcher_goals", + ) + + with mock.patch.object(extension, "ExtensionManager") as m_ext_manager: + m_ext_manager.return_value = fake_extmanager_call + loaded_goal = self.goal_loader.load("dummy") + + self.assertEqual("dummy", loaded_goal.name) + self.assertEqual("Dummy goal", loaded_goal.display_name) + + def test_load_dummy_goal(self): + goal_loader = default_loading.DefaultGoalLoader() + loaded_goal = goal_loader.load("dummy") + self.assertIsInstance(loaded_goal, goals.Dummy) + + +class TestLoadGoalsWithDefaultGoalLoader(base.TestCase): + + goal_loader = default_loading.DefaultGoalLoader() + + # test matrix (1 test execution per goal entry point) + scenarios = [ + (goal_name, + {"goal_name": goal_name, "goal_cls": goal_cls}) + for goal_name, goal_cls + in goal_loader.list_available().items()] + + def test_load_goals(self): + goal = self.goal_loader.load(self.goal_name) + self.assertIsNotNone(goal) + self.assertEqual(self.goal_cls.get_name(), goal.name) diff --git a/watcher/tests/decision_engine/strategy/loading/test_default_strategy_loader.py b/watcher/tests/decision_engine/strategy/loading/test_default_strategy_loader.py index 27ae55359..3248a8876 100644 --- a/watcher/tests/decision_engine/strategy/loading/test_default_strategy_loader.py +++ b/watcher/tests/decision_engine/strategy/loading/test_default_strategy_loader.py @@ -13,31 +13,25 @@ # implied. # See the License for the specific language governing permissions and # limitations under the License. -from __future__ import unicode_literals import mock from stevedore import extension from watcher.common import exception -from watcher.decision_engine.strategy.loading import default as default_loading +from watcher.decision_engine.loading import default as default_loading from watcher.decision_engine.strategy.strategies import dummy_strategy from watcher.tests import base class TestDefaultStrategyLoader(base.TestCase): - def test_load_strategy_with_empty_model(self): - strategy_loader = default_loading.DefaultStrategyLoader() - self.assertRaises(exception.LoadingError, strategy_loader.load, None) + def setUp(self): + super(TestDefaultStrategyLoader, self).setUp() + self.strategy_loader = default_loading.DefaultStrategyLoader() - def test_load_strategy_is_basic(self): - strategy_loader = default_loading.DefaultStrategyLoader() - expected_strategy = 'basic' - selected_strategy = strategy_loader.load(expected_strategy) - self.assertEqual( - selected_strategy.id, - expected_strategy, - 'The default strategy should be basic') + def test_load_strategy_with_empty_model(self): + self.assertRaises( + exception.LoadingError, self.strategy_loader.load, None) def test_strategy_loader(self): dummy_strategy_name = "dummy" @@ -56,10 +50,10 @@ class TestDefaultStrategyLoader(base.TestCase): with mock.patch.object(extension, "ExtensionManager") as m_ext_manager: m_ext_manager.return_value = fake_extmanager_call - strategy_loader = default_loading.DefaultStrategyLoader() - loaded_strategy = strategy_loader.load("dummy") + loaded_strategy = self.strategy_loader.load( + "dummy") - self.assertEqual("dummy", loaded_strategy.id) + self.assertEqual("dummy", loaded_strategy.name) self.assertEqual("Dummy strategy", loaded_strategy.display_name) def test_load_dummy_strategy(self): @@ -67,6 +61,18 @@ class TestDefaultStrategyLoader(base.TestCase): loaded_strategy = strategy_loader.load("dummy") self.assertIsInstance(loaded_strategy, dummy_strategy.DummyStrategy) - def test_endpoints(self): - for endpoint in strategy_loader.list_available(): - self.assertIsNotNone(self.strategy_loader.load(endpoint)) + +class TestLoadStrategiesWithDefaultStrategyLoader(base.TestCase): + + strategy_loader = default_loading.DefaultStrategyLoader() + + scenarios = [ + (strategy_name, + {"strategy_name": strategy_name, "strategy_cls": strategy_cls}) + for strategy_name, strategy_cls + in strategy_loader.list_available().items()] + + def test_load_strategies(self): + strategy = self.strategy_loader.load(self.strategy_name) + self.assertIsNotNone(strategy) + self.assertEqual(self.strategy_cls.get_name(), strategy.name) 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 0875f8786..e80f1499a 100644 --- a/watcher/tests/decision_engine/strategy/selector/test_strategy_selector.py +++ b/watcher/tests/decision_engine/strategy/selector/test_strategy_selector.py @@ -18,7 +18,7 @@ import mock from oslo_config import cfg from watcher.common import exception -from watcher.decision_engine.strategy.loading import default as default_loader +from watcher.decision_engine.loading import default as default_loader from watcher.decision_engine.strategy.selection import ( default as default_selector) from watcher.decision_engine.strategy import strategies @@ -34,7 +34,7 @@ class TestStrategySelector(base.TestCase): @mock.patch.object(default_loader.DefaultStrategyLoader, 'load') def test_select_with_strategy_name(self, m_load): - expected_goal = 'DUMMY' + expected_goal = 'dummy' expected_strategy = "dummy" strategy_selector = default_selector.DefaultStrategySelector( expected_goal, expected_strategy, osc=None) @@ -45,7 +45,7 @@ class TestStrategySelector(base.TestCase): @mock.patch.object(default_loader.DefaultStrategyLoader, 'list_available') def test_select_with_goal_name_only(self, m_list_available, m_load): m_list_available.return_value = {"dummy": strategies.DummyStrategy} - expected_goal = 'DUMMY' + expected_goal = 'dummy' expected_strategy = "dummy" strategy_selector = default_selector.DefaultStrategySelector( expected_goal, osc=None) @@ -54,13 +54,13 @@ class TestStrategySelector(base.TestCase): def test_select_non_existing_strategy(self): strategy_selector = default_selector.DefaultStrategySelector( - "DUMMY", "NOT_FOUND") + "dummy", "NOT_FOUND") self.assertRaises(exception.LoadingError, strategy_selector.select) @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") + "dummy") self.assertRaises(exception.NoAvailableStrategyForGoal, strategy_selector.select) diff --git a/watcher/tests/decision_engine/test_sync.py b/watcher/tests/decision_engine/test_sync.py index 5aae86a98..8471ea028 100644 --- a/watcher/tests/decision_engine/test_sync.py +++ b/watcher/tests/decision_engine/test_sync.py @@ -18,10 +18,11 @@ import mock from watcher.common import context from watcher.common import utils -from watcher.decision_engine.strategy.loading import default +from watcher.decision_engine.loading import default from watcher.decision_engine import sync from watcher import objects from watcher.tests.db import base +from watcher.tests.decision_engine import fake_goals from watcher.tests.decision_engine import fake_strategies @@ -43,12 +44,28 @@ class TestSyncer(base.DbTestCase): fake_strategies.FakeDummy2Strategy4, }) + self.m_available_goals = mock.Mock(return_value={ + fake_goals.FakeDummy1.get_name(): fake_goals.FakeDummy1, + fake_goals.FakeDummy2.get_name(): fake_goals.FakeDummy2, + }) + + p_goals_load = mock.patch.object( + default.DefaultGoalLoader, 'load', + side_effect=lambda goal: self.m_available_goals()[goal]()) + p_goals = mock.patch.object( + default.DefaultGoalLoader, 'list_available', + self.m_available_goals) p_strategies = mock.patch.object( default.DefaultStrategyLoader, 'list_available', self.m_available_strategies) + + p_goals.start() + p_goals_load.start() p_strategies.start() self.syncer = sync.Syncer() + self.addCleanup(p_goals.stop) + self.addCleanup(p_goals_load.stop) self.addCleanup(p_strategies.stop) @mock.patch.object(objects.Strategy, "soft_delete") @@ -94,7 +111,7 @@ class TestSyncer(base.DbTestCase): objects.Goal(self.ctx, id=i) for i in range(1, 10)] m_g_list.return_value = [ objects.Goal(self.ctx, id=1, uuid=utils.generate_uuid(), - name="DUMMY_1", display_name="Dummy 1") + name="dummy_1", display_name="Dummy 1",) ] m_s_list.return_value = [] @@ -124,10 +141,10 @@ class TestSyncer(base.DbTestCase): objects.Goal(self.ctx, id=i) for i in range(1, 10)] m_g_list.return_value = [ objects.Goal(self.ctx, id=1, uuid=utils.generate_uuid(), - name="DUMMY_1", display_name="Dummy 1") + name="dummy_1", display_name="Dummy 1",) ] m_s_list.return_value = [ - objects.Strategy(self.ctx, id=1, name="STRATEGY_1", + objects.Strategy(self.ctx, id=1, name="strategy_1", goal_id=1, display_name="Strategy 1") ] self.syncer.sync() @@ -154,10 +171,10 @@ class TestSyncer(base.DbTestCase): m_g_get_by_name, m_s_list, m_s_create, m_s_save, m_s_soft_delete): m_g_get_by_name.side_effect = [ objects.Goal(self.ctx, id=i) for i in range(1, 10)] - m_g_list.return_value = [ - objects.Goal(self.ctx, id=1, uuid=utils.generate_uuid(), - name="DUMMY_2", display_name="original") - ] + m_g_list.return_value = [objects.Goal( + self.ctx, id=1, uuid=utils.generate_uuid(), + name="dummy_2", display_name="original", + )] m_s_list.return_value = [] self.syncer.sync() @@ -185,10 +202,10 @@ class TestSyncer(base.DbTestCase): objects.Goal(self.ctx, id=i) for i in range(1, 10)] m_g_list.return_value = [ objects.Goal(self.ctx, id=1, uuid=utils.generate_uuid(), - name="DUMMY_1", display_name="Dummy 1") + name="dummy_1", display_name="Dummy 1",) ] m_s_list.return_value = [ - objects.Strategy(self.ctx, id=1, name="STRATEGY_1", + objects.Strategy(self.ctx, id=1, name="strategy_1", goal_id=1, display_name="original") ] self.syncer.sync() @@ -208,29 +225,32 @@ class TestSyncer(base.DbTestCase): # that were saved in DB # Should stay unmodified after sync() - goal1 = objects.Goal(self.ctx, id=1, uuid=utils.generate_uuid(), - name="DUMMY_1", display_name="Dummy 1") + goal1 = objects.Goal( + self.ctx, id=1, uuid=utils.generate_uuid(), + name="dummy_1", display_name="Dummy 1",) # Should be modified by the sync() - goal2 = objects.Goal(self.ctx, id=2, uuid=utils.generate_uuid(), - name="DUMMY_2", display_name="Original") + goal2 = objects.Goal( + self.ctx, id=2, uuid=utils.generate_uuid(), + name="dummy_2", display_name="Original", + ) goal1.create() goal2.create() # Should stay unmodified after sync() strategy1 = objects.Strategy( - self.ctx, id=1, name="STRATEGY_1", uuid=utils.generate_uuid(), + self.ctx, id=1, name="strategy_1", uuid=utils.generate_uuid(), display_name="Strategy 1", goal_id=goal1.id) # Should stay unmodified after sync() strategy2 = objects.Strategy( - self.ctx, id=2, name="STRATEGY_2", uuid=utils.generate_uuid(), + 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() strategy3 = objects.Strategy( - self.ctx, id=3, name="STRATEGY_3", uuid=utils.generate_uuid(), + self.ctx, id=3, name="strategy_3", uuid=utils.generate_uuid(), display_name="Original", goal_id=goal2.id) # Should be modified by the sync() strategy4 = objects.Strategy( - self.ctx, id=4, name="STRATEGY_4", uuid=utils.generate_uuid(), + self.ctx, id=4, name="strategy_4", uuid=utils.generate_uuid(), display_name="Original", goal_id=goal2.id) strategy1.create() strategy2.create() @@ -288,10 +308,10 @@ class TestSyncer(base.DbTestCase): self.assertEqual(4, len(after_strategies)) self.assertEqual(4, len(after_audit_templates)) self.assertEqual( - {"DUMMY_1", "DUMMY_2"}, + {"dummy_1", "dummy_2"}, set([g.name for g in after_goals])) self.assertEqual( - {"STRATEGY_1", "STRATEGY_2", "STRATEGY_3", "STRATEGY_4"}, + {"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 @@ -341,33 +361,39 @@ class TestSyncer(base.DbTestCase): def test_end2end_sync_goals_with_removed_goal_and_strategy(self): # ### Setup ### # - # We Simulate the fact that we removed 2 strategies - # as well as the DUMMY_2 goal + # We simulate the fact that we removed 2 strategies self.m_available_strategies.return_value = { fake_strategies.FakeDummy1Strategy1.get_name(): fake_strategies.FakeDummy1Strategy1 } - + # We simulate the fact that we removed the dummy_2 goal + self.m_available_goals.return_value = { + fake_goals.FakeDummy1.get_name(): fake_goals.FakeDummy1, + } # Should stay unmodified after sync() - goal1 = objects.Goal(self.ctx, id=1, uuid=utils.generate_uuid(), - name="DUMMY_1", display_name="Dummy 1") + goal1 = objects.Goal( + self.ctx, id=1, uuid=utils.generate_uuid(), + name="dummy_1", display_name="Dummy 1", + ) # To be removed by the sync() - goal2 = objects.Goal(self.ctx, id=2, uuid=utils.generate_uuid(), - name="DUMMY_2", display_name="Dummy 2") + goal2 = objects.Goal( + self.ctx, id=2, uuid=utils.generate_uuid(), + name="dummy_2", display_name="Dummy 2", + ) goal1.create() goal2.create() # Should stay unmodified after sync() strategy1 = objects.Strategy( - self.ctx, id=1, name="STRATEGY_1", uuid=utils.generate_uuid(), + 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() strategy2 = objects.Strategy( - self.ctx, id=2, name="STRATEGY_2", uuid=utils.generate_uuid(), + 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() strategy3 = objects.Strategy( - self.ctx, id=3, name="STRATEGY_3", uuid=utils.generate_uuid(), + self.ctx, id=3, name="strategy_3", uuid=utils.generate_uuid(), display_name="Original", goal_id=goal2.id) strategy1.create() strategy2.create() @@ -413,10 +439,10 @@ class TestSyncer(base.DbTestCase): self.assertEqual(1, len(after_strategies)) self.assertEqual(2, len(after_audit_templates)) self.assertEqual( - {"DUMMY_1"}, + {"dummy_1"}, set([g.name for g in after_goals])) self.assertEqual( - {"STRATEGY_1"}, + {"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]] diff --git a/watcher/tests/test_list_opts.py b/watcher/tests/test_list_opts.py index 305f2678a..7e376cbdf 100644 --- a/watcher/tests/test_list_opts.py +++ b/watcher/tests/test_list_opts.py @@ -74,7 +74,7 @@ class TestListOpts(base.TestCase): def test_list_opts_with_opts(self): expected_sections = self.base_sections + [ - 'watcher_strategies.STRATEGY_1'] + 'watcher_strategies.strategy_1'] # Set up the fake Stevedore extensions fake_extmanager_call = extension.ExtensionManager.make_test_instance( extensions=[extension.Extension( @@ -105,8 +105,7 @@ class TestListOpts(base.TestCase): self.assertTrue(len(options)) result_map = dict(result) - - strategy_opts = result_map['watcher_strategies.STRATEGY_1'] + strategy_opts = result_map['watcher_strategies.strategy_1'] self.assertEqual(['test_opt'], [opt.name for opt in strategy_opts]) @@ -140,6 +139,6 @@ class TestPlugins(base.TestCase): m_ext_manager.side_effect = m_list_available opts.show_plugins() m_show.assert_called_once_with( - [('watcher_strategies.STRATEGY_1', 'STRATEGY_1', + [('watcher_strategies.strategy_1', 'strategy_1', 'watcher.tests.decision_engine.' 'fake_strategies.FakeDummy1Strategy1')]) diff --git a/watcher_tempest_plugin/services/infra_optim/v1/json/client.py b/watcher_tempest_plugin/services/infra_optim/v1/json/client.py index b72c158db..e60d7cc65 100644 --- a/watcher_tempest_plugin/services/infra_optim/v1/json/client.py +++ b/watcher_tempest_plugin/services/infra_optim/v1/json/client.py @@ -66,7 +66,7 @@ class InfraOptimClientJSON(base.BaseInfraOptimClient): this audit template. :param extra: Metadata associated to this audit template. :return: A tuple with the server response and the created audit - template. + template. """ parameters = {k: v for k, v in kwargs.items() if v is not None} diff --git a/watcher_tempest_plugin/tests/api/admin/test_audit_template.py b/watcher_tempest_plugin/tests/api/admin/test_audit_template.py index 81b68bd46..6b0b2edb5 100644 --- a/watcher_tempest_plugin/tests/api/admin/test_audit_template.py +++ b/watcher_tempest_plugin/tests/api/admin/test_audit_template.py @@ -165,7 +165,7 @@ class TestAuditTemplate(base.BaseInfraOptimTest): @test.attr(type='smoke') def test_update_audit_template_replace(self): - _, new_goal = self.client.show_goal("SERVER_CONSOLIDATION") + _, new_goal = self.client.show_goal("server_consolidation") _, new_strategy = self.client.show_strategy("basic") params = {'name': 'my at name %s' % uuid.uuid4(), diff --git a/watcher_tempest_plugin/tests/api/admin/test_goal.py b/watcher_tempest_plugin/tests/api/admin/test_goal.py index bcf4839df..1d6a7cbb5 100644 --- a/watcher_tempest_plugin/tests/api/admin/test_goal.py +++ b/watcher_tempest_plugin/tests/api/admin/test_goal.py @@ -24,7 +24,7 @@ from watcher_tempest_plugin.tests.api.admin import base class TestShowListGoal(base.BaseInfraOptimTest): """Tests for goals""" - DUMMY_GOAL = "DUMMY" + DUMMY_GOAL = "dummy" @classmethod def resource_setup(cls): diff --git a/watcher_tempest_plugin/tests/scenario/test_execute_basic_optim.py b/watcher_tempest_plugin/tests/scenario/test_execute_basic_optim.py index 08cc728dd..93b53958a 100644 --- a/watcher_tempest_plugin/tests/scenario/test_execute_basic_optim.py +++ b/watcher_tempest_plugin/tests/scenario/test_execute_basic_optim.py @@ -30,7 +30,7 @@ CONF = config.CONF class TestExecuteBasicStrategy(base.BaseInfraOptimScenarioTest): """Tests for action plans""" - BASIC_GOAL = "SERVER_CONSOLIDATION" + BASIC_GOAL = "server_consolidation" @classmethod def skip_checks(cls): diff --git a/watcher_tempest_plugin/tests/scenario/test_execute_dummy_optim.py b/watcher_tempest_plugin/tests/scenario/test_execute_dummy_optim.py index 033914ec6..247acad91 100644 --- a/watcher_tempest_plugin/tests/scenario/test_execute_dummy_optim.py +++ b/watcher_tempest_plugin/tests/scenario/test_execute_dummy_optim.py @@ -29,15 +29,15 @@ class TestExecuteDummyStrategy(base.BaseInfraOptimScenarioTest): """Tests for action plans""" def test_execute_dummy_action_plan(self): - """Execute an action plan based on the DUMMY strategy + """Execute an action plan based on the 'dummy' strategy - - create an audit template with the dummy strategy + - create an audit template with the 'dummy' strategy - run the audit to create an action plan - get the action plan - run the action plan - get results and make sure it succeeded """ - _, goal = self.client.show_goal("DUMMY") + _, goal = self.client.show_goal("dummy") _, audit_template = self.create_audit_template(goal['uuid']) _, audit = self.create_audit(audit_template['uuid'])