From 13bb216eb10544caeebb775aa3ac15ee2535840c Mon Sep 17 00:00:00 2001
From: Thomas Goirand <zigo@debian.org>
Date: Wed, 15 Mar 2023 11:03:02 +0100
Subject: [PATCH] Add a new floating IP notification handler

The older handler needed a zone_id to be able to delete the
FloatingIP associated recordsets. This new handler works
without the zone_id, and make it possible for operators to
have their recordsets automatically deleted when a floating
IP is deleted even if they manage more than a single public
IP range.

Change-Id: Ic27796fdad347ed44634ee8fa6367cfa93d8c8fa
---
 designate/conf/sink.py                        | 18 +++++
 .../neutron_floatingip_ng.py                  | 72 +++++++++++++++++++
 .../test_neutron_ng.py                        | 67 +++++++++++++++++
 setup.cfg                                     |  1 +
 4 files changed, 158 insertions(+)
 create mode 100644 designate/notification_handler/neutron_floatingip_ng.py
 create mode 100644 designate/tests/test_notification_handler/test_neutron_ng.py

diff --git a/designate/conf/sink.py b/designate/conf/sink.py
index 4b24495d..d17e0b5d 100644
--- a/designate/conf/sink.py
+++ b/designate/conf/sink.py
@@ -84,6 +84,21 @@ SINK_NOVA_OPTS = [
     cfg.MultiStrOpt('formatv6', help='IPv6 format'),
 ]
 
+SINK_NEUTRON_NG_GROUP = cfg.OptGroup(
+    name='handler:neutron_floatingip_ng',
+    title="Configuration for PTR delete Notification Handler"
+)
+
+SINK_NEUTRON_NG_OPTS = [
+    cfg.ListOpt('notification_topics', default=['notifications,notifications_designate'],
+               help='notification any events from neutron'),
+    cfg.StrOpt('control_exchange', default='neutron',
+              help='control-exchange for neutron notification'),
+    cfg.MultiStrOpt('format', deprecated_for_removal=True,
+                    deprecated_reason="Replaced by 'formatv4/formatv6'",
+                    help='format which replaced by formatv4/formatv6'),
+]
+
 
 def register_opts(conf):
     conf.register_group(SINK_GROUP)
@@ -94,6 +109,8 @@ def register_opts(conf):
     conf.register_opts(SINK_NEUTRON_OPTS, group=SINK_NEUTRON_GROUP)
     conf.register_group(SINK_NOVA_GROUP)
     conf.register_opts(SINK_NOVA_OPTS, group=SINK_NOVA_GROUP)
+    conf.register_group(SINK_NEUTRON_NG_GROUP)
+    conf.register_opts(SINK_NEUTRON_NG_OPTS, group=SINK_NEUTRON_NG_GROUP)
 
 
 def list_opts():
@@ -101,4 +118,5 @@ def list_opts():
         SINK_GROUP: SINK_OPTS,
         SINK_NEUTRON_GROUP: SINK_NEUTRON_OPTS,
         SINK_NOVA_GROUP: SINK_NOVA_OPTS,
+        SINK_NEUTRON_NG_GROUP: SINK_NEUTRON_NG_OPTS,
     }
