diff --git a/watcher/applier/actions/base.py b/watcher/applier/actions/base.py index ef31a4334..2f6adff5d 100644 --- a/watcher/applier/actions/base.py +++ b/watcher/applier/actions/base.py @@ -23,18 +23,26 @@ import abc import six from watcher.common import clients +from watcher.common.loader import loadable @six.add_metaclass(abc.ABCMeta) -class BaseAction(object): +class BaseAction(loadable.Loadable): # NOTE(jed) by convention we decided # that the attribute "resource_id" is the unique id of # the resource to which the Action applies to allow us to use it in the # watcher dashboard and will be nested in input_parameters RESOURCE_ID = 'resource_id' - def __init__(self, osc=None): - """:param osc: an OpenStackClients instance""" + def __init__(self, config, osc=None): + """Constructor + + :param config: A mapping containing the configuration of this action + :type config: dict + :param osc: an OpenStackClients instance, defaults to None + :type osc: :py:class:`~.OpenStackClients` instance, optional + """ + super(BaseAction, self).__init__(config) self._input_parameters = {} self._osc = osc @@ -56,6 +64,15 @@ class BaseAction(object): def resource_id(self): return self.input_parameters[self.RESOURCE_ID] + @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 [] + @abc.abstractmethod def execute(self): """Executes the main logic of the action diff --git a/watcher/applier/workflow_engine/base.py b/watcher/applier/workflow_engine/base.py index 255c898cf..666bfdd16 100644 --- a/watcher/applier/workflow_engine/base.py +++ b/watcher/applier/workflow_engine/base.py @@ -23,18 +23,38 @@ import six from watcher.applier.actions import factory from watcher.applier.messaging import event_types from watcher.common import clients +from watcher.common.loader import loadable from watcher.common.messaging.events import event from watcher import objects @six.add_metaclass(abc.ABCMeta) -class BaseWorkFlowEngine(object): - def __init__(self, context=None, applier_manager=None): +class BaseWorkFlowEngine(loadable.Loadable): + + def __init__(self, config, context=None, applier_manager=None): + """Constructor + + :param config: A mapping containing the configuration of this + workflow engine + :type config: dict + :param osc: an OpenStackClients object, defaults to None + :type osc: :py:class:`~.OpenStackClients` instance, optional + """ + super(BaseWorkFlowEngine, self).__init__(config) self._context = context self._applier_manager = applier_manager self._action_factory = factory.ActionFactory() self._osc = None + @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 [] + @property def context(self): return self._context diff --git a/watcher/common/loader/default.py b/watcher/common/loader/default.py index 9048548e7..ae295646b 100644 --- a/watcher/common/loader/default.py +++ b/watcher/common/loader/default.py @@ -1,5 +1,5 @@ # -*- encoding: utf-8 -*- -# Copyright (c) 2015 b<>com +# 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. @@ -16,33 +16,82 @@ from __future__ import unicode_literals +from oslo_config import cfg from oslo_log import log -from stevedore.driver import DriverManager -from stevedore import ExtensionManager +from stevedore import driver as drivermanager +from stevedore import extension as extensionmanager from watcher.common import exception -from watcher.common.loader.base import BaseLoader +from watcher.common.loader import base +from watcher.common import utils LOG = log.getLogger(__name__) -class DefaultLoader(BaseLoader): - def __init__(self, namespace): +class DefaultLoader(base.BaseLoader): + + def __init__(self, namespace, conf=cfg.CONF): + """Entry point loader for Watcher using Stevedore + + :param namespace: namespace of the entry point(s) to load or list + :type namespace: str + :param conf: ConfigOpts instance, defaults to cfg.CONF + """ super(DefaultLoader, self).__init__() self.namespace = namespace + self.conf = conf def load(self, name, **kwargs): try: LOG.debug("Loading in namespace %s => %s ", self.namespace, name) - driver_manager = DriverManager(namespace=self.namespace, - name=name) - loaded = driver_manager.driver + driver_manager = drivermanager.DriverManager( + namespace=self.namespace, + name=name, + invoke_on_load=False, + ) + + driver_cls = driver_manager.driver + config = self._load_plugin_config(name, driver_cls) + + driver = driver_cls(config, **kwargs) except Exception as exc: LOG.exception(exc) raise exception.LoadingError(name=name) - return loaded(**kwargs) + return driver + + def _reload_config(self): + self.conf() + + def get_entry_name(self, name): + return ".".join([self.namespace, name]) + + def _load_plugin_config(self, name, driver_cls): + """Load the config of the plugin""" + config = utils.Struct() + config_opts = driver_cls.get_config_opts() + + if not config_opts: + return config + + group_name = self.get_entry_name(name) + self.conf.register_opts(config_opts, group=group_name) + + # Finalise the opt import by re-checking the configuration + # against the provided config files + self._reload_config() + + config_group = self.conf.get(group_name) + if not config_group: + raise exception.LoadingError(name=name) + + config.update({ + name: value for name, value in config_group.items() + }) + + return config def list_available(self): - extension_manager = ExtensionManager(namespace=self.namespace) + extension_manager = extensionmanager.ExtensionManager( + namespace=self.namespace) return {ext.name: ext.plugin for ext in extension_manager.extensions} diff --git a/watcher/tests/common/loader/FakeLoadable.py b/watcher/common/loader/loadable.py similarity index 50% rename from watcher/tests/common/loader/FakeLoadable.py rename to watcher/common/loader/loadable.py index 34b1a07f5..a3260b2e2 100644 --- a/watcher/tests/common/loader/FakeLoadable.py +++ b/watcher/common/loader/loadable.py @@ -1,5 +1,5 @@ # -*- encoding: utf-8 -*- -# Copyright (c) 2015 b<>com +# 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. @@ -13,16 +13,29 @@ # 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 FakeLoadable(object): - @classmethod - def namespace(cls): - return "TESTING" +class Loadable(object): + """Generic interface for dynamically loading a driver/entry point. + + This defines the contract in order to let the loader manager inject + the configuration parameters during the loading. + """ + + def __init__(self, config): + self.config = config @classmethod - def get_name(cls): - return 'fake' + @abc.abstractmethod + 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 + """ + raise NotImplementedError diff --git a/watcher/common/utils.py b/watcher/common/utils.py index 1bdacaf7e..5673d3466 100644 --- a/watcher/common/utils.py +++ b/watcher/common/utils.py @@ -41,6 +41,29 @@ CONF.register_opts(UTILS_OPTS) LOG = logging.getLogger(__name__) +class Struct(dict): + """Specialized dict where you access an item like an attribute + + >>> struct = Struct() + >>> struct['a'] = 1 + >>> struct.b = 2 + >>> assert struct.a == 1 + >>> assert struct['b'] == 2 + """ + + def __getattr__(self, name): + try: + return self[name] + except KeyError: + raise AttributeError(name) + + def __setattr__(self, name, value): + try: + self[name] = value + except KeyError: + raise AttributeError(name) + + def safe_rstrip(value, chars=None): """Removes trailing characters from a string if that does not make it empty diff --git a/watcher/decision_engine/planner/base.py b/watcher/decision_engine/planner/base.py index 8fd47eee3..9c255b4c2 100644 --- a/watcher/decision_engine/planner/base.py +++ b/watcher/decision_engine/planner/base.py @@ -47,12 +47,24 @@ See :doc:`../architecture` for more details on this component. import abc import six +from watcher.common.loader import loadable + @six.add_metaclass(abc.ABCMeta) -class BasePlanner(object): +class BasePlanner(loadable.Loadable): + + @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 [] + @abc.abstractmethod def schedule(self, context, audit_uuid, solution): - """The planner receives a solution to schedule + """The planner receives a solution to schedule :param solution: A solution provided by a strategy for scheduling :type solution: :py:class:`~.BaseSolution` subclass instance @@ -60,7 +72,7 @@ class BasePlanner(object): :type audit_uuid: str :return: Action plan with an ordered sequence of actions such that all security, dependency, and performance requirements are met. - :rtype: :py:class:`watcher.objects.action_plan.ActionPlan` instance + :rtype: :py:class:`watcher.objects.ActionPlan` instance """ # example: directed acyclic graph raise NotImplementedError() diff --git a/watcher/decision_engine/planner/loading/default.py b/watcher/decision_engine/planner/loading/default.py index c8896de39..3c4738a6a 100644 --- a/watcher/decision_engine/planner/loading/default.py +++ b/watcher/decision_engine/planner/loading/default.py @@ -17,10 +17,10 @@ from __future__ import unicode_literals -from watcher.common.loader.default import DefaultLoader +from watcher.common.loader import default -class DefaultPlannerLoader(DefaultLoader): +class DefaultPlannerLoader(default.DefaultLoader): def __init__(self): super(DefaultPlannerLoader, self).__init__( namespace='watcher_planners') diff --git a/watcher/decision_engine/strategy/strategies/base.py b/watcher/decision_engine/strategy/strategies/base.py index 6dc78e1d4..583a243bd 100644 --- a/watcher/decision_engine/strategy/strategies/base.py +++ b/watcher/decision_engine/strategy/strategies/base.py @@ -41,20 +41,22 @@ import six from watcher._i18n import _ from watcher.common import clients +from watcher.common.loader import loadable from watcher.decision_engine.solution import default from watcher.decision_engine.strategy.common import level @six.add_metaclass(abc.ABCMeta) -class BaseStrategy(object): +class BaseStrategy(loadable.Loadable): """A base class for all the strategies A Strategy is an algorithm implementation which is able to find a Solution for a given Goal. """ - def __init__(self, osc=None): + def __init__(self, config, osc=None): """:param osc: an OpenStackClients instance""" + super(BaseStrategy, self).__init__(config) self._name = self.get_name() self._display_name = self.get_display_name() # default strategy level @@ -104,6 +106,15 @@ class BaseStrategy(object): # 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 [] + @abc.abstractmethod def execute(self, original_model): """Execute a strategy diff --git a/watcher/decision_engine/strategy/strategies/basic_consolidation.py b/watcher/decision_engine/strategy/strategies/basic_consolidation.py index 27e054885..8a60525ae 100644 --- a/watcher/decision_engine/strategy/strategies/basic_consolidation.py +++ b/watcher/decision_engine/strategy/strategies/basic_consolidation.py @@ -71,16 +71,14 @@ class BasicConsolidation(base.ServerConsolidationBaseStrategy): MIGRATION = "migrate" CHANGE_NOVA_SERVICE_STATE = "change_nova_service_state" - def __init__(self, osc=None): + def __init__(self, config=None, osc=None): """Basic offline Consolidation using live migration - :param name: The name of the strategy (Default: "basic") - :param description: The description of the strategy - (Default: "Basic offline consolidation") - :param osc: An :py:class:`~watcher.common.clients.OpenStackClients` - instance + :param config: A mapping containing the configuration of this strategy + :type config: dict + :param osc: :py:class:`~.OpenStackClients` instance """ - super(BasicConsolidation, self).__init__(osc) + super(BasicConsolidation, self).__init__(config, osc) # set default value for the number of released nodes self.number_of_released_nodes = 0 diff --git a/watcher/decision_engine/strategy/strategies/dummy_strategy.py b/watcher/decision_engine/strategy/strategies/dummy_strategy.py index 2d71b2d15..64b7c1caa 100644 --- a/watcher/decision_engine/strategy/strategies/dummy_strategy.py +++ b/watcher/decision_engine/strategy/strategies/dummy_strategy.py @@ -48,8 +48,14 @@ class DummyStrategy(base.DummyBaseStrategy): NOP = "nop" SLEEP = "sleep" - def __init__(self, osc=None): - super(DummyStrategy, self).__init__(osc) + def __init__(self, config=None, osc=None): + """Dummy Strategy implemented for demo and testing purposes + + :param config: A mapping containing the configuration of this strategy + :type config: dict + :param osc: :py:class:`~.OpenStackClients` instance + """ + super(DummyStrategy, self).__init__(config, osc) def execute(self, original_model): LOG.debug("Executing Dummy strategy") diff --git a/watcher/decision_engine/strategy/strategies/outlet_temp_control.py b/watcher/decision_engine/strategy/strategies/outlet_temp_control.py index 66c6ab209..15d914cb4 100644 --- a/watcher/decision_engine/strategy/strategies/outlet_temp_control.py +++ b/watcher/decision_engine/strategy/strategies/outlet_temp_control.py @@ -78,14 +78,15 @@ class OutletTempControl(base.ThermalOptimizationBaseStrategy): MIGRATION = "migrate" - def __init__(self, osc=None): + def __init__(self, config=None, osc=None): """Outlet temperature control using live migration - :param name: the name of the strategy - :param description: a description of the strategy - :param osc: an OpenStackClients object + :param config: A mapping containing the configuration of this strategy + :type config: dict + :param osc: an OpenStackClients object, defaults to None + :type osc: :py:class:`~.OpenStackClients` instance, optional """ - super(OutletTempControl, self).__init__(osc) + super(OutletTempControl, self).__init__(config, osc) # the migration plan will be triggered when the outlet temperature # reaches threshold # TODO(zhenzanz): Threshold should be configurable for each audit diff --git a/watcher/decision_engine/strategy/strategies/vm_workload_consolidation.py b/watcher/decision_engine/strategy/strategies/vm_workload_consolidation.py index c897e2322..e8c6969bb 100644 --- a/watcher/decision_engine/strategy/strategies/vm_workload_consolidation.py +++ b/watcher/decision_engine/strategy/strategies/vm_workload_consolidation.py @@ -84,8 +84,8 @@ class VMWorkloadConsolidation(base.ServerConsolidationBaseStrategy): https://github.com/openstack/watcher-specs/blob/master/specs/mitaka/implemented/zhaw-load-consolidation.rst """ # noqa - def __init__(self, osc=None): - super(VMWorkloadConsolidation, self).__init__(osc) + def __init__(self, config=None, osc=None): + super(VMWorkloadConsolidation, self).__init__(config, osc) self._ceilometer = None self.number_of_migrations = 0 self.number_of_released_hypervisors = 0 diff --git a/watcher/opts.py b/watcher/opts.py index fd7abaf71..587cf3f38 100644 --- a/watcher/opts.py +++ b/watcher/opts.py @@ -18,14 +18,27 @@ from keystoneauth1 import loading as ka_loading import watcher.api.app +from watcher.applier.actions.loading import default as action_loader from watcher.applier import manager as applier_manager +from watcher.applier.workflow_engine.loading import default as \ + workflow_engine_loader from watcher.common import clients 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 = ( + action_loader.DefaultActionLoader, + planner_loader.DefaultPlannerLoader, + strategy_loader.DefaultStrategyLoader, + workflow_engine_loader.DefaultWorkFlowEngineLoader, +) def list_opts(): - return [ + watcher_opts = [ ('api', watcher.api.app.API_SERVICE_OPTS), ('watcher_decision_engine', decision_engine_manger.WATCHER_DECISION_ENGINE_OPTS), @@ -41,3 +54,22 @@ def list_opts(): ka_loading.get_auth_plugin_conf_options('password') + ka_loading.get_session_conf_options())) ] + + watcher_opts += list_plugin_opts() + + return watcher_opts + + +def list_plugin_opts(): + plugins_opts = [] + for plugin_loader_cls in PLUGIN_LOADERS: + plugin_loader = plugin_loader_cls() + plugins_map = plugin_loader.list_available() + + for plugin_name, plugin_cls in plugins_map.items(): + plugin_opts = plugin_cls.get_config_opts() + if plugin_opts: + plugins_opts.append( + (plugin_loader.get_entry_name(plugin_name), plugin_opts)) + + return plugins_opts diff --git a/watcher/tests/applier/actions/test_change_nova_service_state.py b/watcher/tests/applier/actions/test_change_nova_service_state.py index 4ed09b428..7de14bb86 100644 --- a/watcher/tests/applier/actions/test_change_nova_service_state.py +++ b/watcher/tests/applier/actions/test_change_nova_service_state.py @@ -54,7 +54,8 @@ class TestChangeNovaServiceState(base.TestCase): baction.BaseAction.RESOURCE_ID: "compute-1", "state": hstate.HypervisorState.ENABLED.value, } - self.action = change_nova_service_state.ChangeNovaServiceState() + self.action = change_nova_service_state.ChangeNovaServiceState( + mock.Mock()) self.action.input_parameters = self.input_parameters def test_parameters_down(self): diff --git a/watcher/tests/applier/actions/test_migration.py b/watcher/tests/applier/actions/test_migration.py index d8e6ab8f4..7a87edad4 100644 --- a/watcher/tests/applier/actions/test_migration.py +++ b/watcher/tests/applier/actions/test_migration.py @@ -58,7 +58,7 @@ class TestMigration(base.TestCase): "dst_hypervisor": "hypervisor2-hostname", baction.BaseAction.RESOURCE_ID: self.INSTANCE_UUID, } - self.action = migration.Migrate() + self.action = migration.Migrate(mock.Mock()) self.action.input_parameters = self.input_parameters self.input_parameters_cold = { @@ -67,7 +67,7 @@ class TestMigration(base.TestCase): "dst_hypervisor": "hypervisor2-hostname", baction.BaseAction.RESOURCE_ID: self.INSTANCE_UUID, } - self.action_cold = migration.Migrate() + self.action_cold = migration.Migrate(mock.Mock()) self.action_cold.input_parameters = self.input_parameters_cold def test_parameters(self): diff --git a/watcher/tests/applier/actions/test_sleep.py b/watcher/tests/applier/actions/test_sleep.py index b28e4f0ec..578d330f7 100644 --- a/watcher/tests/applier/actions/test_sleep.py +++ b/watcher/tests/applier/actions/test_sleep.py @@ -14,6 +14,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # + +import mock import voluptuous from watcher.applier.actions import sleep @@ -23,7 +25,7 @@ from watcher.tests import base class TestSleep(base.TestCase): def setUp(self): super(TestSleep, self).setUp() - self.s = sleep.Sleep() + self.s = sleep.Sleep(mock.Mock()) def test_parameters_duration(self): self.s.input_parameters = {self.s.DURATION: 1.0} diff --git a/watcher/tests/applier/workflow_engine/test_default_workflow_engine.py b/watcher/tests/applier/workflow_engine/test_default_workflow_engine.py index 9172c106a..e5c2bb86a 100644 --- a/watcher/tests/applier/workflow_engine/test_default_workflow_engine.py +++ b/watcher/tests/applier/workflow_engine/test_default_workflow_engine.py @@ -20,10 +20,9 @@ import abc import mock import six -from stevedore import driver -from stevedore import extension from watcher.applier.actions import base as abase +from watcher.applier.actions import factory from watcher.applier.workflow_engine import default as tflow from watcher.common import exception from watcher.common import utils @@ -31,6 +30,10 @@ from watcher import objects from watcher.tests.db import base +class ExpectedException(Exception): + pass + + @six.add_metaclass(abc.ABCMeta) class FakeAction(abase.BaseAction): def schema(self): @@ -46,21 +49,14 @@ class FakeAction(abase.BaseAction): pass def execute(self): - raise Exception() - - @classmethod - def namespace(cls): - return "TESTING" - - @classmethod - def get_name(cls): - return 'fake_action' + raise ExpectedException() class TestDefaultWorkFlowEngine(base.DbTestCase): def setUp(self): super(TestDefaultWorkFlowEngine, self).setUp() self.engine = tflow.DefaultWorkFlowEngine( + config=mock.Mock(), context=self.context, applier_manager=mock.MagicMock()) @@ -86,6 +82,7 @@ class TestDefaultWorkFlowEngine(base.DbTestCase): new_action = objects.Action(self.context, **action) new_action.create(self.context) new_action.save() + return new_action def check_action_state(self, action, expected_state): @@ -175,18 +172,13 @@ class TestDefaultWorkFlowEngine(base.DbTestCase): self.check_action_state(second, objects.action.State.SUCCEEDED) self.check_action_state(third, objects.action.State.FAILED) - @mock.patch("watcher.common.loader.default.DriverManager") - def test_execute_with_action_exception(self, m_driver): - m_driver.return_value = driver.DriverManager.make_test_instance( - extension=extension.Extension(name=FakeAction.get_name(), - entry_point="%s:%s" % ( - FakeAction.__module__, - FakeAction.__name__), - plugin=FakeAction, - obj=None), - namespace=FakeAction.namespace()) - actions = [self.create_action("dontcare", {}, None)] + @mock.patch.object(factory.ActionFactory, "make_action") + def test_execute_with_action_exception(self, m_make_action): + actions = [self.create_action("fake_action", {}, None)] + m_make_action.return_value = FakeAction(mock.Mock()) - self.assertRaises(exception.WorkflowExecutionException, - self.engine.execute, actions) + exc = self.assertRaises(exception.WorkflowExecutionException, + self.engine.execute, actions) + + self.assertIsInstance(exc.kwargs['error'], ExpectedException) self.check_action_state(actions[0], objects.action.State.FAILED) diff --git a/watcher/tests/common/loader/test_loader.py b/watcher/tests/common/loader/test_loader.py index 09c5723f7..1395c069f 100644 --- a/watcher/tests/common/loader/test_loader.py +++ b/watcher/tests/common/loader/test_loader.py @@ -18,36 +18,88 @@ from __future__ import unicode_literals import mock -from oslotest.base import BaseTestCase -from stevedore.driver import DriverManager +from oslo_config import cfg +from stevedore import driver as drivermanager from stevedore.extension import Extension from watcher.common import exception -from watcher.common.loader.default import DefaultLoader -from watcher.tests.common.loader.FakeLoadable import FakeLoadable +from watcher.common.loader import default +from watcher.common.loader import loadable +from watcher.tests import base -class TestLoader(BaseTestCase): - @mock.patch("watcher.common.loader.default.DriverManager") - def test_load_driver_no_opt(self, m_driver_manager): - m_driver_manager.return_value = DriverManager.make_test_instance( - extension=Extension(name=FakeLoadable.get_name(), - entry_point="%s:%s" % ( - FakeLoadable.__module__, - FakeLoadable.__name__), - plugin=FakeLoadable, - obj=None), - namespace=FakeLoadable.namespace()) +class FakeLoadable(loadable.Loadable): - loader_manager = DefaultLoader(namespace='TESTING') - loaded_driver = loader_manager.load(name='fake') + @classmethod + def get_config_opts(cls): + return [] - self.assertEqual(FakeLoadable.get_name(), loaded_driver.get_name()) - @mock.patch("watcher.common.loader.default.DriverManager") - def test_load_driver_bad_plugin(self, m_driver_manager): +class FakeLoadableWithOpts(loadable.Loadable): + + @classmethod + def get_config_opts(cls): + return [ + cfg.StrOpt("test_opt", default="fake_with_opts"), + ] + + +class TestLoader(base.TestCase): + + def setUp(self): + super(TestLoader, self).setUp() + + def _fake_parse(self, *args, **kw): + return cfg.ConfigOpts._parse_cli_opts(cfg.CONF, []) + + cfg.CONF._parse_cli_opts = _fake_parse + + def test_load_loadable_no_opt(self): + fake_driver = drivermanager.DriverManager.make_test_instance( + extension=Extension( + name="fake", + entry_point="%s:%s" % (FakeLoadable.__module__, + FakeLoadable.__name__), + plugin=FakeLoadable, + obj=None), + namespace="TESTING") + + loader_manager = default.DefaultLoader(namespace='TESTING') + with mock.patch.object(drivermanager, + "DriverManager") as m_driver_manager: + m_driver_manager.return_value = fake_driver + loaded_driver = loader_manager.load(name='fake') + + self.assertIsInstance(loaded_driver, FakeLoadable) + + @mock.patch("watcher.common.loader.default.drivermanager.DriverManager") + def test_load_loadable_bad_plugin(self, m_driver_manager): m_driver_manager.side_effect = Exception() - loader_manager = DefaultLoader(namespace='TESTING') + loader_manager = default.DefaultLoader(namespace='TESTING') self.assertRaises(exception.LoadingError, loader_manager.load, name='bad_driver') + + def test_load_loadable_with_opts(self): + fake_driver = drivermanager.DriverManager.make_test_instance( + extension=Extension( + name="fake", + entry_point="%s:%s" % (FakeLoadableWithOpts.__module__, + FakeLoadableWithOpts.__name__), + plugin=FakeLoadableWithOpts, + obj=None), + namespace="TESTING") + + loader_manager = default.DefaultLoader(namespace='TESTING') + with mock.patch.object(drivermanager, + "DriverManager") as m_driver_manager: + m_driver_manager.return_value = fake_driver + loaded_driver = loader_manager.load(name='fake') + + self.assertIsInstance(loaded_driver, FakeLoadableWithOpts) + + self.assertEqual( + "fake_with_opts", loaded_driver.config.get("test_opt")) + + self.assertEqual( + "fake_with_opts", loaded_driver.config.test_opt) diff --git a/watcher/tests/decision_engine/fake_strategies.py b/watcher/tests/decision_engine/fake_strategies.py index c25658f75..7d24f055e 100644 --- a/watcher/tests/decision_engine/fake_strategies.py +++ b/watcher/tests/decision_engine/fake_strategies.py @@ -14,8 +14,12 @@ # See the License for the specific language governing permissions and # limitations under the License. +from oslo_config import cfg + from watcher.decision_engine.strategy.strategies import base as base_strategy +CONF = cfg.CONF + class FakeStrategy(base_strategy.BaseStrategy): @@ -48,6 +52,10 @@ class FakeStrategy(base_strategy.BaseStrategy): def get_translatable_goal_display_name(cls): return cls.GOAL_DISPLAY_NAME + @classmethod + def get_config_opts(cls): + return [] + def execute(self, original_model): pass @@ -58,6 +66,12 @@ class FakeDummy1Strategy1(FakeStrategy): NAME = "STRATEGY_1" DISPLAY_NAME = "Strategy 1" + @classmethod + def get_config_opts(cls): + return [ + cfg.StrOpt('test_opt', help="Option used for testing."), + ] + class FakeDummy1Strategy2(FakeStrategy): GOAL_NAME = "DUMMY_1" diff --git a/watcher/tests/decision_engine/planner/test_default_planner.py b/watcher/tests/decision_engine/planner/test_default_planner.py index d561cb159..9cb46122d 100644 --- a/watcher/tests/decision_engine/planner/test_default_planner.py +++ b/watcher/tests/decision_engine/planner/test_default_planner.py @@ -57,7 +57,7 @@ class SolutionFakerSingleHyp(object): class TestActionScheduling(base.DbTestCase): def test_schedule_actions(self): - default_planner = pbase.DefaultPlanner() + default_planner = pbase.DefaultPlanner(mock.Mock()) audit = db_utils.create_test_audit(uuid=utils.generate_uuid()) solution = dsol.DefaultSolution() @@ -83,7 +83,7 @@ class TestActionScheduling(base.DbTestCase): self.assertEqual("migrate", actions[0].action_type) def test_schedule_two_actions(self): - default_planner = pbase.DefaultPlanner() + default_planner = pbase.DefaultPlanner(mock.Mock()) audit = db_utils.create_test_audit(uuid=utils.generate_uuid()) solution = dsol.DefaultSolution() @@ -115,9 +115,10 @@ class TestActionScheduling(base.DbTestCase): class TestDefaultPlanner(base.DbTestCase): + def setUp(self): super(TestDefaultPlanner, self).setUp() - self.default_planner = pbase.DefaultPlanner() + self.default_planner = pbase.DefaultPlanner(mock.Mock()) obj_utils.create_test_audit_template(self.context) p = mock.patch.object(db_api.BaseConnection, 'create_action_plan') 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 54f29592b..27ae55359 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 @@ -15,8 +15,9 @@ # limitations under the License. from __future__ import unicode_literals -from mock import patch +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.strategy.strategies import dummy_strategy @@ -25,38 +26,38 @@ from watcher.tests import base class TestDefaultStrategyLoader(base.TestCase): - strategy_loader = default_loading.DefaultStrategyLoader() - def test_load_strategy_with_empty_model(self): - self.assertRaises( - exception.LoadingError, self.strategy_loader.load, None) + strategy_loader = default_loading.DefaultStrategyLoader() + self.assertRaises(exception.LoadingError, strategy_loader.load, None) def test_load_strategy_is_basic(self): + strategy_loader = default_loading.DefaultStrategyLoader() expected_strategy = 'basic' - selected_strategy = self.strategy_loader.load(expected_strategy) + selected_strategy = strategy_loader.load(expected_strategy) self.assertEqual( selected_strategy.id, expected_strategy, 'The default strategy should be basic') - @patch("watcher.common.loader.default.ExtensionManager") - def test_strategy_loader(self, m_extension_manager): + def test_strategy_loader(self): dummy_strategy_name = "dummy" # Set up the fake Stevedore extensions - m_extension_manager.return_value = extension.\ - ExtensionManager.make_test_instance( - extensions=[extension.Extension( - name=dummy_strategy_name, - entry_point="%s:%s" % ( - dummy_strategy.DummyStrategy.__module__, - dummy_strategy.DummyStrategy.__name__), - plugin=dummy_strategy.DummyStrategy, - obj=None, - )], - namespace="watcher_strategies", - ) - strategy_loader = default_loading.DefaultStrategyLoader() - loaded_strategy = strategy_loader.load("dummy") + fake_extmanager_call = extension.ExtensionManager.make_test_instance( + extensions=[extension.Extension( + name=dummy_strategy_name, + entry_point="%s:%s" % ( + dummy_strategy.DummyStrategy.__module__, + dummy_strategy.DummyStrategy.__name__), + plugin=dummy_strategy.DummyStrategy, + obj=None, + )], + namespace="watcher_strategies", + ) + + 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") self.assertEqual("dummy", loaded_strategy.id) self.assertEqual("Dummy strategy", loaded_strategy.display_name) @@ -67,5 +68,5 @@ class TestDefaultStrategyLoader(base.TestCase): self.assertIsInstance(loaded_strategy, dummy_strategy.DummyStrategy) def test_endpoints(self): - for endpoint in self.strategy_loader.list_available(): + for endpoint in strategy_loader.list_available(): self.assertIsNotNone(self.strategy_loader.load(endpoint)) diff --git a/watcher/tests/test_list_opts.py b/watcher/tests/test_list_opts.py index d1b22f815..967251588 100644 --- a/watcher/tests/test_list_opts.py +++ b/watcher/tests/test_list_opts.py @@ -14,18 +14,97 @@ # See the License for the specific language governing permissions and # limitations under the License. -from __future__ import absolute_import -from __future__ import unicode_literals +import mock +from stevedore import extension -import watcher.opts as opt - -from watcher.tests.base import BaseTestCase +from watcher import opts +from watcher.tests import base +from watcher.tests.decision_engine import fake_strategies -class TestListOpts(BaseTestCase): +class TestListOpts(base.TestCase): def setUp(self): super(TestListOpts, self).setUp() + self.base_sections = [ + 'api', 'watcher_decision_engine', 'watcher_applier', + 'watcher_planner', 'nova_client', 'glance_client', + 'cinder_client', 'ceilometer_client', 'neutron_client', + 'watcher_clients_auth'] def test_run_list_opts(self): - result = opt.list_opts() + expected_sections = self.base_sections + + result = opts.list_opts() + self.assertIsNotNone(result) + for section_name, options in result: + self.assertIn(section_name, expected_sections) + self.assertTrue(len(options)) + + def test_list_opts_no_opts(self): + expected_sections = self.base_sections + # Set up the fake Stevedore extensions + fake_extmanager_call = extension.ExtensionManager.make_test_instance( + extensions=[extension.Extension( + name=fake_strategies.FakeDummy1Strategy2.get_name(), + entry_point="%s:%s" % ( + fake_strategies.FakeDummy1Strategy2.__module__, + fake_strategies.FakeDummy1Strategy2.__name__), + plugin=fake_strategies.FakeDummy1Strategy2, + obj=None, + )], + namespace="watcher_strategies", + ) + + def m_list_available(namespace): + if namespace == "watcher_strategies": + return fake_extmanager_call + else: + return extension.ExtensionManager.make_test_instance( + extensions=[], namespace=namespace) + + with mock.patch.object(extension, "ExtensionManager") as m_ext_manager: + m_ext_manager.side_effect = m_list_available + result = opts.list_opts() + + self.assertIsNotNone(result) + for section_name, options in result: + self.assertIn(section_name, expected_sections) + self.assertTrue(len(options)) + + def test_list_opts_with_opts(self): + expected_sections = self.base_sections + [ + 'watcher_strategies.STRATEGY_1'] + # Set up the fake Stevedore extensions + fake_extmanager_call = extension.ExtensionManager.make_test_instance( + extensions=[extension.Extension( + name=fake_strategies.FakeDummy1Strategy1.get_name(), + entry_point="%s:%s" % ( + fake_strategies.FakeDummy1Strategy1.__module__, + fake_strategies.FakeDummy1Strategy1.__name__), + plugin=fake_strategies.FakeDummy1Strategy1, + obj=None, + )], + namespace="watcher_strategies", + ) + + def m_list_available(namespace): + if namespace == "watcher_strategies": + return fake_extmanager_call + else: + return extension.ExtensionManager.make_test_instance( + extensions=[], namespace=namespace) + + with mock.patch.object(extension, "ExtensionManager") as m_ext_manager: + m_ext_manager.side_effect = m_list_available + result = opts.list_opts() + + self.assertIsNotNone(result) + for section_name, options in result: + self.assertIn(section_name, expected_sections) + self.assertTrue(len(options)) + + result_map = dict(result) + + strategy_opts = result_map['watcher_strategies.STRATEGY_1'] + self.assertEqual(['test_opt'], [opt.name for opt in strategy_opts])