From ff57eb73f950ce93ea5a1b882b836bb8b954c9fd Mon Sep 17 00:00:00 2001 From: deepak_mourya Date: Tue, 10 Apr 2018 10:58:02 +0530 Subject: [PATCH] Refactor watcher API for Action Plan Start Currently the REST API to start action plan in watcher is which is same as for update action plan. PATCH /v1/action_plans https://docs.openstack.org/watcher/latest/api/v1.html we need to make it easy to understand like : POST /v1/action_plans/{action_plan_uuid}/start the action should be start in above case. Change-Id: I5353e4aa58d1675d8afb94bea35d9b953514129a Closes-Bug: #1756274 --- watcher/api/controllers/v1/action_plan.py | 35 ++++++++++++++- watcher/common/exception.py | 4 ++ watcher/common/policies/action_plan.py | 11 +++++ watcher/tests/api/base.py | 6 +++ watcher/tests/api/v1/test_actions_plans.py | 51 +++++++++++++++++++++- 5 files changed, 103 insertions(+), 4 deletions(-) diff --git a/watcher/api/controllers/v1/action_plan.py b/watcher/api/controllers/v1/action_plan.py index 46e79f443..da85b2439 100644 --- a/watcher/api/controllers/v1/action_plan.py +++ b/watcher/api/controllers/v1/action_plan.py @@ -337,7 +337,8 @@ class ActionPlansController(rest.RestController): from the top-level resource ActionPlan.""" _custom_actions = { - 'detail': ['GET'], + 'start': ['POST'], + 'detail': ['GET'] } def _get_action_plans_collection(self, marker, limit, @@ -540,7 +541,7 @@ class ActionPlansController(rest.RestController): if action_plan_to_update[field] != patch_val: action_plan_to_update[field] = patch_val - if (field == 'state'and + if (field == 'state' and patch_val == objects.action_plan.State.PENDING): launch_action_plan = True @@ -565,3 +566,33 @@ class ActionPlansController(rest.RestController): pecan.request.context, action_plan_uuid) return ActionPlan.convert_with_links(action_plan_to_update) + + @wsme_pecan.wsexpose(ActionPlan, types.uuid) + def start(self, action_plan_uuid, **kwargs): + """Start an action_plan + + :param action_plan_uuid: UUID of an action_plan. + """ + + action_plan_to_start = api_utils.get_resource( + 'ActionPlan', action_plan_uuid, eager=True) + context = pecan.request.context + + policy.enforce(context, 'action_plan:start', action_plan_to_start, + action='action_plan:start') + + if action_plan_to_start['state'] != \ + objects.action_plan.State.RECOMMENDED: + raise Exception.StartError( + state=action_plan_to_start.state) + + action_plan_to_start['state'] = objects.action_plan.State.PENDING + action_plan_to_start.save() + + applier_client = rpcapi.ApplierAPI() + applier_client.launch_action_plan(pecan.request.context, + action_plan_uuid) + action_plan_to_start = objects.ActionPlan.get_by_uuid( + pecan.request.context, action_plan_uuid) + + return ActionPlan.convert_with_links(action_plan_to_start) diff --git a/watcher/common/exception.py b/watcher/common/exception.py index 7326ffba4..1ecc284b8 100644 --- a/watcher/common/exception.py +++ b/watcher/common/exception.py @@ -336,6 +336,10 @@ class DeleteError(Invalid): msg_fmt = _("Couldn't delete when state is '%(state)s'.") +class StartError(Invalid): + msg_fmt = _("Couldn't start when state is '%(state)s'.") + + # decision engine class WorkflowExecutionException(WatcherException): diff --git a/watcher/common/policies/action_plan.py b/watcher/common/policies/action_plan.py index 613d4bcf7..bc1103ffe 100644 --- a/watcher/common/policies/action_plan.py +++ b/watcher/common/policies/action_plan.py @@ -71,6 +71,17 @@ rules = [ 'method': 'PATCH' } ] + ), + policy.DocumentedRuleDefault( + name=ACTION_PLAN % 'start', + check_str=base.RULE_ADMIN_API, + description='Start an action plans.', + operations=[ + { + 'path': '/v1/action_plans/{action_plan_uuid}/action', + 'method': 'POST' + } + ] ) ] diff --git a/watcher/tests/api/base.py b/watcher/tests/api/base.py index 697934777..82bc830c8 100644 --- a/watcher/tests/api/base.py +++ b/watcher/tests/api/base.py @@ -137,6 +137,12 @@ class FunctionalTest(base.DbTestCase): headers=headers, extra_environ=extra_environ, status=status, method="put") + def post(self, *args, **kwargs): + headers = kwargs.pop('headers', {}) + headers.setdefault('Accept', 'application/json') + kwargs['headers'] = headers + return self.app.post(*args, **kwargs) + def post_json(self, path, params, expect_errors=False, headers=None, extra_environ=None, status=None): """Sends simulated HTTP POST request to Pecan test app. diff --git a/watcher/tests/api/v1/test_actions_plans.py b/watcher/tests/api/v1/test_actions_plans.py index a001a99d9..12b3aa991 100644 --- a/watcher/tests/api/v1/test_actions_plans.py +++ b/watcher/tests/api/v1/test_actions_plans.py @@ -357,6 +357,53 @@ class TestDelete(api_base.FunctionalTest): self.assertTrue(response.json['error_message']) +class TestStart(api_base.FunctionalTest): + + def setUp(self): + super(TestStart, self).setUp() + obj_utils.create_test_goal(self.context) + obj_utils.create_test_strategy(self.context) + obj_utils.create_test_audit(self.context) + self.action_plan = obj_utils.create_test_action_plan( + self.context, state=objects.action_plan.State.RECOMMENDED) + p = mock.patch.object(db_api.BaseConnection, 'update_action_plan') + self.mock_action_plan_update = p.start() + self.mock_action_plan_update.side_effect = \ + self._simulate_rpc_action_plan_update + self.addCleanup(p.stop) + + def _simulate_rpc_action_plan_update(self, action_plan): + action_plan.save() + return action_plan + + @mock.patch('watcher.common.policy.enforce') + def test_start_action_plan_not_found(self, mock_policy): + mock_policy.return_value = True + uuid = utils.generate_uuid() + response = self.post('/v1/action_plans/%s/%s' % + (uuid, 'start'), expect_errors=True) + self.assertEqual(404, response.status_int) + self.assertEqual('application/json', response.content_type) + self.assertTrue(response.json['error_message']) + + @mock.patch('watcher.common.policy.enforce') + def test_start_action_plan(self, mock_policy): + mock_policy.return_value = True + action = obj_utils.create_test_action( + self.context, id=1) + self.action_plan.state = objects.action_plan.State.SUCCEEDED + response = self.post('/v1/action_plans/%s/%s/' + % (self.action_plan.uuid, 'start'), + expect_errors=True) + self.assertEqual(200, response.status_int) + act_response = self.get_json( + '/actions/%s' % action.uuid, + expect_errors=True) + self.assertEqual(200, act_response.status_int) + self.assertEqual('PENDING', act_response.json['state']) + self.assertEqual('application/json', act_response.content_type) + + class TestPatch(api_base.FunctionalTest): def setUp(self): @@ -562,7 +609,6 @@ class TestPatchStateTransitionOk(api_base.FunctionalTest): '/action_plans/%s' % action_plan.uuid, [{'path': '/state', 'value': self.new_state, 'op': 'replace'}]) updated_ap = self.get_json('/action_plans/%s' % action_plan.uuid) - self.assertNotEqual(self.new_state, initial_ap['state']) self.assertEqual(self.new_state, updated_ap['state']) self.assertEqual('application/json', response.content_type) @@ -636,4 +682,5 @@ class TestActionPlanPolicyEnforcementWithAdminContext(TestListActionPlan, "action_plan:detail": "rule:default", "action_plan:get": "rule:default", "action_plan:get_all": "rule:default", - "action_plan:update": "rule:default"}) + "action_plan:update": "rule:default", + "action_plan:start": "rule:default"})