diff options
author | Tim Simmons <tim.simmons@rackspace.com> | 2016-07-25 17:45:08 +0100 |
---|---|---|
committer | Tim Simmons <tim.simmons@rackspace.com> | 2017-01-19 16:10:15 +0000 |
commit | 93c0161242c1047e2fbe6dc241f1eaf3ffca874c (patch) | |
tree | a4c5510c73663208b6dd5843eb4af981398c0ba8 | |
parent | 8966e7166b1a5e3dc0df5504de0ba002e2ddf8e5 (diff) | |
download | designate-93c0161242c1047e2fbe6dc241f1eaf3ffca874c.tar.gz |
Make notifications pluggable
- Defines a plugin interface for what's actually emitted as part
of designate "notifications".
- The default plugin emits the same thing as notifications did prior
to this patch.
- The "audit" notification plugin emits recordset data changes and
zone/recordset names, if they exist, the notifications with this
plugin look like http://paste.openstack.org/show/545210/
- Adds support for multiple notifications for a single change
- Also adds client IP to the context object, as it's a field that
may be of interest to some types of notifications
- Many tests
Change-Id: I01118fae8ce6e38ccc61b0ce763fd759affd9a86
-rw-r--r-- | contrib/consume.py | 39 | ||||
-rw-r--r-- | designate/api/middleware.py | 5 | ||||
-rw-r--r-- | designate/central/service.py | 15 | ||||
-rw-r--r-- | designate/context.py | 16 | ||||
-rw-r--r-- | designate/notifications.py | 182 | ||||
-rw-r--r-- | designate/tests/unit/test_central/test_notifications.py | 498 | ||||
-rw-r--r-- | releasenotes/notes/pluggable-notifications-bd049b755a2ace9c.yaml | 21 | ||||
-rw-r--r-- | setup.cfg | 3 |
8 files changed, 772 insertions, 7 deletions
diff --git a/contrib/consume.py b/contrib/consume.py new file mode 100644 index 00000000..c3f141d2 --- /dev/null +++ b/contrib/consume.py @@ -0,0 +1,39 @@ +# Copyright 2016 Rackspace, Inc. +# +# Author: Tim Simmons <tim.simmons@rackspace.com> +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +""" +This dumb script allows you to see what's being dumped onto +the notifications.info queue + +nabbed from: +https://pika.readthedocs.io/en/latest/examples/blocking_consume.html +""" +import pika + + +def on_message(channel, method_frame, header_frame, body): + print(method_frame.delivery_tag) + print(body) + channel.basic_ack(delivery_tag=method_frame.delivery_tag) + + +connection = pika.BlockingConnection() +channel = connection.channel() +channel.basic_consume(on_message, 'notifications.info') +try: + channel.start_consuming() +except KeyboardInterrupt: + channel.stop_consuming() +connection.close() diff --git a/designate/api/middleware.py b/designate/api/middleware.py index 49cfc024..0a941de8 100644 --- a/designate/api/middleware.py +++ b/designate/api/middleware.py @@ -107,6 +107,10 @@ class ContextMiddleware(base.Middleware): strutils.bool_from_string( request.headers.get('X-Designate-Edit-Managed-Records')) + def _extract_client_addr(self, ctxt, request): + if hasattr(request, 'client_addr'): + ctxt.client_addr = request.client_addr + def make_context(self, request, *args, **kwargs): req_id = request.environ.get(request_id.ENV_REQUEST_ID) kwargs.setdefault('request_id', req_id) @@ -118,6 +122,7 @@ class ContextMiddleware(base.Middleware): self._extract_all_projects(ctxt, request) self._extract_edit_managed_records(ctxt, request) self._extract_dns_hide_counts(ctxt, request) + self._extract_client_addr(ctxt, request) finally: request.environ['context'] = ctxt return ctxt diff --git a/designate/central/service.py b/designate/central/service.py index 52852e56..2f512702 100644 --- a/designate/central/service.py +++ b/designate/central/service.py @@ -42,6 +42,7 @@ from designate import context as dcontext from designate import exceptions from designate import dnsutils from designate import network_api +from designate import notifications from designate import objects from designate import policy from designate import quota @@ -157,11 +158,17 @@ def notification(notification_type): # Call the wrapped function result = f(self, *args, **kwargs) + # Feed the args/result to a notification plugin + # to determine what is emitted + payloads = notifications.get_plugin().emit( + notification_type, context, result, args, kwargs) + # Enqueue the notification - LOG.debug('Queueing notification for %(type)s ', - {'type': notification_type}) - NOTIFICATION_BUFFER.queue.appendleft( - (context, notification_type, result,)) + for payload in payloads: + LOG.debug('Queueing notification for %(type)s ', + {'type': notification_type}) + NOTIFICATION_BUFFER.queue.appendleft( + (context, notification_type, payload,)) return result diff --git a/designate/context.py b/designate/context.py index 4c2a695b..72a632c8 100644 --- a/designate/context.py +++ b/designate/context.py @@ -32,10 +32,12 @@ class DesignateContext(context.RequestContext): _abandon = None original_tenant = None _edit_managed_records = False + _client_addr = None def __init__(self, service_catalog=None, all_tenants=False, abandon=None, tsigkey_id=None, user_identity=None, original_tenant=None, - edit_managed_records=False, hide_counts=False, **kwargs): + edit_managed_records=False, hide_counts=False, + client_addr=None, **kwargs): # NOTE: user_identity may be passed in, but will be silently dropped as # it is a generated field based on several others. @@ -50,6 +52,7 @@ class DesignateContext(context.RequestContext): self.abandon = abandon self.edit_managed_records = edit_managed_records self.hide_counts = hide_counts + self.client_addr = client_addr def deepcopy(self): d = self.to_dict() @@ -83,7 +86,8 @@ class DesignateContext(context.RequestContext): 'abandon': self.abandon, 'edit_managed_records': self.edit_managed_records, 'tsigkey_id': self.tsigkey_id, - 'hide_counts': self.hide_counts + 'hide_counts': self.hide_counts, + 'client_addr': self.client_addr, }) return copy.deepcopy(d) @@ -184,6 +188,14 @@ class DesignateContext(context.RequestContext): policy.check('edit_managed_records', self) self._edit_managed_records = value + @property + def client_addr(self): + return self._client_addr + + @client_addr.setter + def client_addr(self, value): + self._client_addr = value + def get_current(): return context.get_current() diff --git a/designate/notifications.py b/designate/notifications.py index 817c30e1..e9c6dd81 100644 --- a/designate/notifications.py +++ b/designate/notifications.py @@ -15,21 +15,28 @@ # under the License. # # Copied: nova.notifications +import abc from oslo_config import cfg from oslo_log import log as logging +from designate.i18n import _LW +from designate.plugin import DriverPlugin +from designate import objects from designate import rpc LOG = logging.getLogger(__name__) notify_opts = [ cfg.BoolOpt('notify_api_faults', default=False, - help='Send notifications if there\'s a failure in the API.') + help='Send notifications if there\'s a failure in the API.'), + cfg.StrOpt('notification-plugin', default='default', + help='The notification plugin to use'), ] CONF = cfg.CONF CONF.register_opts(notify_opts) +NOTIFICATION_PLUGIN = None def send_api_fault(context, url, status, exception): @@ -41,3 +48,176 @@ def send_api_fault(context, url, status, exception): payload = {'url': url, 'exception': str(exception), 'status': status} rpc.get_notifier('api').error(context, 'dns.api.fault', payload) + + +def init_notification_plugin(): + LOG.debug("Loading notification plugin: %s" % cfg.CONF.notification_plugin) + cls = NotificationPlugin.get_driver(cfg.CONF.notification_plugin) + + global NOTIFICATION_PLUGIN + NOTIFICATION_PLUGIN = cls() + + +def get_plugin(): + if NOTIFICATION_PLUGIN is None: + init_notification_plugin() + return NOTIFICATION_PLUGIN + + +class NotificationPlugin(DriverPlugin): + """Base class for Notification Driver implementations""" + __plugin_type__ = 'notification' + __plugin_ns__ = 'designate.notification.plugin' + + def __init__(self): + super(NotificationPlugin, self).__init__() + + @abc.abstractmethod + def emit(self, notification_type, context, result, *args, **kwargs): + """Return a payload to emit as part of the notification""" + + +class Default(NotificationPlugin): + """Returns the result, as implemented in the base class""" + __plugin_name__ = 'default' + + def emit(self, notification_type, context, result, *args, **kwargs): + """Return the result of the function called""" + return [result] + + +class Audit(NotificationPlugin): + """Grabs Zone/Recordset names and RRData changes""" + __plugin_name__ = 'audit' + + def zone_name(self, arglist, result): + for arg in arglist + [result]: + if isinstance(arg, objects.Zone): + if arg.name is not None: + return arg.name + if hasattr(arg, 'zone_name'): + if arg.zone_name is not None: + return arg.zone_name + + return None + + def zone_id(self, arglist, result): + for arg in arglist + [result]: + if isinstance(arg, objects.Zone): + if arg.id is not None: + return arg.id + if hasattr(arg, 'zone_id'): + if arg.zone_id is not None: + return arg.zone_id + + return None + + def recordset_name(self, arglist, result): + for arg in arglist + [result]: + if isinstance(arg, objects.RecordSet): + if arg.name is not None: + return arg.name + + return None + + def recordset_data(self, arglist, result): + if not isinstance(result, objects.RecordSet): + return [] + + for arg in arglist: + if isinstance(arg, objects.RecordSet): + if 'records' not in arg.obj_what_changed(): + return [] + original_rrs = arg.obj_get_original_value('records') + + old_value = ' '.join( + [obj['data'] for obj in original_rrs]) + + new_value = ' '.join( + [rr.data for rr in result.records]) + + if old_value == new_value: + return [] + + return [{ + 'change': 'records', + 'old_value': old_value, + 'new_value': new_value, + }] + + return [] + + def other_data(self, arglist, result): + changes = [] + + for arg in arglist: + if isinstance(arg, objects.DesignateObject): + for change in arg.obj_what_changed(): + if change != 'records': + old_value = arg.obj_get_original_value(change) + new_value = getattr(arg, change) + + # Just in case something odd makes it here + if any(type(val) not in + (int, float, bool, str, type(None)) + for val in (old_value, new_value)): + old_value, new_value = None, None + msg = _LW("Nulling notification values after " + "unexpected values %s") + LOG.warning(msg, (old_value, new_value)) + + if old_value == new_value: + continue + + changes.append({ + 'change': change, + 'old_value': str(old_value), + 'new_value': str(new_value), + }) + + return changes + + def blank_event(self): + return [{ + 'change': None, + 'old_value': None, + 'new_value': None, + }] + + def gather_changes(self, arglist, result, notification_type): + changes = [] + + if 'update' in notification_type: + changes.extend(self.other_data(arglist, result)) + if notification_type == 'dns.recordset.update': + changes.extend(self.recordset_data(arglist, result)) + elif 'create' in notification_type: + if notification_type == 'dns.recordset.create': + changes.extend(self.recordset_data(arglist, result)) + else: + changes.extend(self.blank_event()) + else: + changes.extend(self.blank_event()) + + return changes + + def emit(self, notification_type, context, result, *args, **kwargs): + arglist = [] + for item in args: + if isinstance(item, tuple) or isinstance(item, list): + arglist.extend(item) + if isinstance(item, dict): + arglist.extend(list(item.values())) + + payloads = [] + for change in self.gather_changes(arglist, result, notification_type): + payloads.append({ + 'zone_name': self.zone_name(arglist, result), + 'zone_id': self.zone_id(arglist, result), + 'recordset_name': self.recordset_name(arglist, result), + 'old_data': change['old_value'], + 'new_data': change['new_value'], + 'changed_field': change['change'], + }) + + return payloads diff --git a/designate/tests/unit/test_central/test_notifications.py b/designate/tests/unit/test_central/test_notifications.py new file mode 100644 index 00000000..8e874927 --- /dev/null +++ b/designate/tests/unit/test_central/test_notifications.py @@ -0,0 +1,498 @@ +# Copyright 2016 Rackspace +# +# Author: Tim Simmons <tim.simmons@rackspace.com> +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +import unittest + +import mock +from oslo_log import log as logging + +from designate import objects +from designate import notifications + +LOG = logging.getLogger(__name__) + + +class DefaultNotificationTest(unittest.TestCase): + + def setUp(self): + self.driver = notifications.Default() + + self.context = mock.Mock() + + def test_default_notifications(self): + result = 'result' + event = 'dns.zone.create' + args = ('foo', 'bar',) + kwargs = {'wumbo': 'mumbo'} + + driver_result = self.driver.emit( + event, self.context, result, args, kwargs) + self.assertEqual(driver_result, ['result']) + + +class AuditNotificationTest(unittest.TestCase): + + def setUp(self): + self.driver = notifications.Audit() + + self.context = mock.Mock() + self.maxDiff = None + + # + # Zone changes + # + + def test_audit_zone_name(self): + zone = objects.Zone( + name='example.com.', + type='PRIMARY', + ) + + result = zone + event = 'dns.zone.create' + args = (zone,) + kwargs = {'wumbo': 'mumbo'} + + expected = [{ + 'changed_field': None, + 'new_data': None, + 'old_data': None, + 'recordset_name': None, + 'zone_id': None, + 'zone_name': 'example.com.' + }] + driver_result = self.driver.emit( + event, self.context, result, args, kwargs) + self.assertEqual(driver_result, expected) + + def test_audit_zone_id(self): + zone = objects.Zone( + id='123', + name='example.com.', + type='PRIMARY', + ) + + result = zone + event = 'dns.zone.create' + args = (zone,) + kwargs = {'wumbo': 'mumbo'} + + expected = [{ + 'changed_field': None, + 'new_data': None, + 'old_data': None, + 'recordset_name': None, + 'zone_id': '123', + 'zone_name': 'example.com.' + }] + driver_result = self.driver.emit( + event, self.context, result, args, kwargs) + self.assertEqual(driver_result, expected) + + def test_audit_zone_update(self): + zone = objects.Zone( + id='123', + name='example.com.', + type='PRIMARY', + ttl=1 + ) + zone.ttl = 300 + + result = zone + event = 'dns.zone.update' + args = (zone,) + kwargs = {'wumbo': 'mumbo'} + + expected = [{ + 'changed_field': 'ttl', + 'new_data': '300', + 'old_data': '1', + 'recordset_name': None, + 'zone_id': '123', + 'zone_name': 'example.com.' + }] + driver_result = self.driver.emit( + event, self.context, result, args, kwargs) + self.assertEqual(driver_result, expected) + + def test_audit_zone_delete(self): + zone = objects.Zone( + id='123', + name='example.com.', + type='PRIMARY', + ttl=1 + ) + + result = zone + event = 'dns.zone.delete' + args = ('123',) + kwargs = {'wumbo': 'mumbo'} + + expected = [{ + 'changed_field': None, + 'new_data': None, + 'old_data': None, + 'recordset_name': None, + 'zone_id': '123', + 'zone_name': 'example.com.' + }] + driver_result = self.driver.emit( + event, self.context, result, args, kwargs) + self.assertEqual(driver_result, expected) + + # + # Recordset Changes + # + + def test_audit_rrset_name(self): + rrset = objects.RecordSet( + name='foo.example.com.', + type='PRIMARY', + records=objects.RecordList.from_list([]) + ) + + rrset.records = objects.RecordList.from_list( + [{'data': '192.168.1.1'}]) + + result = rrset + event = 'dns.recordset.create' + args = (rrset,) + kwargs = {'wumbo': 'mumbo'} + + expected = [{ + 'changed_field': 'records', + 'new_data': '192.168.1.1', + 'old_data': '', + 'recordset_name': 'foo.example.com.', + 'zone_id': None, + 'zone_name': None + }] + driver_result = self.driver.emit( + event, self.context, result, args, kwargs) + self.assertEqual(driver_result, expected) + + def test_audit_rrset_create(self): + rrset = objects.RecordSet( + name='foo.example.com.', + type='PRIMARY', + records=[], + zone_id='123', + zone_name='example.com.' + ) + + rrset.records = objects.RecordList.from_list( + [{'data': '192.168.1.1'}]) + + result = rrset + event = 'dns.recordset.create' + args = (rrset,) + kwargs = {'wumbo': 'mumbo'} + + expected = [{ + 'changed_field': 'records', + 'new_data': '192.168.1.1', + 'old_data': '', + 'recordset_name': 'foo.example.com.', + 'zone_id': '123', + 'zone_name': 'example.com.' + }] + driver_result = self.driver.emit( + event, self.context, result, args, kwargs) + self.assertEqual(driver_result, expected) + + def test_audit_rrset_update_records(self): + rrset = objects.RecordSet( + name='foo.example.com.', + type='PRIMARY', + records=objects.RecordList.from_list( + [{'data': '192.168.1.1'}]), + zone_id='123', + zone_name='example.com.' + ) + + rrset.records = objects.RecordList.from_list( + [{'data': '192.168.1.2'}]) + + result = rrset + event = 'dns.recordset.update' + args = (rrset,) + kwargs = {'wumbo': 'mumbo'} + + expected = [{ + 'changed_field': 'records', + 'new_data': '192.168.1.2', + 'old_data': '192.168.1.1', + 'recordset_name': 'foo.example.com.', + 'zone_id': '123', + 'zone_name': 'example.com.' + }] + driver_result = self.driver.emit( + event, self.context, result, args, kwargs) + self.assertEqual(driver_result, expected) + + def test_audit_rrset_update_other(self): + rrset = objects.RecordSet( + name='foo.example.com.', + type='PRIMARY', + records=objects.RecordList.from_list( + [{'data': '192.168.1.1'}]), + zone_id='123', + zone_name='example.com.', + ttl=300 + ) + + rrset.ttl = 400 + + result = rrset + event = 'dns.recordset.update' + args = (rrset,) + kwargs = {'wumbo': 'mumbo'} + + expected = [{ + 'changed_field': 'ttl', + 'new_data': '400', + 'old_data': '300', + 'recordset_name': 'foo.example.com.', + 'zone_id': '123', + 'zone_name': 'example.com.' + }] + driver_result = self.driver.emit( + event, self.context, result, args, kwargs) + self.assertEqual(driver_result, expected) + + def test_audit_rrset_delete(self): + rrset = objects.RecordSet( + name='foo.example.com.', + type='PRIMARY', + records=objects.RecordList.from_list([]), + zone_id='123', + zone_name='example.com.', + id='1', + ) + + result = rrset + event = 'dns.recordset.delete' + args = ('123', '1',) + kwargs = {'wumbo': 'mumbo'} + + expected = [{ + 'changed_field': None, + 'new_data': None, + 'old_data': None, + 'recordset_name': 'foo.example.com.', + 'zone_id': '123', + 'zone_name': 'example.com.' + }] + driver_result = self.driver.emit( + event, self.context, result, args, kwargs) + self.assertEqual(driver_result, expected) + + # + # Zone Imports + # + + def test_audit_import_create(self): + zimport = objects.ZoneImport( + zone_id='123', + ) + + result = zimport + event = 'dns.zone_import.create' + args = (zimport,) + kwargs = {'wumbo': 'mumbo'} + + expected = [{ + 'changed_field': None, + 'new_data': None, + 'old_data': None, + 'recordset_name': None, + 'zone_id': '123', + 'zone_name': None + }] + driver_result = self.driver.emit( + event, self.context, result, args, kwargs) + self.assertEqual(driver_result, expected) + + def test_audit_import_delete(self): + zimport = objects.ZoneImport( + zone_id='123', + ) + + result = zimport + event = 'dns.zone_import.create' + args = ('1') + kwargs = {'wumbo': 'mumbo'} + + expected = [{ + 'changed_field': None, + 'new_data': None, + 'old_data': None, + 'recordset_name': None, + 'zone_id': '123', + 'zone_name': None + }] + driver_result = self.driver.emit( + event, self.context, result, args, kwargs) + self.assertEqual(driver_result, expected) + + # + # Zone Exports + # + + def test_audit_export_create(self): + zexport = objects.ZoneExport( + zone_id='123', + ) + + result = zexport + event = 'dns.zone_export.create' + args = (zexport,) + kwargs = {'wumbo': 'mumbo'} + + expected = [{ + 'changed_field': None, + 'new_data': None, + 'old_data': None, + 'recordset_name': None, + 'zone_id': '123', + 'zone_name': None + }] + driver_result = self.driver.emit( + event, self.context, result, args, kwargs) + self.assertEqual(driver_result, expected) + + def test_audit_export_delete(self): + zexport = objects.ZoneExport( + zone_id='123', + ) + + result = zexport + event = 'dns.zone_export.create' + args = ('1') + kwargs = {'wumbo': 'mumbo'} + + expected = [{ + 'changed_field': None, + 'new_data': None, + 'old_data': None, + 'recordset_name': None, + 'zone_id': '123', + 'zone_name': None + }] + driver_result = self.driver.emit( + event, self.context, result, args, kwargs) + self.assertEqual(driver_result, expected) + + # + # Zone Transfer Requests + # + def test_audit_transfer_request_create(self): + ztransfer_request = objects.ZoneTransferRequest( + zone_id='123', + zone_name='example.com.', + target_tenant_id='tenant_a', + ) + + result = ztransfer_request + event = 'dns.zone_transfer_request.create' + args = (ztransfer_request,) + kwargs = {'wumbo': 'mumbo'} + + expected = [{ + 'changed_field': None, + 'new_data': None, + 'old_data': None, + 'recordset_name': None, + 'zone_id': '123', + 'zone_name': 'example.com.' + }] + driver_result = self.driver.emit( + event, self.context, result, args, kwargs) + self.assertEqual(driver_result, expected) + + def test_audit_transfer_request_update(self): + ztransfer_request = objects.ZoneTransferRequest( + zone_id='123', + zone_name='example.com.', + target_tenant_id='tenant_a', + ) + + ztransfer_request.target_tenant_id = 'tenant_b' + + result = ztransfer_request + event = 'dns.zone_transfer_request.update' + args = (ztransfer_request,) + kwargs = {'wumbo': 'mumbo'} + + expected = [{ + 'changed_field': 'target_tenant_id', + 'new_data': 'tenant_b', + 'old_data': 'tenant_a', + 'recordset_name': None, + 'zone_id': '123', + 'zone_name': 'example.com.' + }] + driver_result = self.driver.emit( + event, self.context, result, args, kwargs) + self.assertEqual(driver_result, expected) + + def test_audit_transfer_request_delete(self): + ztransfer_request = objects.ZoneTransferRequest( + zone_id='123', + zone_name='example.com.', + target_tenant_id='tenant_a', + ) + + result = ztransfer_request + event = 'dns.zone_transfer_request.create' + args = ('1') + kwargs = {'wumbo': 'mumbo'} + + expected = [{ + 'changed_field': None, + 'new_data': None, + 'old_data': None, + 'recordset_name': None, + 'zone_id': '123', + 'zone_name': 'example.com.' + }] + driver_result = self.driver.emit( + event, self.context, result, args, kwargs) + self.assertEqual(driver_result, expected) + + # + # Zone Transfer Requests + # + def test_audit_transfer_accept_create(self): + ztransfer_accept = objects.ZoneTransferAccept( + zone_id='123', + ) + + result = ztransfer_accept + event = 'dns.zone_transfer_accept.create' + args = (ztransfer_accept,) + kwargs = {'wumbo': 'mumbo'} + + expected = [{ + 'changed_field': None, + 'new_data': None, + 'old_data': None, + 'recordset_name': None, + 'zone_id': '123', + 'zone_name': None + }] + driver_result = self.driver.emit( + event, self.context, result, args, kwargs) + self.assertEqual(driver_result, expected) diff --git a/releasenotes/notes/pluggable-notifications-bd049b755a2ace9c.yaml b/releasenotes/notes/pluggable-notifications-bd049b755a2ace9c.yaml new file mode 100644 index 00000000..c669d8f7 --- /dev/null +++ b/releasenotes/notes/pluggable-notifications-bd049b755a2ace9c.yaml @@ -0,0 +1,21 @@ +--- +features: + - Operators now have a choice in the type of notification payload + content that Designate can emit via oslo.messaging's Notifier. + + The default plugin is configured to emit the same information + that the notifications previous to this patch emitted. So there + is no functional change. + + Operators can write their own notification plugins that exist + in designate/notifications.py. + + An "audit" plugin is included. This plugin emits object changes, + if they exist, along with zone ids, zone/recordset names. + + A plugin can define multiple payloads from a single notification + to be emitted at once, if desirable. + + The selection of a plugin is defined by python entrypoints (for + driver availability) and the new "notification_plugin" option + in the "DEFAULT" config section. @@ -137,6 +137,9 @@ designate.heartbeat_emitter = noop = designate.service_status:NoopEmitter rpc = designate.service_status:RpcEmitter +designate.notification.plugin = + default = designate.notifications:Default + audit = designate.notifications:Audit [build_sphinx] all_files = 1 |