diff --git a/releasenotes/notes/add-force-field-to-audit-4bcaeedfe27233ad.yaml b/releasenotes/notes/add-force-field-to-audit-4bcaeedfe27233ad.yaml new file mode 100644 index 000000000..5360beef7 --- /dev/null +++ b/releasenotes/notes/add-force-field-to-audit-4bcaeedfe27233ad.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + Add force field to Audit. User can set --force to enable the new option when + launching audit. If force is True, audit will be executed despite of ongoing + actionplan. The new audit may create a wrong actionplan if they use the same + data model. diff --git a/watcher/api/controllers/v1/audit.py b/watcher/api/controllers/v1/audit.py index 3f67f4164..74df4a4f0 100644 --- a/watcher/api/controllers/v1/audit.py +++ b/watcher/api/controllers/v1/audit.py @@ -73,6 +73,8 @@ def hide_fields_in_newer_versions(obj): if not api_utils.allow_start_end_audit_time(): obj.start_time = wtypes.Unset obj.end_time = wtypes.Unset + if not api_utils.allow_force(): + obj.force = wtypes.Unset class AuditPostType(wtypes.Base): @@ -194,7 +196,8 @@ class AuditPostType(wtypes.Base): scope=self.scope, auto_trigger=self.auto_trigger, start_time=self.start_time, - end_time=self.end_time) + end_time=self.end_time, + force=self.force) class AuditPatchType(types.JsonPatchType): diff --git a/watcher/api/controllers/v1/utils.py b/watcher/api/controllers/v1/utils.py index 55e83d001..123c8e6e9 100644 --- a/watcher/api/controllers/v1/utils.py +++ b/watcher/api/controllers/v1/utils.py @@ -165,3 +165,12 @@ def allow_start_end_audit_time(): audits. """ return pecan.request.version.minor >= versions.MINOR_1_START_END_TIMING + + +def allow_force(): + """Check if we should support optional force attribute for Audit. + + Version 1.2 of the API added support for forced audits that allows to + launch audit when other action plan is ongoing. + """ + return pecan.request.version.minor >= versions.MINOR_2_FORCE diff --git a/watcher/api/controllers/v1/versions.py b/watcher/api/controllers/v1/versions.py index eec13f9fe..6de00d196 100644 --- a/watcher/api/controllers/v1/versions.py +++ b/watcher/api/controllers/v1/versions.py @@ -22,11 +22,13 @@ BASE_VERSION = 1 # # v1.0: corresponds to Rocky API # v1.1: Add start/end time for continuous audit +# v1.2: Add force field to audit MINOR_0_ROCKY = 0 MINOR_1_START_END_TIMING = 1 +MINOR_2_FORCE = 2 -MINOR_MAX_VERSION = MINOR_1_START_END_TIMING +MINOR_MAX_VERSION = MINOR_2_FORCE # String representations of the minor and maximum versions _MIN_VERSION_STRING = '{}.{}'.format(BASE_VERSION, MINOR_0_ROCKY) diff --git a/watcher/decision_engine/audit/base.py b/watcher/decision_engine/audit/base.py index 1d4e25adc..229c90290 100644 --- a/watcher/decision_engine/audit/base.py +++ b/watcher/decision_engine/audit/base.py @@ -121,7 +121,10 @@ class AuditHandler(BaseAuditHandler): def pre_execute(self, audit, request_context): LOG.debug("Trigger audit %s", audit.uuid) - self.check_ongoing_action_plans(request_context) + # If audit.force is true, audit will be executed + # despite of ongoing actionplan + if not audit.force: + self.check_ongoing_action_plans(request_context) # Write hostname that will execute this audit. audit.hostname = CONF.host # change state of the audit to ONGOING diff --git a/watcher/tests/api/v1/test_audits.py b/watcher/tests/api/v1/test_audits.py index c40d16dfb..14137f62a 100644 --- a/watcher/tests/api/v1/test_audits.py +++ b/watcher/tests/api/v1/test_audits.py @@ -950,6 +950,39 @@ class TestPost(api_base.FunctionalTest): self.assertIn(expected_error_msg, response.json['error_message']) assert not mock_trigger_audit.called + @mock.patch.object(deapi.DecisionEngineAPI, 'trigger_audit') + def test_create_audit_with_force_false(self, mock_trigger_audit): + mock_trigger_audit.return_value = mock.ANY + + audit_dict = post_get_test_audit( + params_to_exclude=['uuid', 'state', 'interval', 'scope', + 'next_run_time', 'hostname', 'goal']) + + response = self.post_json( + '/audits', + audit_dict, + headers={'OpenStack-API-Version': 'infra-optim 1.2'}) + self.assertEqual('application/json', response.content_type) + self.assertEqual(201, response.status_int) + self.assertFalse(response.json['force']) + + @mock.patch.object(deapi.DecisionEngineAPI, 'trigger_audit') + def test_create_audit_with_force_true(self, mock_trigger_audit): + mock_trigger_audit.return_value = mock.ANY + + audit_dict = post_get_test_audit( + params_to_exclude=['uuid', 'state', 'interval', 'scope', + 'next_run_time', 'hostname', 'goal']) + + audit_dict['force'] = True + response = self.post_json( + '/audits', + audit_dict, + headers={'OpenStack-API-Version': 'infra-optim 1.2'}) + self.assertEqual('application/json', response.content_type) + self.assertEqual(201, response.status_int) + self.assertTrue(response.json['force']) + class TestDelete(api_base.FunctionalTest): diff --git a/watcher/tests/decision_engine/audit/test_audit_handlers.py b/watcher/tests/decision_engine/audit/test_audit_handlers.py index de1000116..5acacfc9f 100644 --- a/watcher/tests/decision_engine/audit/test_audit_handlers.py +++ b/watcher/tests/decision_engine/audit/test_audit_handlers.py @@ -227,6 +227,13 @@ class TestAutoTriggerActionPlan(base.DbTestCase): mock_applier.assert_called_once_with(self.context, self.recommended_action_plan.uuid) + @mock.patch.object(oneshot.OneShotAuditHandler, 'do_execute') + def test_trigger_audit_with_force(self, mock_do_execute): + audit_handler = oneshot.OneShotAuditHandler() + self.audit.force = True + audit_handler.execute(self.audit, self.context) + self.assertTrue(mock_do_execute.called) + class TestContinuousAuditHandler(base.DbTestCase):