diff --git a/designate/notification_handler/neutron_floatingip_ng.py b/designate/notification_handler/neutron_floatingip_ng.py
new file mode 100644
index 00000000..2774ad18
--- /dev/null
+++ b/designate/notification_handler/neutron_floatingip_ng.py
@@ -0,0 +1,72 @@
+# Copyright 2023 Infomaniak Networks SA
+#
+# Author: Axel Jacquet <axel.jacquet@infomaniak.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.
+
+from oslo_config import cfg
+from oslo_log import log as logging
+
+from designate.notification_handler import base
+from designate.context import DesignateContext
+
+LOG = logging.getLogger(__name__)
+
+
+class NeutronFloatingHandlerNG(base.NotificationHandler):
+    """Handler for Neutron's notifications"""
+    __plugin_name__ = 'neutron_floatingip_ng'
+
+    def get_exchange_topics(self):
+        exchange = cfg.CONF[self.name].control_exchange
+
+        topics = [topic for topic in cfg.CONF[self.name].notification_topics]
+
+        return (exchange, topics)
+
+    def get_event_types(self):
+        return [
+            'floatingip.update.end',
+            'floatingip.delete.start'
+        ]
+
+    def delete_ptr(self, payload):
+        """
+        Handle a generic delete of a fixed ip within a zone
+
+        :param zone_id: The ID of the designate zone.
+        :param resource_id: The managed resource ID
+        :param resource_type: The managed resource type
+        :param criterion: Criterion to search and destroy records
+        """
+        context = DesignateContext().elevated()
+        context.all_tenants = True
+        context.edit_managed_records = True
+
+        criterion = {
+            'managed': True,
+            'managed_resource_id': payload['floatingip_id'],
+            'managed_resource_type': 'ptr:floatingip'
+        }
+
+        records = self.central_api.find_records(context, criterion)
+        for record in records:
+            self._update_or_delete_recordset(
+                context, record['zone_id'], record['recordset_id'], record
+            )
+    def process_notification(self, context, event_type, payload):
+        LOG.debug('%s received notification - %s',
+                  self.get_canonical_name(), event_type)
+
+        if event_type.startswith('floatingip.delete'):
+            self.delete_ptr(payload)
diff --git a/designate/tests/test_notification_handler/test_neutron_ng.py b/designate/tests/test_notification_handler/test_neutron_ng.py
new file mode 100644
index 00000000..d31696e3
--- /dev/null
+++ b/designate/tests/test_notification_handler/test_neutron_ng.py
@@ -0,0 +1,67 @@
+# Copyright 2012 Managed I.T.
+#
+# Author: Kiall Mac Innes <kiall@managedit.ie>
+#
+# 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.
+from oslo_log import log as logging
+
+from designate.notification_handler.neutron_floatingip_ng import NeutronFloatingHandlerNG
+from designate.tests.test_notification_handler import NotificationHandlerMixin
+from designate.tests import TestCase
+
+LOG = logging.getLogger(__name__)
+
+
+class NeutronFloatingHandlerNGTest(TestCase, NotificationHandlerMixin):
+    def setUp(self):
+        super(NeutronFloatingHandlerTest, self).setUp()
+
+        self.config(notification_topics='notifications,notifications_designate', group='handler:neutron_floatingip_ng')
+
+        self.plugin = NeutronFloatingHandlerNG()
+
+    def test_floatingip_delete(self):
+        start_event_type = 'floatingip.update.end'
+        start_fixture = self.get_notification_fixture(
+            'neutron', start_event_type + '_associate')
+        self.plugin.process_notification(self.admin_context.to_dict(),
+                                         start_event_type,
+                                         start_fixture['payload'])
+
+        event_type = 'floatingip.delete.start'
+        fixture = self.get_notification_fixture(
+            'neutron', event_type)
+
+        self.assertIn(event_type, self.plugin.get_event_types())
+
+        payload = fixture['payload']
+
+        criterion = {
+            'managed': True,
+            'managed_resource_id': payload['floatingip_id'],
+            'managed_resource_type': 'ptr:floatingip'
+        }
+
+        # Ensure we start with exactly 2 records, plus NS and SOA
+        records = self.central_service.find_records(self.admin_context,
+                                                    criterion)
+        self.assertEqual(4, len(records))
+
+        self.plugin.process_notification(
+            self.admin_context.to_dict(), event_type, fixture['payload'])
+
+        # Ensure we now have exactly 0 recordsets, plus NS and SOA
+        recordsets = self.central_service.find_recordsets(self.admin_context,
+                                                          criterion)
+
+        self.assertEqual(2, len(recordsets))
diff --git a/setup.cfg b/setup.cfg
index 92f789f1..e1cb632d 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -70,6 +70,7 @@ designate.notification.handler =
     fake = designate.notification_handler.fake:FakeHandler
     nova_fixed = designate.notification_handler.nova:NovaFixedHandler
     neutron_floatingip = designate.notification_handler.neutron:NeutronFloatingHandler
+    neutron_floatingip_ng = designate.notification_handler.neutron_floatingip_ng:NeutronFloatingHandlerNG
 
 designate.backend =
     bind9 = designate.backend.impl_bind9:Bind9Backend
-- 
2.30.2

