diff --git a/nova/objects/service.py b/nova/objects/service.py index e24e54f4a584..cf95414d2d87 100644 --- a/nova/objects/service.py +++ b/nova/objects/service.py @@ -22,6 +22,7 @@ from nova.i18n import _LW from nova import objects from nova.objects import base from nova.objects import fields +from nova.objects import notification LOG = logging.getLogger(__name__) @@ -282,6 +283,24 @@ class Service(base.NovaPersistentObject, base.NovaObject, db_service = db.service_update(self._context, self.id, updates) self._from_db_object(self._context, self, db_service) + self._send_status_update_notification(updates) + + def _send_status_update_notification(self, updates): + # Note(gibi): We do not trigger notification on version as that field + # is always dirty, which would cause that nova sends notification on + # every other field change. See the comment in save() too. + if set(updates.keys()).intersection( + {'disabled', 'disabled_reason', 'forced_down'}): + payload = ServiceStatusPayload(self) + 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) + @base.remotable def destroy(self): db.service_destroy(self._context, self.id) @@ -372,3 +391,47 @@ class ServiceList(base.ObjectListBase, base.NovaObject): context, db_services) return base.obj_make_list(context, cls(context), objects.Service, db_services) + + +@base.NovaObjectRegistry.register +class ServiceStatusNotification(notification.NotificationBase): + # Version 1.0: Initial version + VERSION = '1.0' + + fields = { + 'payload': fields.ObjectField('ServiceStatusPayload') + } + + +@base.NovaObjectRegistry.register +class ServiceStatusPayload(notification.NotificationPayloadBase): + SCHEMA = { + 'host': ('service', 'host'), + 'binary': ('service', 'binary'), + 'topic': ('service', 'topic'), + 'report_count': ('service', 'report_count'), + 'disabled': ('service', 'disabled'), + 'disabled_reason': ('service', 'disabled_reason'), + 'availability_zone': ('service', 'availability_zone'), + 'last_seen_up': ('service', 'last_seen_up'), + 'forced_down': ('service', 'forced_down'), + 'version': ('service', 'version') + } + # Version 1.0: Initial version + VERSION = '1.0' + fields = { + 'host': fields.StringField(nullable=True), + 'binary': fields.StringField(nullable=True), + 'topic': fields.StringField(nullable=True), + 'report_count': fields.IntegerField(), + 'disabled': fields.BooleanField(), + 'disabled_reason': fields.StringField(nullable=True), + 'availability_zone': fields.StringField(nullable=True), + 'last_seen_up': fields.DateTimeField(nullable=True), + 'forced_down': fields.BooleanField(), + 'version': fields.IntegerField(), + } + + def __init__(self, service): + super(ServiceStatusPayload, self).__init__() + self.populate_schema(service=service) diff --git a/nova/tests/functional/api_sample_tests/test_services.py b/nova/tests/functional/api_sample_tests/test_services.py index c51ebc226a6e..32db0e374da0 100644 --- a/nova/tests/functional/api_sample_tests/test_services.py +++ b/nova/tests/functional/api_sample_tests/test_services.py @@ -18,6 +18,8 @@ from oslo_utils import fixture as utils_fixture from nova.tests.functional.api_sample_tests import api_sample_base from nova.tests.unit.api.openstack.compute import test_services +from nova.tests.unit import fake_notifier + CONF = cfg.CONF CONF.import_opt('osapi_compute_extension', @@ -50,6 +52,18 @@ class ServicesJsonTest(api_sample_base.ApiSampleTestBaseV21): self.stub_out("nova.db.service_update", test_services.fake_service_update) self.useFixture(utils_fixture.TimeFixture(test_services.fake_utcnow())) + fake_notifier.stub_notifier(self.stubs) + self.addCleanup(fake_notifier.reset) + + def _verify_notification(self, **kwargs): + # TODO(gibi): store notification sample and start using that for + # verification instead + self.assertEqual(1, len(fake_notifier.VERSIONED_NOTIFICATIONS)) + payload = fake_notifier.VERSIONED_NOTIFICATIONS[0]['payload'] + fields = payload['nova_object.data'] + for key, value in kwargs.items(): + self.assertEqual(value, fields[key], + 'Mismatch in key %s' % key) def test_services_list(self): """Return a list of all agent builds.""" @@ -61,6 +75,7 @@ class ServicesJsonTest(api_sample_base.ApiSampleTestBaseV21): 'status': 'disabled', 'state': 'up'} self._verify_response('services-list-get-resp', subs, response, 200) + self.assertEqual(0, len(fake_notifier.VERSIONED_NOTIFICATIONS)) def test_service_enable(self): """Enable an existing agent build.""" @@ -70,6 +85,7 @@ class ServicesJsonTest(api_sample_base.ApiSampleTestBaseV21): 'service-enable-put-req', subs, api_version=self.microversion) self._verify_response('service-enable-put-resp', subs, response, 200) + self._verify_notification(disabled=False, disabled_reason=None) def test_service_disable(self): """Disable an existing agent build.""" @@ -79,6 +95,7 @@ class ServicesJsonTest(api_sample_base.ApiSampleTestBaseV21): 'service-disable-put-req', subs, api_version=self.microversion) self._verify_response('service-disable-put-resp', subs, response, 200) + self._verify_notification(disabled=True, disabled_reason=None) def test_service_disable_log_reason(self): """Disable an existing service and log the reason.""" @@ -90,6 +107,7 @@ class ServicesJsonTest(api_sample_base.ApiSampleTestBaseV21): api_version=self.microversion) self._verify_response('service-disable-log-put-resp', subs, response, 200) + self._verify_notification(disabled=True, disabled_reason='test2') def test_service_delete(self): """Delete an existing service.""" @@ -97,6 +115,7 @@ class ServicesJsonTest(api_sample_base.ApiSampleTestBaseV21): api_version=self.microversion) self.assertEqual(204, response.status_code) self.assertEqual("", response.content) + self.assertEqual(0, len(fake_notifier.VERSIONED_NOTIFICATIONS)) class ServicesV211JsonTest(ServicesJsonTest): @@ -116,6 +135,7 @@ class ServicesV211JsonTest(ServicesJsonTest): 'status': 'disabled', 'state': 'up'} self._verify_response('services-list-get-resp', subs, response, 200) + self.assertEqual(0, len(fake_notifier.VERSIONED_NOTIFICATIONS)) def test_force_down(self): """Set forced_down flag""" @@ -127,3 +147,4 @@ class ServicesV211JsonTest(ServicesJsonTest): api_version=self.microversion) self._verify_response('service-force-down-put-resp', subs, response, 200) + self._verify_notification(forced_down=True) diff --git a/nova/tests/unit/api/openstack/compute/test_services.py b/nova/tests/unit/api/openstack/compute/test_services.py index f01a8ec4f9f7..b54ff2c4c324 100644 --- a/nova/tests/unit/api/openstack/compute/test_services.py +++ b/nova/tests/unit/api/openstack/compute/test_services.py @@ -13,6 +13,7 @@ # under the License. +import copy import datetime import iso8601 @@ -142,6 +143,8 @@ def fake_db_service_update(services): service = _service_get_by_id(services, service_id) if service is None: raise exception.ServiceNotFound(service_id=service_id) + service = copy.deepcopy(service) + service.update(values) return service return service_update diff --git a/nova/tests/unit/objects/test_objects.py b/nova/tests/unit/objects/test_objects.py index 05b4a98b6896..fbe681e2ac79 100644 --- a/nova/tests/unit/objects/test_objects.py +++ b/nova/tests/unit/objects/test_objects.py @@ -1189,6 +1189,8 @@ object_data = { 'SecurityGroupRuleList': '1.2-0005c47fcd0fb78dd6d7fd32a1409f5b', 'Service': '1.19-8914320cbeb4ec29f252d72ce55d07e1', 'ServiceList': '1.17-b767102cba7cbed290e396114c3f86b3', + 'ServiceStatusNotification': '1.0-a73147b93b520ff0061865849d3dfa56', + 'ServiceStatusPayload': '1.0-b13764918aaa0e29acc868cf38a0c39b', 'TaskLog': '1.0-78b0534366f29aa3eebb01860fbe18fe', 'TaskLogList': '1.0-cc8cce1af8a283b9d28b55fcd682e777', 'Tag': '1.1-8b8d7d5b48887651a0e01241672e2963', diff --git a/nova/tests/unit/objects/test_service.py b/nova/tests/unit/objects/test_service.py index 2504f64a7c82..443e132c9d6d 100644 --- a/nova/tests/unit/objects/test_service.py +++ b/nova/tests/unit/objects/test_service.py @@ -22,6 +22,7 @@ from nova import db from nova import exception from nova import objects from nova.objects import aggregate +from nova.objects import fields from nova.objects import service from nova import test from nova.tests.unit.objects import test_compute_node @@ -400,3 +401,57 @@ class TestServiceVersion(test.TestCase): mock.sentinel.context, fake_service['id'], {'version': service.SERVICE_VERSION, 'host': 'foo'}) + + +class TestServiceStatusNotification(test.TestCase): + + @mock.patch('nova.objects.service.ServiceStatusNotification') + def _verify_notification(self, service_obj, mock_notification): + service_obj.save() + + self.assertTrue(mock_notification.called) + + event_type = mock_notification.call_args[1]['event_type'] + priority = mock_notification.call_args[1]['priority'] + publisher = mock_notification.call_args[1]['publisher'] + payload = mock_notification.call_args[1]['payload'] + + self.assertEqual(service_obj.host, publisher.host) + self.assertEqual(service_obj.binary, publisher.binary) + self.assertEqual(fields.NotificationPriority.INFO, priority) + self.assertEqual('service', event_type.object) + self.assertEqual(fields.NotificationAction.UPDATE, + event_type.action) + for field in service.ServiceStatusPayload.SCHEMA: + if field in fake_service: + self.assertEqual(fake_service[field], getattr(payload, field)) + + mock_notification.return_value.emit.assert_called_once_with( + mock.sentinel.context) + + @mock.patch('nova.db.service_update') + def test_service_update_with_notification(self, mock_db_service_update): + service_obj = objects.Service(context=mock.sentinel.context, + id=fake_service['id']) + mock_db_service_update.return_value = fake_service + for key, value in {'disabled': True, + 'disabled_reason': 'my reason', + 'forced_down': True}.items(): + setattr(service_obj, key, value) + self._verify_notification(service_obj) + + @mock.patch('nova.objects.service.ServiceStatusNotification') + @mock.patch('nova.db.service_update') + def test_service_update_without_notification(self, + mock_db_service_update, + mock_notification): + service_obj = objects.Service(context=mock.sentinel.context, + id=fake_service['id']) + + mock_db_service_update.return_value = fake_service + + for key, value in {'report_count': 13, + 'last_seen_up': timeutils.utcnow()}.items(): + setattr(service_obj, key, value) + service_obj.save() + self.assertFalse(mock_notification.called) diff --git a/releasenotes/notes/service-status-notification-e137297f5d5aa45d.yaml b/releasenotes/notes/service-status-notification-e137297f5d5aa45d.yaml new file mode 100644 index 000000000000..535492967d26 --- /dev/null +++ b/releasenotes/notes/service-status-notification-e137297f5d5aa45d.yaml @@ -0,0 +1,7 @@ +--- +features: + - A new service.status versioned notification has been introduced. + When the status of the Service object is changed nova will + send a new service.update notification with versioned payload + according to bp versioned-notification-api. +