diff --git a/doc/notification_samples/action-cancel-end.json b/doc/notification_samples/action-cancel-end.json new file mode 100644 index 000000000..708396c7a --- /dev/null +++ b/doc/notification_samples/action-cancel-end.json @@ -0,0 +1,41 @@ +{ + "priority": "INFO", + "payload": { + "watcher_object.namespace": "watcher", + "watcher_object.version": "1.0", + "watcher_object.name": "ActionCancelPayload", + "watcher_object.data": { + "uuid": "10a47dd1-4874-4298-91cf-eff046dbdb8d", + "input_parameters": { + "param2": 2, + "param1": 1 + }, + "fault": null, + "created_at": "2016-10-18T09:52:05Z", + "updated_at": null, + "state": "CANCELLED", + "action_plan": { + "watcher_object.namespace": "watcher", + "watcher_object.version": "1.0", + "watcher_object.name": "TerseActionPlanPayload", + "watcher_object.data": { + "uuid": "76be87bd-3422-43f9-93a0-e85a577e3061", + "global_efficacy": {}, + "created_at": "2016-10-18T09:52:05Z", + "updated_at": null, + "state": "CANCELLING", + "audit_uuid": "10a47dd1-4874-4298-91cf-eff046dbdb8d", + "strategy_uuid": "cb3d0b58-4415-4d90-b75b-1e96878730e3", + "deleted_at": null + } + }, + "parents": [], + "action_type": "nop", + "deleted_at": null + } + }, + "event_type": "action.cancel.end", + "publisher_id": "infra-optim:node0", + "timestamp": "2017-01-01 00:00:00.000000", + "message_id": "530b409c-9b6b-459b-8f08-f93dbfeb4d41" +} diff --git a/doc/notification_samples/action-cancel-error.json b/doc/notification_samples/action-cancel-error.json new file mode 100644 index 000000000..aee7197bb --- /dev/null +++ b/doc/notification_samples/action-cancel-error.json @@ -0,0 +1,51 @@ +{ + "priority": "ERROR", + "payload": { + "watcher_object.namespace": "watcher", + "watcher_object.version": "1.0", + "watcher_object.name": "ActionCancelPayload", + "watcher_object.data": { + "uuid": "10a47dd1-4874-4298-91cf-eff046dbdb8d", + "input_parameters": { + "param2": 2, + "param1": 1 + }, + "fault": { + "watcher_object.namespace": "watcher", + "watcher_object.version": "1.0", + "watcher_object.name": "ExceptionPayload", + "watcher_object.data": { + "module_name": "watcher.tests.notifications.test_action_notification", + "exception": "WatcherException", + "exception_message": "TEST", + "function_name": "test_send_action_cancel_with_error" + } + }, + "created_at": "2016-10-18T09:52:05Z", + "updated_at": null, + "state": "FAILED", + "action_plan": { + "watcher_object.namespace": "watcher", + "watcher_object.version": "1.0", + "watcher_object.name": "TerseActionPlanPayload", + "watcher_object.data": { + "uuid": "76be87bd-3422-43f9-93a0-e85a577e3061", + "global_efficacy": {}, + "created_at": "2016-10-18T09:52:05Z", + "updated_at": null, + "state": "CANCELLING", + "audit_uuid": "10a47dd1-4874-4298-91cf-eff046dbdb8d", + "strategy_uuid": "cb3d0b58-4415-4d90-b75b-1e96878730e3", + "deleted_at": null + } + }, + "parents": [], + "action_type": "nop", + "deleted_at": null + } + }, + "event_type": "action.cancel.error", + "publisher_id": "infra-optim:node0", + "timestamp": "2017-01-01 00:00:00.000000", + "message_id": "530b409c-9b6b-459b-8f08-f93dbfeb4d41" +} diff --git a/doc/notification_samples/action-cancel-start.json b/doc/notification_samples/action-cancel-start.json new file mode 100644 index 000000000..9ef4d80d4 --- /dev/null +++ b/doc/notification_samples/action-cancel-start.json @@ -0,0 +1,41 @@ +{ + "priority": "INFO", + "payload": { + "watcher_object.namespace": "watcher", + "watcher_object.version": "1.0", + "watcher_object.name": "ActionCancelPayload", + "watcher_object.data": { + "uuid": "10a47dd1-4874-4298-91cf-eff046dbdb8d", + "input_parameters": { + "param2": 2, + "param1": 1 + }, + "fault": null, + "created_at": "2016-10-18T09:52:05Z", + "updated_at": null, + "state": "CANCELLING", + "action_plan": { + "watcher_object.namespace": "watcher", + "watcher_object.version": "1.0", + "watcher_object.name": "TerseActionPlanPayload", + "watcher_object.data": { + "uuid": "76be87bd-3422-43f9-93a0-e85a577e3061", + "global_efficacy": {}, + "created_at": "2016-10-18T09:52:05Z", + "updated_at": null, + "state": "CANCELLING", + "audit_uuid": "10a47dd1-4874-4298-91cf-eff046dbdb8d", + "strategy_uuid": "cb3d0b58-4415-4d90-b75b-1e96878730e3", + "deleted_at": null + } + }, + "parents": [], + "action_type": "nop", + "deleted_at": null + } + }, + "event_type": "action.cancel.start", + "publisher_id": "infra-optim:node0", + "timestamp": "2017-01-01 00:00:00.000000", + "message_id": "530b409c-9b6b-459b-8f08-f93dbfeb4d41" +} diff --git a/doc/notification_samples/action_plan-cancel-end.json b/doc/notification_samples/action_plan-cancel-end.json new file mode 100644 index 000000000..7e774406a --- /dev/null +++ b/doc/notification_samples/action_plan-cancel-end.json @@ -0,0 +1,55 @@ +{ + "event_type": "action_plan.cancel.end", + "payload": { + "watcher_object.namespace": "watcher", + "watcher_object.name": "ActionPlanCancelPayload", + "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": "CANCELLED", + "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-cancel-error.json b/doc/notification_samples/action_plan-cancel-error.json new file mode 100644 index 000000000..791491d37 --- /dev/null +++ b/doc/notification_samples/action_plan-cancel-error.json @@ -0,0 +1,65 @@ +{ + "event_type": "action_plan.cancel.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": "ActionPlanCancelPayload", + "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_cancel_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": "SUCCEEDED" + } + }, + "global_efficacy": {}, + "state": "CANCELLING" + } + }, + "timestamp": "2016-10-18 09:52:05.219414" +} diff --git a/doc/notification_samples/action_plan-cancel-start.json b/doc/notification_samples/action_plan-cancel-start.json new file mode 100644 index 000000000..f21a59895 --- /dev/null +++ b/doc/notification_samples/action_plan-cancel-start.json @@ -0,0 +1,55 @@ +{ + "event_type": "action_plan.cancel.start", + "payload": { + "watcher_object.namespace": "watcher", + "watcher_object.name": "ActionPlanCancelPayload", + "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": "CANCELLING", + "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 a63221e12..481f05045 100644 --- a/watcher/applier/action_plan/default.py +++ b/watcher/applier/action_plan/default.py @@ -54,6 +54,7 @@ class DefaultActionPlanHandler(base.BaseActionPlanHandler): applier.execute(self.action_plan_uuid) action_plan.state = objects.action_plan.State.SUCCEEDED + action_plan.save() notifications.action_plan.send_action_notification( self.ctx, action_plan, action=fields.NotificationAction.EXECUTION, @@ -63,17 +64,32 @@ class DefaultActionPlanHandler(base.BaseActionPlanHandler): LOG.exception(e) action_plan.state = objects.action_plan.State.CANCELLED self._update_action_from_pending_to_cancelled() + action_plan.save() + notifications.action_plan.send_cancel_notification( + self.ctx, action_plan, + action=fields.NotificationAction.CANCEL, + phase=fields.NotificationPhase.END) except Exception as e: LOG.exception(e) - 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: - action_plan.save() + action_plan = objects.ActionPlan.get_by_uuid( + self.ctx, self.action_plan_uuid, eager=True) + if action_plan.state == objects.action_plan.State.CANCELLING: + action_plan.state = objects.action_plan.State.FAILED + action_plan.save() + notifications.action_plan.send_cancel_notification( + self.ctx, action_plan, + action=fields.NotificationAction.CANCEL, + priority=fields.NotificationPriority.ERROR, + phase=fields.NotificationPhase.ERROR) + else: + action_plan.state = objects.action_plan.State.FAILED + action_plan.save() + notifications.action_plan.send_action_notification( + self.ctx, action_plan, + action=fields.NotificationAction.EXECUTION, + priority=fields.NotificationPriority.ERROR, + phase=fields.NotificationPhase.ERROR) def _update_action_from_pending_to_cancelled(self): filters = {'action_plan_uuid': self.action_plan_uuid, diff --git a/watcher/applier/workflow_engine/base.py b/watcher/applier/workflow_engine/base.py index b25208cb7..677394490 100644 --- a/watcher/applier/workflow_engine/base.py +++ b/watcher/applier/workflow_engine/base.py @@ -57,6 +57,7 @@ class BaseWorkFlowEngine(loadable.Loadable): self._applier_manager = applier_manager self._action_factory = factory.ActionFactory() self._osc = None + self._is_notified = False @classmethod def get_config_opts(cls): @@ -92,6 +93,17 @@ class BaseWorkFlowEngine(loadable.Loadable): db_action.save() return db_action + def notify_cancel_start(self, action_plan_uuid): + action_plan = objects.ActionPlan.get_by_uuid(self.context, + action_plan_uuid, + eager=True) + if not self._is_notified: + self._is_notified = True + notifications.action_plan.send_cancel_notification( + self._context, action_plan, + action=fields.NotificationAction.CANCEL, + phase=fields.NotificationPhase.START) + @abc.abstractmethod def execute(self, actions): raise NotImplementedError() @@ -157,6 +169,7 @@ class BaseTaskFlowActionContainer(flow_task.Task): fields.NotificationPhase.START) except exception.ActionPlanCancelled as e: LOG.exception(e) + self.engine.notify_cancel_start(action_plan.uuid) raise except Exception as e: LOG.exception(e) @@ -218,6 +231,7 @@ class BaseTaskFlowActionContainer(flow_task.Task): # taskflow will call revert for the action, # we will redirect it to abort. except eventlet.greenlet.GreenletExit: + self.engine.notify_cancel_start(action_plan_object.uuid) raise exception.ActionPlanCancelled(uuid=action_plan_object.uuid) except Exception as e: @@ -249,15 +263,42 @@ class BaseTaskFlowActionContainer(flow_task.Task): action_object = objects.Action.get_by_uuid( self.engine.context, self._db_action.uuid, eager=True) - if action_object.state == objects.action.State.ONGOING: - action_object.state = objects.action.State.CANCELLING + try: + if action_object.state == objects.action.State.ONGOING: + action_object.state = objects.action.State.CANCELLING + action_object.save() + notifications.action.send_cancel_notification( + self.engine.context, action_object, + fields.NotificationAction.CANCEL, + fields.NotificationPhase.START) + action_object = self.abort() + + notifications.action.send_cancel_notification( + self.engine.context, action_object, + fields.NotificationAction.CANCEL, + fields.NotificationPhase.END) + + if action_object.state == objects.action.State.PENDING: + notifications.action.send_cancel_notification( + self.engine.context, action_object, + fields.NotificationAction.CANCEL, + fields.NotificationPhase.START) + action_object.state = objects.action.State.CANCELLED + action_object.save() + notifications.action.send_cancel_notification( + self.engine.context, action_object, + fields.NotificationAction.CANCEL, + fields.NotificationPhase.END) + + except Exception as e: + LOG.exception(e) + action_object.state = objects.action.State.FAILED action_object.save() - self.abort() - elif action_object.state == objects.action.State.PENDING: - action_object.state = objects.action.State.CANCELLED - action_object.save() - else: - pass + notifications.action.send_cancel_notification( + self.engine.context, action_object, + fields.NotificationAction.CANCEL, + fields.NotificationPhase.ERROR, + priority=fields.NotificationPriority.ERROR) def abort(self, *args, **kwargs): - self.do_abort(*args, **kwargs) + return self.do_abort(*args, **kwargs) diff --git a/watcher/notifications/action.py b/watcher/notifications/action.py index 449a0125e..52bf55c32 100644 --- a/watcher/notifications/action.py +++ b/watcher/notifications/action.py @@ -120,6 +120,21 @@ class ActionExecutionPayload(ActionPayload): **kwargs) +@base.WatcherObjectRegistry.register_notification +class ActionCancelPayload(ActionPayload): + # Version 1.0: Initial version + VERSION = '1.0' + fields = { + 'fault': wfields.ObjectField('ExceptionPayload', nullable=True), + } + + def __init__(self, action, action_plan, **kwargs): + super(ActionCancelPayload, self).__init__( + action=action, + action_plan=action_plan, + **kwargs) + + @base.WatcherObjectRegistry.register_notification class ActionDeletePayload(ActionPayload): # Version 1.0: Initial version @@ -178,6 +193,19 @@ class ActionDeleteNotification(notificationbase.NotificationBase): } +@notificationbase.notification_sample('action-cancel-error.json') +@notificationbase.notification_sample('action-cancel-end.json') +@notificationbase.notification_sample('action-cancel-start.json') +@base.WatcherObjectRegistry.register_notification +class ActionCancelNotification(notificationbase.NotificationBase): + # Version 1.0: Initial version + VERSION = '1.0' + + fields = { + 'payload': wfields.ObjectField('ActionCancelPayload') + } + + def _get_action_plan_payload(action): action_plan = None strategy_uuid = None @@ -300,3 +328,33 @@ def send_execution_notification(context, action, notification_action, phase, payload=versioned_payload) notification.emit(context) + + +def send_cancel_notification(context, action, notification_action, phase, + priority=wfields.NotificationPriority.INFO, + service='infra-optim', host=None): + """Emit an action cancel notification.""" + action_plan_payload = _get_action_plan_payload(action) + + fault = None + if phase == wfields.NotificationPhase.ERROR: + fault = exception_notifications.ExceptionPayload.from_exception() + + versioned_payload = ActionCancelPayload( + action=action, + action_plan=action_plan_payload, + fault=fault, + ) + + notification = ActionCancelNotification( + priority=priority, + event_type=notificationbase.EventType( + object='action', + action=notification_action, + phase=phase), + publisher=notificationbase.NotificationPublisher( + host=host or CONF.host, + binary=service), + payload=versioned_payload) + + notification.emit(context) diff --git a/watcher/notifications/action_plan.py b/watcher/notifications/action_plan.py index 97b714b81..5acb70798 100644 --- a/watcher/notifications/action_plan.py +++ b/watcher/notifications/action_plan.py @@ -167,6 +167,22 @@ class ActionPlanDeletePayload(ActionPlanPayload): strategy=strategy) +@base.WatcherObjectRegistry.register_notification +class ActionPlanCancelPayload(ActionPlanPayload): + # Version 1.0: Initial version + VERSION = '1.0' + fields = { + 'fault': wfields.ObjectField('ExceptionPayload', nullable=True), + } + + def __init__(self, action_plan, audit, strategy, **kwargs): + super(ActionPlanCancelPayload, self).__init__( + action_plan=action_plan, + audit=audit, + strategy=strategy, + **kwargs) + + @notificationbase.notification_sample('action_plan-execution-error.json') @notificationbase.notification_sample('action_plan-execution-end.json') @notificationbase.notification_sample('action_plan-execution-start.json') @@ -213,6 +229,19 @@ class ActionPlanDeleteNotification(notificationbase.NotificationBase): } +@notificationbase.notification_sample('action_plan-cancel-error.json') +@notificationbase.notification_sample('action_plan-cancel-end.json') +@notificationbase.notification_sample('action_plan-cancel-start.json') +@base.WatcherObjectRegistry.register_notification +class ActionPlanCancelNotification(notificationbase.NotificationBase): + # Version 1.0: Initial version + VERSION = '1.0' + + fields = { + 'payload': wfields.ObjectField('ActionPlanCancelPayload') + } + + def _get_common_payload(action_plan): audit = None strategy = None @@ -338,3 +367,34 @@ def send_action_notification(context, action_plan, action, phase=None, payload=versioned_payload) notification.emit(context) + + +def send_cancel_notification(context, action_plan, action, phase=None, + priority=wfields.NotificationPriority.INFO, + service='infra-optim', host=None): + """Emit an action_plan cancel 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 = ActionPlanCancelPayload( + action_plan=action_plan, + audit=audit_payload, + strategy=strategy_payload, + fault=fault, + ) + + notification = ActionPlanCancelNotification( + 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/objects/fields.py b/watcher/objects/fields.py index d0df8548a..47a7b2942 100644 --- a/watcher/objects/fields.py +++ b/watcher/objects/fields.py @@ -153,7 +153,10 @@ class NotificationAction(BaseWatcherEnum): PLANNER = 'planner' EXECUTION = 'execution' - ALL = (CREATE, UPDATE, EXCEPTION, DELETE, STRATEGY, PLANNER, EXECUTION) + CANCEL = 'cancel' + + ALL = (CREATE, UPDATE, EXCEPTION, DELETE, STRATEGY, PLANNER, EXECUTION, + CANCEL) class NotificationPriorityField(BaseEnumField): diff --git a/watcher/tests/notifications/test_action_notification.py b/watcher/tests/notifications/test_action_notification.py index 2a4a5b2e4..9c8e38c98 100644 --- a/watcher/tests/notifications/test_action_notification.py +++ b/watcher/tests/notifications/test_action_notification.py @@ -353,3 +353,133 @@ class TestActionNotification(base.DbTestCase): }, notification ) + + def test_send_action_cancel(self): + action = utils.create_test_action( + mock.Mock(), state=objects.action.State.PENDING, + action_type='nop', input_parameters={'param1': 1, 'param2': 2}, + parents=[], action_plan_id=self.action_plan.id) + notifications.action.send_cancel_notification( + mock.MagicMock(), action, 'cancel', phase='start', host='node0') + + # The 1st notification is because we created the audit object. + # The 2nd notification is because we created the action plan object. + self.assertEqual(4, 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.cancel.start', + 'payload': { + 'watcher_object.namespace': 'watcher', + 'watcher_object.version': '1.0', + 'watcher_object.name': 'ActionCancelPayload', + 'watcher_object.data': { + 'uuid': '10a47dd1-4874-4298-91cf-eff046dbdb8d', + 'input_parameters': { + 'param2': 2, + 'param1': 1 + }, + 'created_at': '2016-10-18T09:52:05Z', + 'fault': None, + 'updated_at': None, + 'state': 'PENDING', + 'action_plan': { + 'watcher_object.namespace': 'watcher', + 'watcher_object.version': '1.0', + 'watcher_object.name': 'TerseActionPlanPayload', + 'watcher_object.data': { + 'uuid': '76be87bd-3422-43f9-93a0-e85a577e3061', + 'global_efficacy': {}, + 'created_at': '2016-10-18T09:52:05Z', + 'updated_at': None, + 'state': 'ONGOING', + 'audit_uuid': '10a47dd1-4874-4298' + '-91cf-eff046dbdb8d', + 'strategy_uuid': 'cb3d0b58-4415-4d90' + '-b75b-1e96878730e3', + 'deleted_at': None + } + }, + 'parents': [], + 'action_type': 'nop', + 'deleted_at': None + } + } + }, + notification + ) + + def test_send_action_cancel_with_error(self): + action = utils.create_test_action( + mock.Mock(), state=objects.action.State.FAILED, + action_type='nop', input_parameters={'param1': 1, 'param2': 2}, + parents=[], action_plan_id=self.action_plan.id) + + try: + # This is to load the exception in sys.exc_info() + raise exception.WatcherException("TEST") + except exception.WatcherException: + notifications.action.send_cancel_notification( + mock.MagicMock(), action, 'cancel', phase='error', + host='node0', priority='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.cancel.error', + 'payload': { + 'watcher_object.namespace': 'watcher', + 'watcher_object.version': '1.0', + 'watcher_object.name': 'ActionCancelPayload', + 'watcher_object.data': { + 'uuid': '10a47dd1-4874-4298-91cf-eff046dbdb8d', + 'input_parameters': { + 'param2': 2, + 'param1': 1 + }, + 'created_at': '2016-10-18T09:52:05Z', + 'fault': { + 'watcher_object.data': { + 'exception': u'WatcherException', + 'exception_message': u'TEST', + 'function_name': ( + 'test_send_action_cancel_with_error'), + 'module_name': ( + 'watcher.tests.notifications.' + 'test_action_notification') + }, + 'watcher_object.name': 'ExceptionPayload', + 'watcher_object.namespace': 'watcher', + 'watcher_object.version': '1.0' + }, + 'updated_at': None, + 'state': 'FAILED', + 'action_plan': { + 'watcher_object.namespace': 'watcher', + 'watcher_object.version': '1.0', + 'watcher_object.name': 'TerseActionPlanPayload', + 'watcher_object.data': { + 'uuid': '76be87bd-3422-43f9-93a0-e85a577e3061', + 'global_efficacy': {}, + 'created_at': '2016-10-18T09:52:05Z', + 'updated_at': None, + 'state': 'ONGOING', + 'audit_uuid': '10a47dd1-4874-4298' + '-91cf-eff046dbdb8d', + 'strategy_uuid': 'cb3d0b58-4415-4d90' + '-b75b-1e96878730e3', + 'deleted_at': None + } + }, + 'parents': [], + 'action_type': 'nop', + 'deleted_at': None + } + } + }, + notification + ) diff --git a/watcher/tests/notifications/test_action_plan_notification.py b/watcher/tests/notifications/test_action_plan_notification.py index 47dce1f61..fbdd24ce4 100644 --- a/watcher/tests/notifications/test_action_plan_notification.py +++ b/watcher/tests/notifications/test_action_plan_notification.py @@ -427,3 +427,164 @@ class TestActionPlanNotification(base.DbTestCase): }, notification ) + + def test_send_action_plan_cancel(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_cancel_notification( + mock.MagicMock(), action_plan, host='node0', + action='cancel', 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.cancel.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.1", + "watcher_object.data": { + "interval": None, + "next_run_time": None, + "auto_trigger": False, + "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": "ActionPlanCancelPayload", + "watcher_object.namespace": "watcher", + "watcher_object.version": "1.0" + } + }, + notification + ) + + def test_send_action_plan_cancel_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_cancel_notification( + mock.MagicMock(), action_plan, host='node0', + action='cancel', 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.cancel.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_cancel_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, + "next_run_time": None, + "auto_trigger": False, + "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.1" + }, + "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": "ActionPlanCancelPayload", + "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 d60a14867..de9f236ef 100644 --- a/watcher/tests/notifications/test_notification.py +++ b/watcher/tests/notifications/test_notification.py @@ -250,7 +250,7 @@ class TestNotificationBase(testbase.TestCase): expected_notification_fingerprints = { - 'EventType': '1.3-4258a2c86eca79fd34a7dffe1278eab9', + 'EventType': '1.3-bc4f4bc4a497d789e5a3c30f921edae1', 'ExceptionNotification': '1.0-9b69de0724fda8310d05e18418178866', 'ExceptionPayload': '1.0-4516ae282a55fe2fd5c754967ee6248b', 'NotificationPublisher': '1.0-bbbc1402fb0e443a3eb227cc52b61545', @@ -277,6 +277,8 @@ expected_notification_fingerprints = { 'ActionPlanUpdateNotification': '1.0-9b69de0724fda8310d05e18418178866', 'ActionPlanUpdatePayload': '1.0-3e1a348a0579c6c43c1c3d7257e3f26b', 'ActionPlanActionNotification': '1.0-9b69de0724fda8310d05e18418178866', + 'ActionPlanCancelNotification': '1.0-9b69de0724fda8310d05e18418178866', + 'ActionCancelNotification': '1.0-9b69de0724fda8310d05e18418178866', 'ActionCreateNotification': '1.0-9b69de0724fda8310d05e18418178866', 'ActionCreatePayload': '1.0-519b93b7450319d8928b4b6e6362df31', 'ActionDeleteNotification': '1.0-9b69de0724fda8310d05e18418178866', @@ -287,6 +289,8 @@ expected_notification_fingerprints = { 'ActionStateUpdatePayload': '1.0-1a1b606bf14a2c468800c2b010801ce5', 'ActionUpdateNotification': '1.0-9b69de0724fda8310d05e18418178866', 'ActionUpdatePayload': '1.0-03306c7e7f4d49ac328c261eff6b30b8', + 'ActionPlanCancelPayload': '1.0-d9f134708e06cf2ff2d3b8d522ac2aa8', + 'ActionCancelPayload': '1.0-bff9f820a2abf7bb6d7027b7450157df', 'TerseActionPlanPayload': '1.0-42bf7a5585cc111a9a4dbc008a04c67e', 'ServiceUpdateNotification': '1.0-9b69de0724fda8310d05e18418178866', 'ServicePayload': '1.0-9c5a9bc51e6606e0ec3cf95baf698f4f',