Description: Add new floating IP handler for sink
Author: Axel Jacquet <axel.jacquet@infomaniak.com>
Forwarded: no
Last-Update: 2023-03-08

Index: designate/designate/notification_handler/neutron_floatingip_ng.py
===================================================================
--- /dev/null
+++ designate/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['id']
+            )
+    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)
Index: designate/setup.cfg
===================================================================
--- designate.orig/setup.cfg
+++ designate/setup.cfg
@@ -72,6 +72,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
Index: designate/designate/conf/sink.py
===================================================================
--- designate.orig/designate/conf/sink.py
+++ designate/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_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,
     }
Index: designate/designate/central/rpcapi.py
===================================================================
--- designate.orig/designate/central/rpcapi.py
+++ designate/designate/central/rpcapi.py
@@ -237,6 +237,14 @@ class CentralAPI:
                                 limit=limit, sort_key=sort_key,
                                 sort_dir=sort_dir, force_index=force_index)
 
+    def find_records(self, context, criterion=None, marker=None, limit=None,
+                     sort_key=None, sort_dir=None):
+        return self.client.call(context, 'find_records', criterion=criterion,
+                                marker=marker, limit=limit, sort_key=sort_key,
+                                sort_dir=sort_dir)
+    def find_recordset(self, context, criterion=None):
+        return self.client.call(context, 'find_recordset', criterion=criterion)
+
     def create_managed_records(self, context, zone_id, records_values,
                                recordset_values):
         return self.client.call(context, 'create_managed_records',
Index: designate/designate/notification_handler/base.py
===================================================================
--- designate.orig/designate/notification_handler/base.py
+++ designate/designate/notification_handler/base.py
@@ -63,7 +63,49 @@ class NotificationHandler(ExtensionPlugi
         context = DesignateContext.get_admin_context(all_tenants=True)
         return self.central_api.get_zone(context, zone_id)
 
+    def _update_or_delete_recordset(self, context, zone_id, recordset_id,
+                                    record_to_delete_id):
+        LOG.debug(
+            'Deleting record in %s / %s',
+            zone_id, record_to_delete_id
+        )
 
+        try:
+            recordset = self.central_api.find_recordset(
+                context, {'id': recordset_id, 'zone_id': zone_id}
+            )
+            record_ids = [record['id'] for record in recordset.records]
+
+            # Record no longer in recordset. Let's abort.
+            if record_to_delete_id not in record_ids:
+                LOG.debug(
+                    'Record %s not found in recordset %s',
+                    record_to_delete_id, recordset_id
+                )
+                return
+
+            # Remove the record from the recordset.
+            for record in list(recordset.records):
+                if record['id'] != record_to_delete_id:
+                    continue
+                recordset.records.remove(record)
+                break
+
+            if not recordset.records:
+                # Recordset is now empty. Remove it.
+                self.central_api.delete_recordset(
+                    context, zone_id, recordset_id
+                )
+                return
+
+            # Recordset still has records, validate it and update it.
+            recordset.validate()
+            self.central_api.update_recordset(context, recordset)
+        except exceptions.RecordSetNotFound:
+            LOG.info(
+                'Recordset %s for record %s was already removed',
+                recordset_id, record_to_delete_id
+            )
 class BaseAddressHandler(NotificationHandler):
     default_formatv4 = ('%(hostname)s.%(zone)s',)
     default_formatv6 = ('%(hostname)s.%(zone)s',)
