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.