From 8e793a6c6fe9cc533cb786cdb995c33160e4c986 Mon Sep 17 00:00:00 2001 From: liyingjun Date: Tue, 14 Nov 2017 16:58:23 +0800 Subject: [PATCH] Versioned notifications for service create and delete New notifications service.create and service.delete are introduced with INFO priority and the payload of the notification is the serialized form of the already existing Service versioned object. Service.create notification will be emitted after the service is created (so the uuid is available) and also send the service.delete notification after the service is deleted. Implement blueprint: service-create-destroy-notification Change-Id: I955d98f9fd4b121f98e172e5ab30eb668a24006d --- doc/notification_samples/service-create.json | 23 ++++++++++++ doc/notification_samples/service-delete.json | 23 ++++++++++++ nova/notifications/objects/base.py | 10 ++++- nova/notifications/objects/service.py | 2 + nova/objects/fields.py | 10 ++++- nova/objects/service.py | 23 +++++++----- .../notification_sample_base.py | 2 + ...test_service_update.py => test_service.py} | 37 ++++++++++++++++--- .../objects/test_notification.py | 2 +- .../notifications/objects/test_service.py | 35 +++++++++++++++--- nova/tests/unit/objects/test_service.py | 15 +++++--- ...destroy-notification-f2f340903eed8f84.yaml | 7 ++++ 12 files changed, 159 insertions(+), 30 deletions(-) create mode 100644 doc/notification_samples/service-create.json create mode 100644 doc/notification_samples/service-delete.json rename nova/tests/functional/notification_sample_tests/{test_service_update.py => test_service.py} (85%) create mode 100644 releasenotes/notes/bp-service-create-destroy-notification-f2f340903eed8f84.yaml diff --git a/doc/notification_samples/service-create.json b/doc/notification_samples/service-create.json new file mode 100644 index 00000000000..3855b1e5a15 --- /dev/null +++ b/doc/notification_samples/service-create.json @@ -0,0 +1,23 @@ +{ + "priority": "INFO", + "payload": { + "nova_object.namespace": "nova", + "nova_object.name": "ServiceStatusPayload", + "nova_object.version": "1.1", + "nova_object.data": { + "host": "host2", + "disabled": false, + "last_seen_up": null, + "binary": "nova-compute", + "topic": "compute", + "disabled_reason": null, + "report_count": 0, + "forced_down": false, + "version": 23, + "availability_zone": null, + "uuid": "fa69c544-906b-4a6a-a9c6-c1f7a8078c73" + } + }, + "event_type": "service.create", + "publisher_id": "nova-compute:host2" +} diff --git a/doc/notification_samples/service-delete.json b/doc/notification_samples/service-delete.json new file mode 100644 index 00000000000..02d2623d5c8 --- /dev/null +++ b/doc/notification_samples/service-delete.json @@ -0,0 +1,23 @@ +{ + "priority": "INFO", + "payload": { + "nova_object.namespace": "nova", + "nova_object.name": "ServiceStatusPayload", + "nova_object.version": "1.1", + "nova_object.data": { + "host": "host2", + "disabled": false, + "last_seen_up": null, + "binary": "nova-compute", + "topic": "compute", + "disabled_reason": null, + "report_count": 0, + "forced_down": false, + "version": 23, + "availability_zone": null, + "uuid": "32887c0a-5063-4d39-826f-4903c241c376" + } + }, + "event_type": "service.delete", + "publisher_id": "nova-compute:host2" +} diff --git a/nova/notifications/objects/base.py b/nova/notifications/objects/base.py index 95bc30cf4f4..7590f76a3e7 100644 --- a/nova/notifications/objects/base.py +++ b/nova/notifications/objects/base.py @@ -147,7 +147,8 @@ class NotificationPublisher(NotificationObject): # 2.1: The type of the source field changed from string to enum. # This only needs a minor bump as the enum uses the possible # values of the previous string field - VERSION = '2.1' + # 2.2: New enum for source fields added + VERSION = '2.2' fields = { 'host': fields.StringField(nullable=False), @@ -161,7 +162,12 @@ def __init__(self, host, source): @classmethod def from_service_obj(cls, service): - return cls(host=service.host, source=service.binary) + # nova-osapi_compute binary name needs to be translated to nova-api + # notification source enum value. + source = ("nova-api" + if service.binary == "nova-osapi_compute" + else service.binary) + return cls(host=service.host, source=source) @base.NovaObjectRegistry.register_if(False) diff --git a/nova/notifications/objects/service.py b/nova/notifications/objects/service.py index f5aa0299e72..654824f19f5 100644 --- a/nova/notifications/objects/service.py +++ b/nova/notifications/objects/service.py @@ -18,7 +18,9 @@ from nova.objects import fields +@base.notification_sample('service-create.json') @base.notification_sample('service-update.json') +@base.notification_sample('service-delete.json') @nova_base.NovaObjectRegistry.register_notification class ServiceStatusNotification(base.NotificationBase): # Version 1.0: Initial version diff --git a/nova/objects/fields.py b/nova/objects/fields.py index 2b13fe2eba6..bbdf7bb0dd7 100644 --- a/nova/objects/fields.py +++ b/nova/objects/fields.py @@ -800,8 +800,14 @@ class NotificationSource(BaseNovaEnum): API = 'nova-api' CONDUCTOR = 'nova-conductor' SCHEDULER = 'nova-scheduler' - - ALL = (API, COMPUTE, CONDUCTOR, SCHEDULER) + NETWORK = 'nova-network' + CONSOLEAUTH = 'nova-consoleauth' + CELLS = 'nova-cells' + CONSOLE = 'nova-console' + METADATA = 'nova-metadata' + + ALL = (API, COMPUTE, CONDUCTOR, SCHEDULER, + NETWORK, CONSOLEAUTH, CELLS, CONSOLE, METADATA) class NotificationAction(BaseNovaEnum): diff --git a/nova/objects/service.py b/nova/objects/service.py index d470ffc733b..6f47bb0a52d 100644 --- a/nova/objects/service.py +++ b/nova/objects/service.py @@ -356,6 +356,7 @@ def create(self): db_service = db.service_create(self._context, updates) self._from_db_object(self._context, self, db_service) + self._send_notification(fields.NotificationAction.CREATE) @base.remotable def save(self): @@ -373,19 +374,23 @@ def _send_status_update_notification(self, updates): # every other field change. See the comment in save() too. if set(updates.keys()).intersection( {'disabled', 'disabled_reason', 'forced_down'}): - payload = service_notification.ServiceStatusPayload(self) - service_notification.ServiceStatusNotification( - publisher=notification.NotificationPublisher.from_service_obj( - self), - event_type=notification.EventType( - object='service', - action=fields.NotificationAction.UPDATE), - priority=fields.NotificationPriority.INFO, - payload=payload).emit(self._context) + self._send_notification(fields.NotificationAction.UPDATE) + + def _send_notification(self, action): + payload = service_notification.ServiceStatusPayload(self) + service_notification.ServiceStatusNotification( + publisher=notification.NotificationPublisher.from_service_obj( + self), + event_type=notification.EventType( + object='service', + action=action), + priority=fields.NotificationPriority.INFO, + payload=payload).emit(self._context) @base.remotable def destroy(self): db.service_destroy(self._context, self.id) + self._send_notification(fields.NotificationAction.DELETE) @classmethod def enable_min_version_cache(cls): diff --git a/nova/tests/functional/notification_sample_tests/notification_sample_base.py b/nova/tests/functional/notification_sample_tests/notification_sample_base.py index dbb3a52f2e9..c0b4b1abc99 100644 --- a/nova/tests/functional/notification_sample_tests/notification_sample_base.py +++ b/nova/tests/functional/notification_sample_tests/notification_sample_base.py @@ -87,6 +87,8 @@ def setUp(self): self.start_service('scheduler') self.start_service('network', manager=CONF.network_manager) self.compute = self.start_service('compute') + # Reset the service create notifications + fake_notifier.reset() def _get_notification_sample(self, sample): sample_dir = os.path.dirname(os.path.abspath(__file__)) diff --git a/nova/tests/functional/notification_sample_tests/test_service_update.py b/nova/tests/functional/notification_sample_tests/test_service.py similarity index 85% rename from nova/tests/functional/notification_sample_tests/test_service_update.py rename to nova/tests/functional/notification_sample_tests/test_service.py index 05143fc5252..0863e7df755 100644 --- a/nova/tests/functional/notification_sample_tests/test_service_update.py +++ b/nova/tests/functional/notification_sample_tests/test_service.py @@ -20,15 +20,12 @@ from nova.tests.functional.notification_sample_tests \ import notification_sample_base from nova.tests.unit.api.openstack.compute import test_services +from nova.tests.unit import fake_notifier -class TestServiceUpdateNotificationSamplev2_52( +class TestServiceNotificationBase( notification_sample_base.NotificationSampleTestBase): - # These tests have to be capped at 2.52 since the PUT format changes in - # the 2.53 microversion. - MAX_MICROVERSION = '2.52' - def _verify_notification(self, sample_file_name, replacements=None, actual=None): # This just extends the generic _verify_notification to default the @@ -36,9 +33,16 @@ def _verify_notification(self, sample_file_name, replacements=None, # after every service version bump. if 'version' not in replacements: replacements['version'] = service.SERVICE_VERSION - base = super(TestServiceUpdateNotificationSamplev2_52, self) + base = super(TestServiceNotificationBase, self) base._verify_notification(sample_file_name, replacements, actual) + +class TestServiceUpdateNotificationSamplev2_52(TestServiceNotificationBase): + + # These tests have to be capped at 2.52 since the PUT format changes in + # the 2.53 microversion. + MAX_MICROVERSION = '2.52' + def setUp(self): super(TestServiceUpdateNotificationSamplev2_52, self).setUp() self.stub_out("nova.db.service_get_by_host_and_binary", @@ -133,3 +137,24 @@ def test_service_force_down(self): 'disabled': True, 'disabled_reason': 'test2', 'uuid': self.service_uuid}) + + +class TestServiceNotificationSample(TestServiceNotificationBase): + + def test_service_create(self): + self.compute2 = self.start_service('compute', host='host2') + self._verify_notification( + 'service-create', + replacements={ + 'uuid': + notification_sample_base.NotificationSampleTestBase.ANY}) + + def test_service_destroy(self): + self.compute2 = self.start_service('compute', host='host2') + compute2_service_id = self.admin_api.get_services( + host=self.compute2.host, binary='nova-compute')[0]['id'] + self.admin_api.api_delete('os-services/%s' % compute2_service_id) + self._verify_notification( + 'service-delete', + replacements={'uuid': compute2_service_id}, + actual=fake_notifier.VERSIONED_NOTIFICATIONS[1]) diff --git a/nova/tests/unit/notifications/objects/test_notification.py b/nova/tests/unit/notifications/objects/test_notification.py index 6247a9bed7b..ddd9b779c41 100644 --- a/nova/tests/unit/notifications/objects/test_notification.py +++ b/nova/tests/unit/notifications/objects/test_notification.py @@ -397,7 +397,7 @@ def test_payload_is_not_generated_if_notification_format_is_unversioned( 'IpPayload': '1.0-8ecf567a99e516d4af094439a7632d34', 'KeypairNotification': '1.0-a73147b93b520ff0061865849d3dfa56', 'KeypairPayload': '1.0-6daebbbde0e1bf35c1556b1ecd9385c1', - 'NotificationPublisher': '2.1-9f89fe4abb80f9a7b726e59800c905de', + 'NotificationPublisher': '2.2-b6ad48126247e10b46b6b0240e52e614', 'ServerGroupNotification': '1.0-a73147b93b520ff0061865849d3dfa56', 'ServerGroupPayload': '1.0-eb4bd1738b4670cfe1b7c30344c143c3', 'ServiceStatusNotification': '1.0-a73147b93b520ff0061865849d3dfa56', diff --git a/nova/tests/unit/notifications/objects/test_service.py b/nova/tests/unit/notifications/objects/test_service.py index 75a6a8626d3..c228a8674a9 100644 --- a/nova/tests/unit/notifications/objects/test_service.py +++ b/nova/tests/unit/notifications/objects/test_service.py @@ -13,6 +13,8 @@ # License for the specific language governing permissions and limitations # under the License. +import copy + import mock from oslo_utils import timeutils @@ -30,8 +32,15 @@ def setUp(self): super(TestServiceStatusNotification, self).setUp() @mock.patch('nova.notifications.objects.service.ServiceStatusNotification') - def _verify_notification(self, service_obj, mock_notification): - service_obj.save() + def _verify_notification(self, service_obj, action, mock_notification): + if action == fields.NotificationAction.CREATE: + service_obj.create() + elif action == fields.NotificationAction.UPDATE: + service_obj.save() + elif action == fields.NotificationAction.DELETE: + service_obj.destroy() + else: + raise Exception('Unsupported action: %s' % action) self.assertTrue(mock_notification.called) @@ -44,8 +53,7 @@ def _verify_notification(self, service_obj, mock_notification): self.assertEqual(service_obj.binary, publisher.source) self.assertEqual(fields.NotificationPriority.INFO, priority) self.assertEqual('service', event_type.object) - self.assertEqual(fields.NotificationAction.UPDATE, - event_type.action) + self.assertEqual(action, event_type.action) for field in service_notification.ServiceStatusPayload.SCHEMA: if field in fake_service: self.assertEqual(fake_service[field], getattr(payload, field)) @@ -60,7 +68,8 @@ def test_service_update_with_notification(self, mock_db_service_update): 'disabled_reason': 'my reason', 'forced_down': True}.items(): setattr(service_obj, key, value) - self._verify_notification(service_obj) + self._verify_notification(service_obj, + fields.NotificationAction.UPDATE) @mock.patch('nova.notifications.objects.service.ServiceStatusNotification') @mock.patch('nova.db.service_update') @@ -75,3 +84,19 @@ def test_service_update_without_notification(self, setattr(service_obj, key, value) service_obj.save() self.assertFalse(mock_notification.called) + + @mock.patch('nova.db.service_create') + def test_service_create_with_notification(self, mock_db_service_create): + service_obj = objects.Service(context=self.ctxt) + service_obj["uuid"] = fake_service["uuid"] + mock_db_service_create.return_value = fake_service + self._verify_notification(service_obj, + fields.NotificationAction.CREATE) + + @mock.patch('nova.db.service_destroy') + def test_service_destroy_with_notification(self, mock_db_service_destroy): + service = copy.deepcopy(fake_service) + service.pop("version") + service_obj = objects.Service(context=self.ctxt, **service) + self._verify_notification(service_obj, + fields.NotificationAction.DELETE) diff --git a/nova/tests/unit/objects/test_service.py b/nova/tests/unit/objects/test_service.py index 55198ae19d5..9d1e749b0fc 100644 --- a/nova/tests/unit/objects/test_service.py +++ b/nova/tests/unit/objects/test_service.py @@ -158,8 +158,9 @@ def test_recreate_fails(self, mock_service_create): mock_service_create(self.context, {'host': 'fake-host', 'version': fake_service['version']}) + @mock.patch('nova.objects.Service._send_notification') @mock.patch.object(db, 'service_update', return_value=fake_service) - def test_save(self, mock_service_update): + def test_save(self, mock_service_update, mock_notify): service_obj = service.Service(context=self.context) service_obj.id = 123 service_obj.host = 'fake-host' @@ -178,8 +179,9 @@ def test_set_id_failure(self, db_mock): self.assertRaises(ovo_exc.ReadOnlyFieldError, setattr, service_obj, 'id', 124) + @mock.patch('nova.objects.Service._send_notification') @mock.patch.object(db, 'service_destroy') - def _test_destroy(self, mock_service_destroy): + def _test_destroy(self, mock_service_destroy, mock_notify): service_obj = service.Service(context=self.context) service_obj.id = 123 service_obj.destroy() @@ -385,17 +387,19 @@ def test_get_min_version_multiple(self, mock_gmv): binaries) self.assertEqual(1, minimum) + @mock.patch('nova.objects.Service._send_notification') @mock.patch('nova.db.service_get_minimum_version', return_value={'nova-compute': 2}) - def test_create_above_minimum(self, mock_get): + def test_create_above_minimum(self, mock_get, mock_notify): with mock.patch('nova.objects.service.SERVICE_VERSION', new=3): objects.Service(context=self.context, binary='nova-compute').create() + @mock.patch('nova.objects.Service._send_notification') @mock.patch('nova.db.service_get_minimum_version', return_value={'nova-compute': 2}) - def test_create_equal_to_minimum(self, mock_get): + def test_create_equal_to_minimum(self, mock_get, mock_notify): with mock.patch('nova.objects.service.SERVICE_VERSION', new=2): objects.Service(context=self.context, @@ -525,8 +529,9 @@ def _create_services(self, *versions): service.create() index += 1 + @mock.patch('nova.objects.Service._send_notification') @mock.patch('nova.objects.Service._check_minimum_version') - def test_version_all_cells(self, mock_check): + def test_version_all_cells(self, mock_check, mock_notify): self._create_services(16, 16, 13, 16) self.assertEqual(13, service.get_minimum_version_all_cells( self.context, ['nova-compute'])) diff --git a/releasenotes/notes/bp-service-create-destroy-notification-f2f340903eed8f84.yaml b/releasenotes/notes/bp-service-create-destroy-notification-f2f340903eed8f84.yaml new file mode 100644 index 00000000000..86df0bccf89 --- /dev/null +++ b/releasenotes/notes/bp-service-create-destroy-notification-f2f340903eed8f84.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + Added support for service create and destroy versioned notifications. + The ``service.create`` notification will be emitted after the service is + created (so the uuid is available) and also send the ``service.delete`` + notification after the service is deleted.