From d49c6c16a69575c9f7795d3c99bb98d1e4d014bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vincent=20Fran=C3=A7oise?= Date: Tue, 24 Jan 2017 14:28:15 +0100 Subject: [PATCH] Added action_plan.execution.* actions Partially Implements: blueprint action-plan-versioned-notifications-api Change-Id: I9bd346c19f1cafcaa720de554fd9c056c76de050 --- .../action_plan-execution-end.json | 55 ++++++ .../action_plan-execution-error.json | 65 +++++++ .../action_plan-execution-start.json | 55 ++++++ watcher/applier/action_plan/default.py | 36 ++-- watcher/applier/default.py | 4 +- watcher/notifications/action_plan.py | 45 +++++ .../test_default_action_handler.py | 61 ++++++- .../test_action_plan_notification.py | 158 ++++++++++++++++++ .../tests/notifications/test_notification.py | 1 + 9 files changed, 463 insertions(+), 17 deletions(-) create mode 100644 doc/notification_samples/action_plan-execution-end.json create mode 100644 doc/notification_samples/action_plan-execution-error.json create mode 100644 doc/notification_samples/action_plan-execution-start.json diff --git a/doc/notification_samples/action_plan-execution-end.json b/doc/notification_samples/action_plan-execution-end.json new file mode 100644 index 000000000..2fee7f083 --- /dev/null +++ b/doc/notification_samples/action_plan-execution-end.json @@ -0,0 +1,55 @@ +{ + "event_type": "action_plan.execution.end", + "payload": { + "watcher_object.namespace": "watcher", + "watcher_object.name": "ActionPlanActionPayload", + "watcher_object.version": "1.0", + "watcher_object.data": { + "created_at": "2016-10-18T09:52:05Z", + "deleted_at": null, + "audit_uuid": "10a47dd1-4874-4298-91cf-eff046dbdb8d", + "audit": { + "watcher_object.namespace": "watcher", + "watcher_object.name": "TerseAuditPayload", + "watcher_object.version": "1.0", + "watcher_object.data": { + "created_at": "2016-10-18T09:52:05Z", + "deleted_at": null, + "uuid": "10a47dd1-4874-4298-91cf-eff046dbdb8d", + "goal_uuid": "bc830f84-8ae3-4fc6-8bc6-e3dd15e8b49a", + "strategy_uuid": "75234dfe-87e3-4f11-a0e0-3c3305d86a39", + "scope": [], + "audit_type": "ONESHOT", + "state": "SUCCEEDED", + "parameters": {}, + "interval": null, + "updated_at": null + } + }, + "uuid": "76be87bd-3422-43f9-93a0-e85a577e3061", + "fault": null, + "state": "ONGOING", + "global_efficacy": {}, + "strategy_uuid": "cb3d0b58-4415-4d90-b75b-1e96878730e3", + "strategy": { + "watcher_object.namespace": "watcher", + "watcher_object.name": "StrategyPayload", + "watcher_object.version": "1.0", + "watcher_object.data": { + "created_at": "2016-10-18T09:52:05Z", + "deleted_at": null, + "name": "TEST", + "uuid": "cb3d0b58-4415-4d90-b75b-1e96878730e3", + "parameters_spec": {}, + "display_name": "test strategy", + "updated_at": null + } + }, + "updated_at": null + } + }, + "priority": "INFO", + "message_id": "3984dc2b-8aef-462b-a220-8ae04237a56e", + "timestamp": "2016-10-18 09:52:05.219414", + "publisher_id": "infra-optim:node0" +} diff --git a/doc/notification_samples/action_plan-execution-error.json b/doc/notification_samples/action_plan-execution-error.json new file mode 100644 index 000000000..466c67f8e --- /dev/null +++ b/doc/notification_samples/action_plan-execution-error.json @@ -0,0 +1,65 @@ +{ + "event_type": "action_plan.execution.error", + "publisher_id": "infra-optim:node0", + "priority": "ERROR", + "message_id": "9a45c5ae-0e21-4300-8fa0-5555d52a66d9", + "payload": { + "watcher_object.version": "1.0", + "watcher_object.namespace": "watcher", + "watcher_object.name": "ActionPlanActionPayload", + "watcher_object.data": { + "fault": { + "watcher_object.version": "1.0", + "watcher_object.namespace": "watcher", + "watcher_object.name": "ExceptionPayload", + "watcher_object.data": { + "exception_message": "TEST", + "module_name": "watcher.tests.notifications.test_action_plan_notification", + "function_name": "test_send_action_plan_action_with_error", + "exception": "WatcherException" + } + }, + "uuid": "76be87bd-3422-43f9-93a0-e85a577e3061", + "created_at": "2016-10-18T09:52:05Z", + "strategy_uuid": "cb3d0b58-4415-4d90-b75b-1e96878730e3", + "strategy": { + "watcher_object.version": "1.0", + "watcher_object.namespace": "watcher", + "watcher_object.name": "StrategyPayload", + "watcher_object.data": { + "uuid": "cb3d0b58-4415-4d90-b75b-1e96878730e3", + "created_at": "2016-10-18T09:52:05Z", + "name": "TEST", + "updated_at": null, + "display_name": "test strategy", + "parameters_spec": {}, + "deleted_at": null + } + }, + "updated_at": null, + "deleted_at": null, + "audit_uuid": "10a47dd1-4874-4298-91cf-eff046dbdb8d", + "audit": { + "watcher_object.version": "1.0", + "watcher_object.namespace": "watcher", + "watcher_object.name": "TerseAuditPayload", + "watcher_object.data": { + "parameters": {}, + "uuid": "10a47dd1-4874-4298-91cf-eff046dbdb8d", + "goal_uuid": "bc830f84-8ae3-4fc6-8bc6-e3dd15e8b49a", + "strategy_uuid": "75234dfe-87e3-4f11-a0e0-3c3305d86a39", + "created_at": "2016-10-18T09:52:05Z", + "scope": [], + "updated_at": null, + "audit_type": "ONESHOT", + "interval": null, + "deleted_at": null, + "state": "PENDING" + } + }, + "global_efficacy": {}, + "state": "ONGOING" + } + }, + "timestamp": "2016-10-18 09:52:05.219414" +} diff --git a/doc/notification_samples/action_plan-execution-start.json b/doc/notification_samples/action_plan-execution-start.json new file mode 100644 index 000000000..704516211 --- /dev/null +++ b/doc/notification_samples/action_plan-execution-start.json @@ -0,0 +1,55 @@ +{ + "event_type": "action_plan.execution.start", + "payload": { + "watcher_object.namespace": "watcher", + "watcher_object.name": "ActionPlanActionPayload", + "watcher_object.version": "1.0", + "watcher_object.data": { + "created_at": "2016-10-18T09:52:05Z", + "deleted_at": null, + "audit_uuid": "10a47dd1-4874-4298-91cf-eff046dbdb8d", + "audit": { + "watcher_object.namespace": "watcher", + "watcher_object.name": "TerseAuditPayload", + "watcher_object.version": "1.0", + "watcher_object.data": { + "created_at": "2016-10-18T09:52:05Z", + "deleted_at": null, + "uuid": "10a47dd1-4874-4298-91cf-eff046dbdb8d", + "goal_uuid": "bc830f84-8ae3-4fc6-8bc6-e3dd15e8b49a", + "strategy_uuid": "75234dfe-87e3-4f11-a0e0-3c3305d86a39", + "scope": [], + "audit_type": "ONESHOT", + "state": "PENDING", + "parameters": {}, + "interval": null, + "updated_at": null + } + }, + "uuid": "76be87bd-3422-43f9-93a0-e85a577e3061", + "fault": null, + "state": "ONGOING", + "global_efficacy": {}, + "strategy_uuid": "cb3d0b58-4415-4d90-b75b-1e96878730e3", + "strategy": { + "watcher_object.namespace": "watcher", + "watcher_object.name": "StrategyPayload", + "watcher_object.version": "1.0", + "watcher_object.data": { + "created_at": "2016-10-18T09:52:05Z", + "deleted_at": null, + "name": "TEST", + "uuid": "cb3d0b58-4415-4d90-b75b-1e96878730e3", + "parameters_spec": {}, + "display_name": "test strategy", + "updated_at": null + } + }, + "updated_at": null + } + }, + "priority": "INFO", + "message_id": "3984dc2b-8aef-462b-a220-8ae04237a56e", + "timestamp": "2016-10-18 09:52:05.219414", + "publisher_id": "infra-optim:node0" +} diff --git a/watcher/applier/action_plan/default.py b/watcher/applier/action_plan/default.py index 36437e208..2462f8454 100644 --- a/watcher/applier/action_plan/default.py +++ b/watcher/applier/action_plan/default.py @@ -20,33 +20,47 @@ from oslo_log import log from watcher.applier.action_plan import base from watcher.applier import default +from watcher import notifications from watcher import objects +from watcher.objects import fields LOG = log.getLogger(__name__) class DefaultActionPlanHandler(base.BaseActionPlanHandler): + def __init__(self, context, service, action_plan_uuid): super(DefaultActionPlanHandler, self).__init__() self.ctx = context self.service = service self.action_plan_uuid = action_plan_uuid - def update_action_plan(self, uuid, state): - action_plan = objects.ActionPlan.get_by_uuid( - self.ctx, uuid, eager=True) - action_plan.state = state - action_plan.save() - def execute(self): try: - self.update_action_plan(self.action_plan_uuid, - objects.action_plan.State.ONGOING) + action_plan = objects.ActionPlan.get_by_uuid( + self.ctx, self.action_plan_uuid, eager=True) + action_plan.state = objects.action_plan.State.ONGOING + action_plan.save() + notifications.action_plan.send_action_notification( + self.ctx, action_plan, + action=fields.NotificationAction.EXECUTION, + phase=fields.NotificationPhase.START) + applier = default.DefaultApplier(self.ctx, self.service) applier.execute(self.action_plan_uuid) - state = objects.action_plan.State.SUCCEEDED + + action_plan.state = objects.action_plan.State.SUCCEEDED + notifications.action_plan.send_action_notification( + self.ctx, action_plan, + action=fields.NotificationAction.EXECUTION, + phase=fields.NotificationPhase.END) except Exception as e: LOG.exception(e) - state = objects.action_plan.State.FAILED + action_plan.state = objects.action_plan.State.FAILED + notifications.action_plan.send_action_notification( + self.ctx, action_plan, + action=fields.NotificationAction.EXECUTION, + priority=fields.NotificationPriority.ERROR, + phase=fields.NotificationPhase.ERROR) finally: - self.update_action_plan(self.action_plan_uuid, state) + action_plan.save() diff --git a/watcher/applier/default.py b/watcher/applier/default.py index 6aec92286..62b5dce36 100644 --- a/watcher/applier/default.py +++ b/watcher/applier/default.py @@ -56,7 +56,7 @@ class DefaultApplier(base.BaseApplier): def execute(self, action_plan_uuid): LOG.debug("Executing action plan %s ", action_plan_uuid) + filters = {'action_plan_uuid': action_plan_uuid} - actions = objects.Action.list(self.context, - filters=filters) + actions = objects.Action.list(self.context, filters=filters) return self.engine.execute(actions) diff --git a/watcher/notifications/action_plan.py b/watcher/notifications/action_plan.py index fe5f2ef3b..1acc2057c 100644 --- a/watcher/notifications/action_plan.py +++ b/watcher/notifications/action_plan.py @@ -22,6 +22,7 @@ from watcher.common import context as wcontext from watcher.common import exception from watcher.notifications import audit as audit_notifications from watcher.notifications import base as notificationbase +from watcher.notifications import exception as exception_notifications from watcher.notifications import strategy as strategy_notifications from watcher import objects from watcher.objects import base @@ -138,6 +139,19 @@ class ActionPlanDeletePayload(ActionPlanPayload): strategy=strategy) +@notificationbase.notification_sample('action_plan-execution-error.json') +@notificationbase.notification_sample('action_plan-execution-end.json') +@notificationbase.notification_sample('action_plan-execution-start.json') +@base.WatcherObjectRegistry.register_notification +class ActionPlanActionNotification(notificationbase.NotificationBase): + # Version 1.0: Initial version + VERSION = '1.0' + + fields = { + 'payload': wfields.ObjectField('ActionPlanActionPayload') + } + + @notificationbase.notification_sample('action_plan-create.json') @base.WatcherObjectRegistry.register_notification class ActionPlanCreateNotification(notificationbase.NotificationBase): @@ -265,3 +279,34 @@ def send_delete(context, action_plan, service='infra-optim', host=None): payload=versioned_payload) notification.emit(context) + + +def send_action_notification(context, action_plan, action, phase=None, + priority=wfields.NotificationPriority.INFO, + service='infra-optim', host=None): + """Emit an action_plan action notification.""" + audit_payload, strategy_payload = _get_common_payload(action_plan) + + fault = None + if phase == wfields.NotificationPhase.ERROR: + fault = exception_notifications.ExceptionPayload.from_exception() + + versioned_payload = ActionPlanActionPayload( + action_plan=action_plan, + audit=audit_payload, + strategy=strategy_payload, + fault=fault, + ) + + notification = ActionPlanActionNotification( + priority=priority, + event_type=notificationbase.EventType( + object='action_plan', + action=action, + phase=phase), + publisher=notificationbase.NotificationPublisher( + host=host or CONF.host, + binary=service), + payload=versioned_payload) + + notification.emit(context) diff --git a/watcher/tests/applier/action_plan/test_default_action_handler.py b/watcher/tests/applier/action_plan/test_default_action_handler.py index ae5ce9bed..94827d738 100644 --- a/watcher/tests/applier/action_plan/test_default_action_handler.py +++ b/watcher/tests/applier/action_plan/test_default_action_handler.py @@ -18,6 +18,9 @@ import mock from watcher.applier.action_plan import default +from watcher.applier import default as ap_applier +from watcher import notifications +from watcher import objects from watcher.objects import action_plan as ap_objects from watcher.tests.db import base from watcher.tests.objects import utils as obj_utils @@ -25,17 +28,67 @@ from watcher.tests.objects import utils as obj_utils class TestDefaultActionPlanHandler(base.DbTestCase): + class FakeApplierException(Exception): + pass + def setUp(self): super(TestDefaultActionPlanHandler, self).setUp() + + p_action_plan_notifications = mock.patch.object( + notifications, 'action_plan', autospec=True) + self.m_action_plan_notifications = p_action_plan_notifications.start() + self.addCleanup(p_action_plan_notifications.stop) + 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) - def test_launch_action_plan(self): + @mock.patch.object(objects.ActionPlan, "get_by_uuid") + def test_launch_action_plan(self, m_get_action_plan): + m_get_action_plan.return_value = self.action_plan command = default.DefaultActionPlanHandler( self.context, mock.MagicMock(), self.action_plan.uuid) command.execute() - action_plan = ap_objects.ActionPlan.get_by_uuid( - self.context, self.action_plan.uuid) - self.assertEqual(ap_objects.State.SUCCEEDED, action_plan.state) + + expected_calls = [ + mock.call(self.context, self.action_plan, + action=objects.fields.NotificationAction.EXECUTION, + phase=objects.fields.NotificationPhase.START), + mock.call(self.context, self.action_plan, + action=objects.fields.NotificationAction.EXECUTION, + phase=objects.fields.NotificationPhase.END)] + + self.assertEqual(ap_objects.State.SUCCEEDED, self.action_plan.state) + + self.assertEqual( + expected_calls, + self.m_action_plan_notifications + .send_action_notification + .call_args_list) + + @mock.patch.object(ap_applier.DefaultApplier, "execute") + @mock.patch.object(objects.ActionPlan, "get_by_uuid") + def test_launch_action_plan_with_error(self, m_get_action_plan, m_execute): + m_get_action_plan.return_value = self.action_plan + m_execute.side_effect = self.FakeApplierException + command = default.DefaultActionPlanHandler( + self.context, mock.MagicMock(), self.action_plan.uuid) + command.execute() + + expected_calls = [ + mock.call(self.context, self.action_plan, + action=objects.fields.NotificationAction.EXECUTION, + phase=objects.fields.NotificationPhase.START), + mock.call(self.context, self.action_plan, + action=objects.fields.NotificationAction.EXECUTION, + priority=objects.fields.NotificationPriority.ERROR, + phase=objects.fields.NotificationPhase.ERROR)] + + self.assertEqual(ap_objects.State.FAILED, self.action_plan.state) + + self.assertEqual( + expected_calls, + self.m_action_plan_notifications + .send_action_notification + .call_args_list) diff --git a/watcher/tests/notifications/test_action_plan_notification.py b/watcher/tests/notifications/test_action_plan_notification.py index 384a37b43..b3e35ae5e 100644 --- a/watcher/tests/notifications/test_action_plan_notification.py +++ b/watcher/tests/notifications/test_action_plan_notification.py @@ -259,3 +259,161 @@ class TestActionPlanNotification(base.DbTestCase): }, payload ) + + def test_send_action_plan_action(self): + action_plan = utils.create_test_action_plan( + mock.Mock(), state=objects.action_plan.State.ONGOING, + audit_id=self.audit.id, strategy_id=self.strategy.id, + audit=self.audit, strategy=self.strategy) + notifications.action_plan.send_action_notification( + mock.MagicMock(), action_plan, host='node0', + action='execution', phase='start') + + # The 1st notification is because we created the audit object. + # The 2nd notification is because we created the action plan object. + self.assertEqual(3, self.m_notifier.info.call_count) + notification = self.m_notifier.info.call_args[1] + + self.assertEqual("infra-optim:node0", self.m_notifier.publisher_id) + self.assertDictEqual( + { + "event_type": "action_plan.execution.start", + "payload": { + "watcher_object.data": { + "created_at": "2016-10-18T09:52:05Z", + "deleted_at": None, + "fault": None, + "audit_uuid": "10a47dd1-4874-4298-91cf-eff046dbdb8d", + "audit": { + "watcher_object.namespace": "watcher", + "watcher_object.name": "TerseAuditPayload", + "watcher_object.version": "1.0", + "watcher_object.data": { + "interval": None, + "parameters": {}, + "uuid": "10a47dd1-4874-4298-91cf-eff046dbdb8d", + "strategy_uuid": None, + "goal_uuid": ( + "f7ad87ae-4298-91cf-93a0-f35a852e3652"), + "deleted_at": None, + "scope": [], + "state": "PENDING", + "updated_at": None, + "created_at": "2016-10-18T09:52:05Z", + "audit_type": "ONESHOT" + } + }, + "global_efficacy": {}, + "state": "ONGOING", + "strategy_uuid": ( + "cb3d0b58-4415-4d90-b75b-1e96878730e3"), + "strategy": { + "watcher_object.data": { + "created_at": "2016-10-18T09:52:05Z", + "deleted_at": None, + "display_name": "test strategy", + "name": "TEST", + "parameters_spec": {}, + "updated_at": None, + "uuid": "cb3d0b58-4415-4d90-b75b-1e96878730e3" + }, + "watcher_object.name": "StrategyPayload", + "watcher_object.namespace": "watcher", + "watcher_object.version": "1.0" + }, + "updated_at": None, + "uuid": "76be87bd-3422-43f9-93a0-e85a577e3061" + }, + "watcher_object.name": "ActionPlanActionPayload", + "watcher_object.namespace": "watcher", + "watcher_object.version": "1.0" + } + }, + notification + ) + + def test_send_action_plan_action_with_error(self): + action_plan = utils.create_test_action_plan( + mock.Mock(), state=objects.action_plan.State.ONGOING, + audit_id=self.audit.id, strategy_id=self.strategy.id, + audit=self.audit, strategy=self.strategy) + + try: + # This is to load the exception in sys.exc_info() + raise exception.WatcherException("TEST") + except exception.WatcherException: + notifications.action_plan.send_action_notification( + mock.MagicMock(), action_plan, host='node0', + action='execution', priority='error', phase='error') + + self.assertEqual(1, self.m_notifier.error.call_count) + notification = self.m_notifier.error.call_args[1] + self.assertEqual("infra-optim:node0", self.m_notifier.publisher_id) + self.assertDictEqual( + { + "event_type": "action_plan.execution.error", + "payload": { + "watcher_object.data": { + "created_at": "2016-10-18T09:52:05Z", + "deleted_at": None, + "fault": { + "watcher_object.data": { + "exception": "WatcherException", + "exception_message": "TEST", + "function_name": ( + "test_send_action_plan_action_with_error"), + "module_name": "watcher.tests.notifications." + "test_action_plan_notification" + }, + "watcher_object.name": "ExceptionPayload", + "watcher_object.namespace": "watcher", + "watcher_object.version": "1.0" + }, + "audit_uuid": "10a47dd1-4874-4298-91cf-eff046dbdb8d", + "audit": { + "watcher_object.data": { + "interval": None, + "parameters": {}, + "uuid": "10a47dd1-4874-4298-91cf-eff046dbdb8d", + "strategy_uuid": None, + "goal_uuid": ( + "f7ad87ae-4298-91cf-93a0-f35a852e3652"), + "deleted_at": None, + "scope": [], + "state": "PENDING", + "updated_at": None, + "created_at": "2016-10-18T09:52:05Z", + "audit_type": "ONESHOT" + }, + "watcher_object.name": "TerseAuditPayload", + "watcher_object.namespace": "watcher", + "watcher_object.version": "1.0" + }, + "global_efficacy": {}, + "state": "ONGOING", + "strategy_uuid": ( + "cb3d0b58-4415-4d90-b75b-1e96878730e3"), + "strategy": { + "watcher_object.data": { + "created_at": "2016-10-18T09:52:05Z", + "deleted_at": None, + "display_name": "test strategy", + "name": "TEST", + "parameters_spec": {}, + "updated_at": None, + "uuid": "cb3d0b58-4415-4d90-b75b-1e96878730e3" + }, + "watcher_object.name": "StrategyPayload", + "watcher_object.namespace": "watcher", + "watcher_object.version": "1.0" + }, + "updated_at": None, + "uuid": "76be87bd-3422-43f9-93a0-e85a577e3061" + }, + "watcher_object.name": "ActionPlanActionPayload", + "watcher_object.namespace": "watcher", + "watcher_object.version": "1.0" + } + }, + notification + ) diff --git a/watcher/tests/notifications/test_notification.py b/watcher/tests/notifications/test_notification.py index 828606a05..8331964dd 100644 --- a/watcher/tests/notifications/test_notification.py +++ b/watcher/tests/notifications/test_notification.py @@ -276,6 +276,7 @@ expected_notification_fingerprints = { 'ActionPlanStateUpdatePayload': '1.0-1a1b606bf14a2c468800c2b010801ce5', 'ActionPlanUpdateNotification': '1.0-9b69de0724fda8310d05e18418178866', 'ActionPlanUpdatePayload': '1.0-7912a45fe53775c721f42aa87f06a023', + 'ActionPlanActionNotification': '1.0-9b69de0724fda8310d05e18418178866', }