From b9b2da6be6de135fb031e55326269f447661b0ca Mon Sep 17 00:00:00 2001 From: liusheng Date: Fri, 6 Jan 2017 18:05:11 +0800 Subject: [PATCH] Versioned notification: Add instance action notification base This change added the base of instance action notifications. Partially-Implements: bp support-versioned-notification Change-Id: Ia96cc378c0e2f0d50178bc4665c4ca4905b15563 --- mogan/notifications/base.py | 63 ++++++++++++++ mogan/notifications/objects/exception.py | 53 ++++++++++++ mogan/notifications/objects/instance.py | 84 +++++++++++++++++++ mogan/objects/fields.py | 2 +- .../unit/notifications/test_notification.py | 38 +++++++++ 5 files changed, 239 insertions(+), 1 deletion(-) create mode 100644 mogan/notifications/base.py create mode 100644 mogan/notifications/objects/exception.py create mode 100644 mogan/notifications/objects/instance.py diff --git a/mogan/notifications/base.py b/mogan/notifications/base.py new file mode 100644 index 00000000..05e4631c --- /dev/null +++ b/mogan/notifications/base.py @@ -0,0 +1,63 @@ +# Copyright 2016 Huawei Technologies Co.,LTD. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Functionality related to notifications common to multiple layers of +the system. +""" + +from mogan.notifications.objects import base as notification_base +from mogan.notifications.objects import exception as notification_exception +from mogan.notifications.objects import instance as instance_notification +from mogan.objects import fields + + +def _get_fault_and_priority_from_exc(exception): + fault = None + priority = fields.NotificationPriority.INFO + + if exception: + priority = fields.NotificationPriority.ERROR + fault = notification_exception.ExceptionPayload.from_exception( + exception) + + return fault, priority + + +def notify_about_instance_action(context, instance, host, action, phase=None, + binary='mogan-engine', exception=None): + """Send versioned notification about the action made on the instance + :param instance: the instance which the action performed on + :param host: the host emitting the notification + :param action: the name of the action + :param phase: the phase of the action + :param binary: the binary emitting the notification + :param exception: the thrown exception (used in error notifications) + """ + + fault, priority = _get_fault_and_priority_from_exc(exception) + + payload = instance_notification.InstanceActionPayload( + instance=instance, + fault=fault) + notification = instance_notification.InstanceActionNotification( + context=context, + priority=priority, + publisher=notification_base.NotificationPublisher( + context=context, host=host, binary=binary), + event_type=notification_base.EventType( + object='instance', + action=action, + phase=phase), + payload=payload) + notification.emit(context) diff --git a/mogan/notifications/objects/exception.py b/mogan/notifications/objects/exception.py new file mode 100644 index 00000000..22b6e4a2 --- /dev/null +++ b/mogan/notifications/objects/exception.py @@ -0,0 +1,53 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +import inspect + +import six + +from mogan.notifications.objects import base +from mogan.objects import base as moban_base +from mogan.objects import fields + + +@moban_base.MoganObjectRegistry.register_notification +class ExceptionPayload(base.NotificationPayloadBase): + # Version 1.0: Initial version + VERSION = '1.0' + fields = { + 'module_name': fields.StringField(), + 'function_name': fields.StringField(), + 'exception': fields.StringField(), + 'exception_message': fields.StringField() + } + + @classmethod + def from_exception(cls, fault): + trace = inspect.trace()[-1] + # TODO(gibi): apply strutils.mask_password on exception_message and + # consider emitting the exception_message only if the safe flag is + # true in the exception like in the REST API + module = inspect.getmodule(trace[0]) + module_name = module.__name__ if module else 'unknown' + return cls( + function_name=trace[3], + module_name=module_name, + exception=fault.__class__.__name__, + exception_message=six.text_type(fault)) + + +@moban_base.MoganObjectRegistry.register_notification +class ExceptionNotification(base.NotificationBase): + # Version 1.0: Initial version + VERSION = '1.0' + fields = { + 'payload': fields.ObjectField('ExceptionPayload') + } diff --git a/mogan/notifications/objects/instance.py b/mogan/notifications/objects/instance.py new file mode 100644 index 00000000..1b2c8800 --- /dev/null +++ b/mogan/notifications/objects/instance.py @@ -0,0 +1,84 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from mogan.notifications.objects import base +from mogan.objects import base as mogan_base +from mogan.objects import fields + + +@mogan_base.MoganObjectRegistry.register_notification +class InstancePayload(base.NotificationPayloadBase): + SCHEMA = { + 'name': ('instance', 'name'), + 'uuid': ('instance', 'uuid'), + 'user_id': ('instance', 'user_id'), + 'project_id': ('instance', 'project_id'), + 'availability_zone': ('instance', 'availability_zone'), + 'image_uuid': ('instance', 'image_uuid'), + 'created_at': ('instance', 'created_at'), + 'launched_at': ('instance', 'launched_at'), + 'updated_at': ('instance', 'updated_at'), + 'status': ('instance', 'status'), + # TODO(liusheng) the instance object hasn't power_state attribute + # 'power_state': ('instance', 'power_state'), + 'instance_type_uuid': ('instance', 'instance_type_uuid'), + 'description': ('instance', 'description') + } + # Version 1.0: Initial version + VERSION = '1.0' + fields = { + 'name': fields.StringField(nullable=False), + 'uuid': fields.UUIDField(nullable=False), + 'user_id': fields.StringField(nullable=True), + 'project_id': fields.StringField(nullable=True), + 'description': fields.StringField(nullable=True), + 'instance_type_uuid': fields.UUIDField(nullable=False), + 'image_uuid': fields.UUIDField(nullable=True), + 'availability_zone': fields.StringField(nullable=True), + # 'power_state': fields.StringField(nullable=True), + 'created_at': fields.DateTimeField(nullable=True), + 'launched_at': fields.DateTimeField(nullable=True), + 'updated_at': fields.DateTimeField(nullable=True), + 'status': fields.StringField(nullable=True), + # 'network_info' + # 'extra' + } + + def __init__(self, instance, **kwargs): + super(InstancePayload, self).__init__(**kwargs) + self.populate_schema(instance=instance) + + +@mogan_base.MoganObjectRegistry.register_notification +class InstanceActionPayload(InstancePayload): + # No SCHEMA as all the additional fields are calculated + + VERSION = '1.0' + fields = { + 'fault': fields.ObjectField('ExceptionPayload', nullable=True), + } + + def __init__(self, instance, fault, **kwargs): + super(InstanceActionPayload, self).__init__( + instance=instance, + fault=fault, + **kwargs) + + +@mogan_base.MoganObjectRegistry.register_notification +class InstanceActionNotification(base.NotificationBase): + # Version 1.0: Initial version + VERSION = '1.0' + + fields = { + 'payload': fields.ObjectField('InstanceActionPayload') + } diff --git a/mogan/objects/fields.py b/mogan/objects/fields.py index 3296c4d0..167e81c7 100644 --- a/mogan/objects/fields.py +++ b/mogan/objects/fields.py @@ -146,7 +146,7 @@ class NotificationAction(BaseMoganEnum): SHUTDOWN = 'shutdown' CREATE = 'create' - ALL = (UPDATE, EXCEPTION, DELETE, POWER_OFF) + ALL = (UPDATE, EXCEPTION, DELETE, CREATE, POWER_OFF) class NotificationPhaseField(object_fields.BaseEnumField): diff --git a/mogan/tests/unit/notifications/test_notification.py b/mogan/tests/unit/notifications/test_notification.py index 44f2aa61..06b093b1 100644 --- a/mogan/tests/unit/notifications/test_notification.py +++ b/mogan/tests/unit/notifications/test_notification.py @@ -14,10 +14,13 @@ import mock +from mogan.notifications import base as notification_base from mogan.notifications.objects import base as notification from mogan.objects import base from mogan.objects import fields +from mogan.objects import instance as inst_obj from mogan.tests import base as test_base +from mogan.tests.unit.db import utils as db_utils class TestNotificationBase(test_base.TestCase): @@ -218,3 +221,38 @@ class TestNotificationBase(test_base.TestCase): self.assertEqual(2, len(self.TestNotification.samples)) self.assertIn('test-update-1.json', self.TestNotification.samples) self.assertIn('test-update-2.json', self.TestNotification.samples) + + +class TestInstanceActionNotification(test_base.TestCase): + @mock.patch('mogan.notifications.objects.instance.' + 'InstanceActionNotification._emit') + def test_send_version_instance_action(self, mock_emit): + # Make sure that the notification payload chooses the values in + # instance.flavor.$value instead of instance.$value + fake_inst_values = db_utils.get_test_instance() + instance = inst_obj.Instance(**fake_inst_values) + notification_base.notify_about_instance_action( + mock.MagicMock(), + instance, + 'test-host', + fields.NotificationAction.CREATE, + fields.NotificationPhase.START, + 'mogan-compute') + self.assertEqual('instance.create.start', + mock_emit.call_args_list[0][1]['event_type']) + self.assertEqual('mogan-compute:test-host', + mock_emit.call_args_list[0][1]['publisher_id']) + payload = mock_emit.call_args_list[0][1]['payload'][ + 'mogan_object.data'] + self.assertEqual(fake_inst_values['uuid'], payload['uuid']) + self.assertEqual(fake_inst_values['instance_type_uuid'], + payload['instance_type_uuid']) + self.assertEqual(fake_inst_values['status'], payload['status']) + self.assertEqual(fake_inst_values['user_id'], payload['user_id']) + self.assertEqual(fake_inst_values['availability_zone'], + payload['availability_zone']) + self.assertEqual(fake_inst_values['name'], payload['name']) + self.assertEqual(fake_inst_values['image_uuid'], payload['image_uuid']) + self.assertEqual(fake_inst_values['project_id'], payload['project_id']) + self.assertEqual(fake_inst_values['description'], + payload['description'])