From e8ed9aacf846bfe83938e4e95a2c01ff429dbbed Mon Sep 17 00:00:00 2001 From: Brianna Poulos Date: Fri, 20 Apr 2018 16:12:50 -0400 Subject: [PATCH] Add notification support for trusted_certs Add the 'trusted_image_certificates' field to InstanceCreatePayload and InstanceActionRebuildPayload notifications. Change-Id: Ib5b50a3889ab15d5aac992f92e9be372a915eeff --- .../InstanceActionRebuildPayload.json | 11 +++ .../InstanceCreatePayload.json | 8 +- .../instance-rebuild-end.json | 2 +- .../instance-rebuild-error.json | 2 +- .../instance-rebuild-start.json | 2 +- nova/compute/manager.py | 9 +- nova/compute/utils.py | 31 +++++++ nova/notifications/objects/instance.py | 51 +++++++++++- .../test_instance.py | 83 ++++++++++++++++++- nova/tests/unit/compute/test_compute.py | 28 ++++--- nova/tests/unit/compute/test_compute_mgr.py | 14 ++-- .../objects/test_notification.py | 5 +- ...d-certs-microversion-589b75f0180d4d51.yaml | 16 ++++ 13 files changed, 222 insertions(+), 40 deletions(-) create mode 100644 doc/notification_samples/common_payloads/InstanceActionRebuildPayload.json diff --git a/doc/notification_samples/common_payloads/InstanceActionRebuildPayload.json b/doc/notification_samples/common_payloads/InstanceActionRebuildPayload.json new file mode 100644 index 0000000000..3b13ca71bb --- /dev/null +++ b/doc/notification_samples/common_payloads/InstanceActionRebuildPayload.json @@ -0,0 +1,11 @@ +{ + "$ref": "InstanceActionPayload.json", + "nova_object.data": { + "trusted_image_certificates": [ + "rebuild-cert-id-1", + "rebuild-cert-id-2" + ] + }, + "nova_object.name": "InstanceActionRebuildPayload", + "nova_object.version": "1.7" +} diff --git a/doc/notification_samples/common_payloads/InstanceCreatePayload.json b/doc/notification_samples/common_payloads/InstanceCreatePayload.json index f549f6b2c4..8162c67324 100644 --- a/doc/notification_samples/common_payloads/InstanceCreatePayload.json +++ b/doc/notification_samples/common_payloads/InstanceCreatePayload.json @@ -16,8 +16,12 @@ } } ], - "tags": ["tag"] + "tags": ["tag"], + "trusted_image_certificates": [ + "cert-id-1", + "cert-id-2" + ] }, "nova_object.name":"InstanceCreatePayload", - "nova_object.version": "1.8" + "nova_object.version": "1.9" } diff --git a/doc/notification_samples/instance-rebuild-end.json b/doc/notification_samples/instance-rebuild-end.json index badee32039..fa350e59d1 100644 --- a/doc/notification_samples/instance-rebuild-end.json +++ b/doc/notification_samples/instance-rebuild-end.json @@ -2,7 +2,7 @@ "event_type": "instance.rebuild.end", "publisher_id": "nova-compute:compute", "payload": { - "$ref":"common_payloads/InstanceActionPayload.json#", + "$ref":"common_payloads/InstanceActionRebuildPayload.json#", "nova_object.data": { "architecture": null, "image_uuid": "a2459075-d96c-40d5-893e-577ff92e721c" diff --git a/doc/notification_samples/instance-rebuild-error.json b/doc/notification_samples/instance-rebuild-error.json index 99310114d2..e51019fd45 100644 --- a/doc/notification_samples/instance-rebuild-error.json +++ b/doc/notification_samples/instance-rebuild-error.json @@ -1,7 +1,7 @@ { "priority": "ERROR", "payload": { - "$ref": "common_payloads/InstanceActionPayload.json#", + "$ref": "common_payloads/InstanceActionRebuildPayload.json#", "nova_object.data": { "fault": { "nova_object.name": "ExceptionPayload", diff --git a/doc/notification_samples/instance-rebuild-start.json b/doc/notification_samples/instance-rebuild-start.json index bb98aa9487..3dc65ff593 100644 --- a/doc/notification_samples/instance-rebuild-start.json +++ b/doc/notification_samples/instance-rebuild-start.json @@ -3,7 +3,7 @@ "event_type": "instance.rebuild.start", "publisher_id": "nova-compute:compute", "payload": { - "$ref":"common_payloads/InstanceActionPayload.json#", + "$ref":"common_payloads/InstanceActionRebuildPayload.json#", "nova_object.data": { "architecture": null, "image_uuid": "a2459075-d96c-40d5-893e-577ff92e721c", diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 6721f23bec..c9ba8f8786 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -2777,9 +2777,8 @@ class ComputeManager(manager.Manager): def _notify_instance_rebuild_error(self, context, instance, error, bdms): self._notify_about_instance_usage(context, instance, 'rebuild.error', fault=error) - compute_utils.notify_about_instance_action( + compute_utils.notify_about_instance_rebuild( context, instance, self.host, - action=fields.NotificationAction.REBUILD, phase=fields.NotificationPhase.ERROR, exception=error, bdms=bdms) @messaging.expected_exceptions(exception.PreserveEphemeralNotSupported) @@ -3024,9 +3023,8 @@ class ComputeManager(manager.Manager): # NOTE: image_name is not included in the versioned notification # because we already provide the image_uuid in the notification # payload and the image details can be looked up via the uuid. - compute_utils.notify_about_instance_action( + compute_utils.notify_about_instance_rebuild( context, instance, self.host, - action=fields.NotificationAction.REBUILD, phase=fields.NotificationPhase.START, bdms=bdms) @@ -3121,9 +3119,8 @@ class ComputeManager(manager.Manager): context, instance, "rebuild.end", network_info=network_info, extra_usage_info=extra_usage_info) - compute_utils.notify_about_instance_action( + compute_utils.notify_about_instance_rebuild( context, instance, self.host, - action=fields.NotificationAction.REBUILD, phase=fields.NotificationPhase.END, bdms=bdms) diff --git a/nova/compute/utils.py b/nova/compute/utils.py index f12932fbe9..b7d83e3bd6 100644 --- a/nova/compute/utils.py +++ b/nova/compute/utils.py @@ -715,6 +715,37 @@ def notify_about_server_group_add_member(context, group_id): notification.emit(context) +@rpc.if_notifications_enabled +def notify_about_instance_rebuild(context, instance, host, phase=None, + exception=None, bdms=None): + """Send versioned notification about instance rebuild + + :param instance: the instance which the action performed on + :param host: the host emitting the notification + :param phase: the phase of the action + :param exception: the thrown exception (used in error notifications) + :param bdms: BlockDeviceMappingList object for the instance. If it is not + provided then we will load it from the db if so configured + """ + fault, priority = _get_fault_and_priority_from_exc(exception) + payload = instance_notification.InstanceActionRebuildPayload( + context=context, + instance=instance, + fault=fault, + bdms=bdms) + notification = instance_notification.InstanceActionRebuildNotification( + context=context, + priority=priority, + publisher=notification_base.NotificationPublisher( + host=host, source=fields.NotificationSource.COMPUTE), + event_type=notification_base.EventType( + object='instance', + action=fields.NotificationAction.REBUILD, + phase=phase), + payload=payload) + notification.emit(context) + + def refresh_info_cache_for_instance(context, instance): """Refresh the info cache for an instance. diff --git a/nova/notifications/objects/instance.py b/nova/notifications/objects/instance.py index 8b38b710bb..30949b3ba2 100644 --- a/nova/notifications/objects/instance.py +++ b/nova/notifications/objects/instance.py @@ -214,11 +214,15 @@ class InstanceCreatePayload(InstanceActionPayload): # 1.6: Add tags field to InstanceCreatePayload # 1.7: Added updated_at field to InstancePayload # 1.8: Added request_id field to InstancePayload - VERSION = '1.8' + # 1.9: Add trusted_image_certificates field to + # InstanceCreatePayload + VERSION = '1.9' fields = { 'keypairs': fields.ListOfObjectsField('KeypairPayload'), 'tags': fields.ListOfStringsField(), + 'trusted_image_certificates': fields.ListOfStringsField( + nullable=True) } def __init__(self, context, instance, fault, bdms): @@ -231,6 +235,9 @@ class InstanceCreatePayload(InstanceActionPayload): for keypair in instance.keypairs] self.tags = [instance_tag.tag for instance_tag in instance.tags] + self.trusted_image_certificates = None + if instance.get("trusted_certs", None): + self.trusted_image_certificates = instance.trusted_certs.ids @nova_base.NovaObjectRegistry.register_notification @@ -300,6 +307,32 @@ class InstanceActionRescuePayload(InstanceActionPayload): self.rescue_image_ref = rescue_image_ref +@nova_base.NovaObjectRegistry.register_notification +class InstanceActionRebuildPayload(InstanceActionPayload): + # No SCHEMA as all the additional fields are calculated + + # Version 1.7: Initial version. It starts at 1.7 to equal one more than + # the version of the InstanceActionPayload at the time + # when this specific payload is created so that the + # instance.rebuild.* notifications using this new payload + # signal the change of nova_object.name. + VERSION = '1.7' + fields = { + 'trusted_image_certificates': fields.ListOfStringsField( + nullable=True) + } + + def __init__(self, context, instance, fault, bdms): + super(InstanceActionRebuildPayload, self).__init__( + context=context, + instance=instance, + fault=fault, + bdms=bdms) + self.trusted_image_certificates = None + if instance.get("trusted_certs", None): + self.trusted_image_certificates = instance.trusted_certs.ids + + @nova_base.NovaObjectRegistry.register_notification class IpPayload(base.NotificationPayloadBase): # Version 1.0: Initial version @@ -491,9 +524,6 @@ class InstanceStateUpdatePayload(base.NotificationPayloadBase): @base.notification_sample('instance-live_migration_rollback-end.json') # @base.notification_sample('instance-live_migration_rollback_dest-start.json') # @base.notification_sample('instance-live_migration_rollback_dest-end.json') -@base.notification_sample('instance-rebuild-start.json') -@base.notification_sample('instance-rebuild-end.json') -@base.notification_sample('instance-rebuild-error.json') @base.notification_sample('instance-interface_detach-start.json') @base.notification_sample('instance-interface_detach-end.json') @base.notification_sample('instance-resize_confirm-start.json') @@ -608,6 +638,19 @@ class InstanceActionRescueNotification(base.NotificationBase): } +@base.notification_sample('instance-rebuild-start.json') +@base.notification_sample('instance-rebuild-end.json') +@base.notification_sample('instance-rebuild-error.json') +@nova_base.NovaObjectRegistry.register_notification +class InstanceActionRebuildNotification(base.NotificationBase): + # Version 1.0: Initial version + VERSION = '1.0' + + fields = { + 'payload': fields.ObjectField('InstanceActionRebuildPayload') + } + + @nova_base.NovaObjectRegistry.register_notification class InstanceActionSnapshotPayload(InstanceActionPayload): # Version 1.6: Initial version. It starts at version 1.6 as diff --git a/nova/tests/functional/notification_sample_tests/test_instance.py b/nova/tests/functional/notification_sample_tests/test_instance.py index 2a174fe1b9..48d0b295ee 100644 --- a/nova/tests/functional/notification_sample_tests/test_instance.py +++ b/nova/tests/functional/notification_sample_tests/test_instance.py @@ -314,9 +314,11 @@ class TestInstanceNotificationSample( self._wait_for_state_change(self.admin_api, server, 'ACTIVE') def test_create_delete_server(self): + fake_trusted_certs = ['cert-id-1', 'cert-id-2'] server = self._boot_a_server( extra_params={'networks': [{'port': self.neutron.port_1['id']}], - 'tags': ['tag']}) + 'tags': ['tag'], + 'trusted_image_certificates': fake_trusted_certs}) self._attach_volume_to_server(server, self.cinder.SWAP_OLD_VOL) self.api.delete_server(server['id']) self._wait_until_deleted(server) @@ -356,10 +358,12 @@ class TestInstanceNotificationSample( mock_build.side_effect = _build_resources + fake_trusted_certs = ['cert-id-1', 'cert-id-2'] server = self._boot_a_server( expected_status='ERROR', extra_params={'networks': [{'port': self.neutron.port_1['id']}], - 'tags': ['tag']}) + 'tags': ['tag'], + 'trusted_image_certificates': fake_trusted_certs}) self.assertEqual(2, len(fake_notifier.VERSIONED_NOTIFICATIONS)) @@ -1009,7 +1013,72 @@ class TestInstanceNotificationSample( post = { 'rebuild': { 'imageRef': 'a2459075-d96c-40d5-893e-577ff92e721c', - 'metadata': {} + 'metadata': {}, + } + } + self.api.post_server_action(server['id'], post) + # Before going back to ACTIVE state + # server state need to be changed to REBUILD state + self._wait_for_state_change(self.api, server, + expected_status='REBUILD') + self._wait_for_state_change(self.api, server, + expected_status='ACTIVE') + + # The compute/manager will detach every volume during rebuild + self.assertEqual(5, len(fake_notifier.VERSIONED_NOTIFICATIONS)) + self._verify_notification( + 'instance-rebuild-start', + replacements={ + 'reservation_id': server['reservation_id'], + 'uuid': server['id'], + 'trusted_image_certificates': None}, + actual=fake_notifier.VERSIONED_NOTIFICATIONS[1]) + self._verify_notification( + 'instance-volume_detach-start', + replacements={ + 'reservation_id': server['reservation_id'], + 'task_state': 'rebuilding', + 'architecture': None, + 'image_uuid': 'a2459075-d96c-40d5-893e-577ff92e721c', + 'uuid': server['id']}, + actual=fake_notifier.VERSIONED_NOTIFICATIONS[2]) + self._verify_notification( + 'instance-volume_detach-end', + replacements={ + 'reservation_id': server['reservation_id'], + 'task_state': 'rebuilding', + 'architecture': None, + 'image_uuid': 'a2459075-d96c-40d5-893e-577ff92e721c', + 'uuid': server['id']}, + actual=fake_notifier.VERSIONED_NOTIFICATIONS[3]) + self._verify_notification( + 'instance-rebuild-end', + replacements={ + 'reservation_id': server['reservation_id'], + 'uuid': server['id'], + 'trusted_image_certificates': None}, + actual=fake_notifier.VERSIONED_NOTIFICATIONS[4]) + + def test_rebuild_server_with_trusted_cert(self): + # NOTE(gabor_antal): Rebuild changes the image used by the instance, + # therefore the actions tested in test_instance_action had to be in + # specific order. To avoid this problem, rebuild was moved from + # test_instance_action to its own method. + + create_trusted_certs = ['cert-id-1', 'cert-id-2'] + server = self._boot_a_server( + extra_params={'networks': [{'port': self.neutron.port_1['id']}], + 'trusted_image_certificates': create_trusted_certs}) + self._attach_volume_to_server(server, self.cinder.SWAP_OLD_VOL) + + fake_notifier.reset() + + rebuild_trusted_certs = ['rebuild-cert-id-1', 'rebuild-cert-id-2'] + post = { + 'rebuild': { + 'imageRef': 'a2459075-d96c-40d5-893e-577ff92e721c', + 'metadata': {}, + 'trusted_image_certificates': rebuild_trusted_certs, } } self.api.post_server_action(server['id'], post) @@ -1081,7 +1150,8 @@ class TestInstanceNotificationSample( 'instance-rebuild-error', replacements={ 'reservation_id': server['reservation_id'], - 'uuid': server['id']}, + 'uuid': server['id'], + 'trusted_image_certificates': None}, actual=notification[0]) def _test_restore_server(self, server): @@ -1573,3 +1643,8 @@ class TestInstanceNotificationSampleOldAttachFlow( @mock.patch('nova.volume.cinder.API.attach') def _test_attach_volume_error(self, server, mock_attach): self._do_test_attach_volume_error(server, mock_attach) + + def test_rebuild_server_with_trusted_cert(self): + # Skipping this test as trusted cert support needs a later service + # version than this test class is limited to. + pass diff --git a/nova/tests/unit/compute/test_compute.py b/nova/tests/unit/compute/test_compute.py index 649dcc2406..61b8999a81 100644 --- a/nova/tests/unit/compute/test_compute.py +++ b/nova/tests/unit/compute/test_compute.py @@ -12634,12 +12634,13 @@ class EvacuateHostTestCase(BaseTestCase): limits = {} @mock.patch('nova.compute.utils.notify_about_instance_action') + @mock.patch('nova.compute.utils.notify_about_instance_rebuild') @mock.patch.object(network_api, 'setup_networks_on_host') @mock.patch.object(network_api, 'setup_instance_network_on_host') @mock.patch('nova.context.RequestContext.elevated', return_value=ctxt) def _test_rebuild(mock_context, mock_setup_instance_network_on_host, - mock_setup_networks_on_host, mock_notify, - vm_is_stopped=False): + mock_setup_networks_on_host, mock_notify_rebuild, + mock_notify_action, vm_is_stopped=False): orig_image_ref = None image_ref = None injected_files = None @@ -12652,21 +12653,22 @@ class EvacuateHostTestCase(BaseTestCase): preserve_ephemeral=False, scheduled_node=node, limits=limits, request_spec=None) if vm_states_is_stopped: - mock_notify.assert_has_calls([ - mock.call(ctxt, self.inst, self.inst.host, - action='rebuild', phase='start', bdms=bdms), + mock_notify_rebuild.assert_has_calls([ + mock.call(ctxt, self.inst, self.inst.host, phase='start', + bdms=bdms), + mock.call(ctxt, self.inst, self.inst.host, phase='end', + bdms=bdms)]) + mock_notify_action.assert_has_calls([ mock.call(ctxt, self.inst, self.inst.host, action='power_off', phase='start'), mock.call(ctxt, self.inst, self.inst.host, - action='power_off', phase='end'), - mock.call(ctxt, self.inst, self.inst.host, - action='rebuild', phase='end', bdms=bdms)]) + action='power_off', phase='end')]) else: - mock_notify.assert_has_calls([ - mock.call(ctxt, self.inst, self.inst.host, - action='rebuild', phase='start', bdms=bdms), - mock.call(ctxt, self.inst, self.inst.host, - action='rebuild', phase='end', bdms=bdms)]) + mock_notify_rebuild.assert_has_calls([ + mock.call(ctxt, self.inst, self.inst.host, phase='start', + bdms=bdms), + mock.call(ctxt, self.inst, self.inst.host, phase='end', + bdms=bdms)]) mock_setup_networks_on_host.assert_called_once_with( ctxt, self.inst, self.inst.host) diff --git a/nova/tests/unit/compute/test_compute_mgr.py b/nova/tests/unit/compute/test_compute_mgr.py index 4c8d5721f2..12ebbdd8e2 100644 --- a/nova/tests/unit/compute/test_compute_mgr.py +++ b/nova/tests/unit/compute/test_compute_mgr.py @@ -3968,7 +3968,7 @@ class ComputeManagerUnitTestCase(test.NoDBTestCase): @mock.patch.object(manager.ComputeManager, '_set_migration_status') @mock.patch.object(manager.ComputeManager, '_do_rebuild_instance_with_claim') - @mock.patch('nova.compute.utils.notify_about_instance_action') + @mock.patch('nova.compute.utils.notify_about_instance_rebuild') @mock.patch.object(manager.ComputeManager, '_notify_about_instance_usage') def _test_rebuild_ex(self, instance, exc, mock_notify_about_instance_usage, @@ -3986,8 +3986,8 @@ class ComputeManagerUnitTestCase(test.NoDBTestCase): mock.ANY, instance, 'rebuild.error', fault=mock_rebuild.side_effect ) mock_notify.assert_called_once_with( - mock.ANY, instance, 'fake-mini', action='rebuild', phase='error', - exception=exc, bdms=None) + mock.ANY, instance, 'fake-mini', phase='error', exception=exc, + bdms=None) def test_rebuild_deleting(self): instance = fake_instance.fake_instance_obj(self.context) @@ -4040,7 +4040,7 @@ class ComputeManagerUnitTestCase(test.NoDBTestCase): 'setup_instance_network_on_host') @mock.patch('nova.network.neutronv2.api.API.setup_networks_on_host') @mock.patch('nova.objects.instance.Instance.save') - @mock.patch('nova.compute.utils.notify_about_instance_action') + @mock.patch('nova.compute.utils.notify_about_instance_rebuild') @mock.patch('nova.compute.utils.notify_about_instance_usage') @mock.patch('nova.compute.utils.notify_usage_exists') @mock.patch('nova.objects.instance.Instance.image_meta', @@ -4080,7 +4080,7 @@ class ComputeManagerUnitTestCase(test.NoDBTestCase): 'delete_allocation_for_evacuated_instance') @mock.patch('nova.context.RequestContext.elevated') @mock.patch('nova.objects.instance.Instance.save') - @mock.patch('nova.compute.utils.notify_about_instance_action') + @mock.patch('nova.compute.utils.notify_about_instance_rebuild') @mock.patch('nova.compute.utils.notify_about_instance_usage') @mock.patch('nova.compute.manager.ComputeManager.' '_validate_instance_group_policy') @@ -4114,8 +4114,8 @@ class ComputeManagerUnitTestCase(test.NoDBTestCase): mock_delete_allocation.assert_called_once_with( elevated_context, instance, 'fake-node', node_type='destination') mock_notify.assert_called_once_with( - elevated_context, instance, 'fake-mini', action='rebuild', - bdms=None, exception=exc, phase='error') + elevated_context, instance, 'fake-mini', bdms=None, exception=exc, + phase='error') def test_rebuild_node_not_updated_if_not_recreate(self): node = uuidutils.generate_uuid() # ironic node uuid diff --git a/nova/tests/unit/notifications/objects/test_notification.py b/nova/tests/unit/notifications/objects/test_notification.py index 5e358ecb37..fc664cee62 100644 --- a/nova/tests/unit/notifications/objects/test_notification.py +++ b/nova/tests/unit/notifications/objects/test_notification.py @@ -376,6 +376,9 @@ notification_object_data = { 'FlavorPayload': '1.4-2e7011b8b4e59167fe8b7a0a81f0d452', 'InstanceActionNotification': '1.0-a73147b93b520ff0061865849d3dfa56', 'InstanceActionPayload': '1.6-e9e4cbb94e07d3bcaa22743f41e094c8', + 'InstanceActionRebuildNotification': + '1.0-a73147b93b520ff0061865849d3dfa56', + 'InstanceActionRebuildPayload': '1.7-db0d6d549a7932428cdc0c7ca78859b5', 'InstanceActionRescueNotification': '1.0-a73147b93b520ff0061865849d3dfa56', 'InstanceActionRescuePayload': '1.1-99b9b25574b77abf6d3e5a0cea341b06', 'InstanceActionResizePrepNotification': @@ -387,7 +390,7 @@ notification_object_data = { '1.0-a73147b93b520ff0061865849d3dfa56', 'InstanceActionVolumeSwapPayload': '1.6-bb322fd649d3626c7a83d5f2d9a866d4', 'InstanceCreateNotification': '1.0-a73147b93b520ff0061865849d3dfa56', - 'InstanceCreatePayload': '1.8-aab72bba998af21dc2e34b31e3c376ea', + 'InstanceCreatePayload': '1.9-1c9f722bf51ddae910fb7e50fff8edd1', 'InstancePayload': '1.6-b1e7818c7adf158e8a6e87e0944b0b21', 'InstanceActionSnapshotNotification': '1.0-a73147b93b520ff0061865849d3dfa56', diff --git a/releasenotes/notes/trusted-certs-microversion-589b75f0180d4d51.yaml b/releasenotes/notes/trusted-certs-microversion-589b75f0180d4d51.yaml index 7084354974..f37c539826 100644 --- a/releasenotes/notes/trusted-certs-microversion-589b75f0180d4d51.yaml +++ b/releasenotes/notes/trusted-certs-microversion-589b75f0180d4d51.yaml @@ -28,3 +28,19 @@ features: * ``GET /servers/{server_id}`` * ``PUT /servers/{server_id}`` * ``POST /servers/{server_id}/action (rebuild)`` + + The payload of the ``instance.create.start`` and ``instance.create.end`` + and ``instance.create.error`` notifications have been extended with the + ``trusted_image_certificates`` field that contains the list of trusted + certificate IDs used when the instance is created. + + The payload of the ``instance.rebuild.start`` and ``instance.rebuild.end`` + and ``instance.rebuild.error`` notifications have been extended with the + ``trusted_image_certificates`` field that contains the list of trusted + certificate IDs used when the instance is rebuilt. This change also causes + the type of the payload object to change from ``InstanceActionPayload`` + version 1.6 to ``InstanceActionRebuildPayload`` version 1.7. See the + `notification dev reference`_ for the sample file of + ``instance.rebuild.start`` as an example. + + .. _notification dev reference: https://docs.openstack.org/developer/nova/notifications.html \ No newline at end of file