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 | 8 +++- 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, 158 insertions(+), 29 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 000000000000..3855b1e5a15c --- /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 000000000000..02d2623d5c89 --- /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 95bc30cf4f47..7590f76a3e76 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 @@ class NotificationPublisher(NotificationObject): @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 f5aa0299e72c..654824f19f5b 100644 --- a/nova/notifications/objects/service.py +++ b/nova/notifications/objects/service.py @@ -18,7 +18,9 @@ from nova.objects import base as nova_base 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 2b13fe2eba6a..bbdf7bb0dd77 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' + NETWORK = 'nova-network' + CONSOLEAUTH = 'nova-consoleauth' + CELLS = 'nova-cells' + CONSOLE = 'nova-console' + METADATA = 'nova-metadata' - ALL = (API, COMPUTE, CONDUCTOR, SCHEDULER) + 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 d470ffc733b2..6f47bb0a52db 100644 --- a/nova/objects/service.py +++ b/nova/objects/service.py @@ -356,6 +356,7 @@ class Service(base.NovaPersistentObject, base.NovaObject, 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 @@ class Service(base.NovaPersistentObject, base.NovaObject, # 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 dbb3a52f2e9e..c0b4b1abc99b 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 @@ class NotificationSampleTestBase(test.TestCase, 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 05143fc52523..0863e7df7551 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 import fixtures 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 @@ class TestServiceUpdateNotificationSamplev2_52( # 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 @@ class TestServiceUpdateNotificationSampleLatest( '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 6247a9bed7b2..ddd9b779c418 100644 --- a/nova/tests/unit/notifications/objects/test_notification.py +++ b/nova/tests/unit/notifications/objects/test_notification.py @@ -397,7 +397,7 @@ notification_object_data = { '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 75a6a8626d3b..c228a8674a99 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 @@ class TestServiceStatusNotification(test.TestCase): 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 @@ class TestServiceStatusNotification(test.TestCase): 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 @@ class TestServiceStatusNotification(test.TestCase): '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 @@ class TestServiceStatusNotification(test.TestCase): 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 55198ae19d53..9d1e749b0fc3 100644 --- a/nova/tests/unit/objects/test_service.py +++ b/nova/tests/unit/objects/test_service.py @@ -158,8 +158,9 @@ class _TestServiceObject(object): 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 @@ class _TestServiceObject(object): 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 @@ class _TestServiceObject(object): 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 @@ class TestServiceVersionCells(test.TestCase): 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 000000000000..86df0bccf891 --- /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.