From b4c5e2bb815f2162e6ffcb9e6cb619c48d35fc8b Mon Sep 17 00:00:00 2001 From: jinquanni Date: Thu, 7 Jul 2016 20:12:53 +0800 Subject: [PATCH] Make default Planner generic to handle new action MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently we have to amend the code of the Planner for each new added Watcher Action. This patch set will modifying the DefaultPlanner class and leverage the plugins-parameters blueprint so that we can configure weights via the configuration file. Documentation and unittest also be update in this patch. Co-Authored-By: Vincent Françoise Change-Id: Ib794488fcafd1e153a7d7b1f7253686136501872 blueprint: configurable-weights-default-planner --- doc/source/dev/plugin/action-plugin.rst | 3 ++ watcher/decision_engine/planner/default.py | 23 +++++++--- watcher/tests/base.py | 1 + watcher/tests/conf_fixture.py | 19 ++++++++ .../planner/test_default_planner.py | 45 ++++++++++++++++--- watcher/tests/test_list_opts.py | 2 +- 6 files changed, 81 insertions(+), 12 deletions(-) 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