diff --git a/doc/source/dev/plugin/action-plugin.rst b/doc/source/dev/plugin/action-plugin.rst index 7a29ef030..62883c30d 100644 --- a/doc/source/dev/plugin/action-plugin.rst +++ b/doc/source/dev/plugin/action-plugin.rst @@ -205,6 +205,9 @@ By doing so, your action will be saved within the Watcher Database, ready to be processed by the planner for creating an action plan which can then be executed by the Watcher Applier via its workflow engine. +At the last, remember to add the action into the weights in ``watcher.conf``, +otherwise you will get an error when the action be referenced in a strategy. + Scheduling of an action plugin ============================== diff --git a/watcher/decision_engine/planner/default.py b/watcher/decision_engine/planner/default.py index 9d568ecc5..a4e6e677c 100644 --- a/watcher/decision_engine/planner/default.py +++ b/watcher/decision_engine/planner/default.py @@ -17,6 +17,7 @@ # limitations under the License. # +from oslo_config import cfg from oslo_log import log from watcher._i18n import _LW @@ -30,18 +31,29 @@ LOG = log.getLogger(__name__) class DefaultPlanner(base.BasePlanner): """Default planner implementation - This implementation comes with basic rules with a fixed set of action types - that are weighted. An action having a lower weight will be scheduled before - the other ones. + This implementation comes with basic rules with a set of action types that + are weighted. An action having a lower weight will be scheduled before the + other ones. The set of action types can be specified by 'weights' in the + ``watcher.conf``. You need to associate a different weight to all available + actions into the configuration file, otherwise you will get an error when + the new action will be referenced in the solution produced by a strategy. """ - priorities = { + weights_dict = { 'nop': 0, 'sleep': 1, 'change_nova_service_state': 2, 'migrate': 3, } + @classmethod + def get_config_opts(cls): + return [cfg.DictOpt( + 'weights', + help="These weights are used to schedule the actions", + default=cls.weights_dict), + ] + def create_action(self, action_plan_id, action_type, @@ -59,6 +71,7 @@ class DefaultPlanner(base.BasePlanner): def schedule(self, context, audit_id, solution): LOG.debug('Create an action plan for the audit uuid: %s ', audit_id) + priorities = self.config.weights action_plan = self._create_action_plan(context, audit_id, solution) actions = list(solution.actions) @@ -68,7 +81,7 @@ class DefaultPlanner(base.BasePlanner): action_plan_id=action_plan.id, action_type=action.get('action_type'), input_parameters=action.get('input_parameters')) - to_schedule.append((self.priorities[action.get('action_type')], + to_schedule.append((priorities[action.get('action_type')], json_action)) self._create_efficacy_indicators( diff --git a/watcher/tests/base.py b/watcher/tests/base.py index 3a031675a..f1644b281 100644 --- a/watcher/tests/base.py +++ b/watcher/tests/base.py @@ -50,6 +50,7 @@ class TestCase(BaseTestCase): def setUp(self): super(TestCase, self).setUp() + self.useFixture(conf_fixture.ConfReloadFixture()) self.app = testing.load_test_app(os.path.join( os.path.dirname(__file__), 'config.py' diff --git a/watcher/tests/conf_fixture.py b/watcher/tests/conf_fixture.py index d33006124..45f0107f3 100644 --- a/watcher/tests/conf_fixture.py +++ b/watcher/tests/conf_fixture.py @@ -39,3 +39,22 @@ class ConfFixture(fixtures.Fixture): self.conf.set_default('verbose', True) config.parse_args([], default_config_files=[]) self.addCleanup(self.conf.reset) + + +class ConfReloadFixture(ConfFixture): + """Fixture to manage reloads of conf settings.""" + + def __init__(self, conf=cfg.CONF): + self.conf = conf + self._original_parse_cli_opts = self.conf._parse_cli_opts + + def _fake_parser(self, *args, **kw): + return cfg.ConfigOpts._parse_cli_opts(self.conf, []) + + def _restore_parser(self): + self.conf._parse_cli_opts = self._original_parse_cli_opts + + def setUp(self): + super(ConfReloadFixture, self).setUp() + self.conf._parse_cli_opts = self._fake_parser + self.addCleanup(self._restore_parser) diff --git a/watcher/tests/decision_engine/planner/test_default_planner.py b/watcher/tests/decision_engine/planner/test_default_planner.py index 66249d1c1..e5f6e38fc 100644 --- a/watcher/tests/decision_engine/planner/test_default_planner.py +++ b/watcher/tests/decision_engine/planner/test_default_planner.py @@ -76,9 +76,9 @@ class TestActionScheduling(base.DbTestCase): with mock.patch.object( pbase.DefaultPlanner, "create_action", wraps=default_planner.create_action) as m_create_action: - action_plan = default_planner.schedule( - self.context, audit.id, solution - ) + default_planner.config.weights = {'migrate': 3} + action_plan = default_planner.schedule(self.context, + audit.id, solution) self.assertIsNotNone(action_plan.uuid) self.assertEqual(1, m_create_action.call_count) @@ -107,9 +107,9 @@ class TestActionScheduling(base.DbTestCase): with mock.patch.object( pbase.DefaultPlanner, "create_action", wraps=default_planner.create_action) as m_create_action: - action_plan = default_planner.schedule( - self.context, audit.id, solution - ) + default_planner.config.weights = {'migrate': 3, 'nop': 0} + action_plan = default_planner.schedule(self.context, + audit.id, solution) self.assertIsNotNone(action_plan.uuid) self.assertEqual(2, m_create_action.call_count) # check order @@ -118,12 +118,45 @@ class TestActionScheduling(base.DbTestCase): self.assertEqual("nop", actions[0].action_type) self.assertEqual("migrate", actions[1].action_type) + def test_schedule_actions_with_unknown_action(self): + default_planner = pbase.DefaultPlanner(mock.Mock()) + audit = db_utils.create_test_audit(uuid=utils.generate_uuid()) + solution = dsol.DefaultSolution( + goal=mock.Mock(), strategy=mock.Mock()) + + parameters = { + "src_uuid_hypervisor": "server1", + "dst_uuid_hypervisor": "server2", + } + solution.add_action(action_type="migrate", + resource_id="b199db0c-1408-4d52-b5a5-5ca14de0ff36", + input_parameters=parameters) + + solution.add_action(action_type="new_action_type", + resource_id="", + input_parameters={}) + + with mock.patch.object( + pbase.DefaultPlanner, "create_action", + wraps=default_planner.create_action) as m_create_action: + default_planner.config.weights = {'migrate': 0} + self.assertRaises(KeyError, default_planner.schedule, + self.context, audit.id, solution) + self.assertEqual(2, m_create_action.call_count) + class TestDefaultPlanner(base.DbTestCase): def setUp(self): super(TestDefaultPlanner, self).setUp() self.default_planner = pbase.DefaultPlanner(mock.Mock()) + self.default_planner.config.weights = { + 'nop': 0, + 'sleep': 1, + 'change_nova_service_state': 2, + 'migrate': 3 + } + obj_utils.create_test_audit_template(self.context) p = mock.patch.object(db_api.BaseConnection, 'create_action_plan') diff --git a/watcher/tests/test_list_opts.py b/watcher/tests/test_list_opts.py index 7e376cbdf..185405c93 100644 --- a/watcher/tests/test_list_opts.py +++ b/watcher/tests/test_list_opts.py @@ -29,7 +29,7 @@ class TestListOpts(base.TestCase): 'api', 'watcher_decision_engine', 'watcher_applier', 'watcher_planner', 'nova_client', 'glance_client', 'cinder_client', 'ceilometer_client', 'neutron_client', - 'watcher_clients_auth'] + 'watcher_clients_auth', 'watcher_planners.default'] def test_run_list_opts(self): expected_sections = self.base_sections