← Back to team overview

sts-sponsors team mailing list archive

[Merge] ~cgrabowski/maas:fix_node_dns_generation into maas:master

 

Christian Grabowski has proposed merging ~cgrabowski/maas:fix_node_dns_generation into maas:master.

Commit message:
add trigger when existing static ip updates

add DELETE-IFACE-IP

add triggers for interface to ip dynamic DNS updates



Requested reviews:
  MAAS Maintainers (maas-maintainers)

For more details, see:
https://code.launchpad.net/~cgrabowski/maas/+git/maas/+merge/434522
-- 
Your team MAAS Committers is subscribed to branch maas:master.
diff --git a/src/maasserver/dns/config.py b/src/maasserver/dns/config.py
index c484570..ea834de 100644
--- a/src/maasserver/dns/config.py
+++ b/src/maasserver/dns/config.py
@@ -21,6 +21,7 @@ from maasserver.models.dnsdata import DNSData
 from maasserver.models.dnspublication import DNSPublication
 from maasserver.models.dnsresource import DNSResource
 from maasserver.models.domain import Domain
+from maasserver.models.interface import Interface
 from maasserver.models.node import RackController
 from maasserver.models.subnet import Subnet
 from provisioningserver.dns.actions import (
@@ -348,7 +349,7 @@ def process_dns_update_notify(message):
         case _:
             # special case where we know an IP has been deleted but, we can't fetch the value
             # and the rrecord may still have other answers
-            if op == "DELETE-IP":
+            if op == "DELETE-IP" or op == "DELETE-IFACE-IP":
                 updates.append(
                     DynamicDNSUpdate.create_from_trigger(
                         operation="DELETE",
@@ -366,23 +367,33 @@ def process_dns_update_notify(message):
                             rectype="AAAA",
                         )
                     )
-                resource = DNSResource.objects.get(
-                    name=update_list[2], domain__name=zone
-                )
+                    
+                ttl = None
+                ip_addresses = []
+                if op == "DELETE-IP":
+                    resource = DNSResource.objects.get(
+                        name=update_list[2], domain__name=zone
+                    )
+                    ttl = int(resource.address_ttl) if resource.address_ttl else None
+                    ip_addresses = list(resource.ip_addresses.exclude(ip__isnull=True))
+                else:
+                    iface_id = int(update_list[-1])
+                    iface = Interface.objects.get(id=iface_id)
+                    default_domain = Domain.objects.get_default_domain()
+                    ttl = int(default_domain.ttl) if default_domain.ttl else None
+                    ip_addresses = list(iface.ip_addresses.exclude(ip__isnull=True))
+                print(ip_addresses)
                 updates += [
                     DynamicDNSUpdate.create_from_trigger(
                         operation="INSERT",
                         zone=zone,
                         name=name,
                         rectype=rectype,
-                        ttl=int(resource.address_ttl)
-                        if resource.address_ttl
-                        else None,
+                        ttl=ttl,
                         answer=ip.ip,
                     )
-                    for ip in resource.ip_addresses.all()
+                    for ip in ip_addresses
                 ]
-
             elif len(update_list) > 4:  # has an answer
                 updates.append(
                     DynamicDNSUpdate.create_from_trigger(
diff --git a/src/maasserver/dns/tests/test_config.py b/src/maasserver/dns/tests/test_config.py
index 9a2835d..ded425f 100644
--- a/src/maasserver/dns/tests/test_config.py
+++ b/src/maasserver/dns/tests/test_config.py
@@ -873,7 +873,8 @@ class TestProcessDNSUpdateNotify(MAASServerTestCase):
         domain = factory.make_Domain()
         resource = factory.make_DNSResource(domain=domain)
         ip = resource.ip_addresses.first().ip
-        ip2 = factory.make_StaticIPAddress()
+        subnet = factory.make_Subnet()
+        ip2 = factory.make_StaticIPAddress(subnet=subnet, ip=subnet.get_next_ip_for_allocation()[0])
         resource.ip_addresses.add(ip2)
         message = f"DELETE-IP {domain.name} {resource.name} A {resource.address_ttl if resource.address_ttl else 60} {ip}"
         resource.ip_addresses.first().delete()
@@ -902,3 +903,38 @@ class TestProcessDNSUpdateNotify(MAASServerTestCase):
             ],
             result,
         )
+
+    def test_delete_iface_ip(self):
+        domain = factory.make_Domain()
+        node = factory.make_Node_with_Interface_on_Subnet()
+        iface = node.current_config.interface_set.first()
+        ip1 = iface.ip_addresses.first()
+        ip2 = factory.make_StaticIPAddress(interface=iface)
+        ip1_id = ip1.id
+        ip1.delete()
+        message = f"DELETE-IFACE-IP {domain.name} {node.hostname} A {domain.ttl if domain.ttl else 60} {iface.id}"
+        result, _ = process_dns_update_notify(message)
+        self.assertCountEqual(
+            [
+                DynamicDNSUpdate(
+                    operation="DELETE",
+                    zone=domain.name,
+                    name=f"{node.hostname}.{domain.name}",
+                    rectype="A",
+                ),
+                DynamicDNSUpdate(
+                    operation="DELETE",
+                    zone=domain.name,
+                    name=f"{node.hostname}.{domain.name}",
+                    rectype="AAAA",
+                ),
+                DynamicDNSUpdate(
+                    operation="INSERT",
+                    zone=domain.name,
+                    name=f"{node.hostname}.{domain.name}",
+                    rectype="A" if IPAddress(ip2.ip).version == 4 else "AAAA",
+                    answer=ip2.ip,
+                ),
+            ],
+            result,
+        )
diff --git a/src/maasserver/triggers/system.py b/src/maasserver/triggers/system.py
index a18fe8a..3e755f5 100644
--- a/src/maasserver/triggers/system.py
+++ b/src/maasserver/triggers/system.py
@@ -2054,6 +2054,134 @@ def render_dns_dynamic_update_subnet_procedure(op):
     )
 
 
+def render_dns_dynamic_update_interface_static_ip_address(op):
+    return dedent(
+        f"""\
+        CREATE OR REPLACE FUNCTION sys_dns_updates_interface_ip_{op}()
+        RETURNS trigger as $$
+        DECLARE
+          current_hostname text;
+          default_domain_id bigint;
+          domain text;
+          iface_name text;
+          ip_addr text;
+          address_ttl int;
+          current_node_config_id bigint;
+          current_node_id bigint;
+        BEGIN
+          ASSERT TG_WHEN = 'AFTER', 'May only run as an AFTER trigger';
+          ASSERT TG_LEVEL <> 'STATEMENT', 'Should not be used as a STATEMENT level trigger', TG_NAME;
+          SELECT domain_id INTO default_domain_id FROM maasserver_globaldefault LIMIT 1;
+          SELECT name, COALESCE(ttl, 0) INTO domain, address_ttl FROM maasserver_domain WHERE id=default_domain_id;
+          IF (TG_OP = 'INSERT' AND TG_LEVEL = 'ROW') THEN
+            SELECT name, node_config_id INTO iface_name, current_node_config_id FROM maasserver_interface WHERE id=NEW.interface_id;
+            SELECT node_id INTO current_node_id FROM maasserver_nodeconfig WHERE id=current_node_config_id;
+            SELECT hostname INTO current_hostname FROM maasserver_node WHERE id=current_node_id;
+            SELECT host(ip) INTO ip_addr FROM maasserver_staticipaddress WHERE id=NEW.staticipaddress_id;
+            PERFORM pg_notify('sys_dns_updates', 'INSERT ' || domain || ' ' || current_hostname || ' A ' || address_ttl || ' ' || ip_addr);
+            PERFORM pg_notify('sys_dns_updates', 'INSERT ' || domain || ' ' || iface_name || '.' || current_hostname || ' A ' || address_ttl || ' ' || ip_addr);
+          ELSIF (TG_OP = 'DELETE' AND TG_LEVEL = 'ROW') THEN
+            IF EXISTS(SELECT id FROM maasserver_interface WHERE id=OLD.interface_id) THEN
+                SELECT name, node_config_id INTO iface_name, current_node_config_id FROM maasserver_interface WHERE id=OLD.interface_id;
+                SELECT node_id INTO current_node_id FROM maasserver_nodeconfig WHERE id=current_node_config_id;
+                SELECT hostname INTO current_hostname FROM maasserver_node WHERE id=current_node_id;
+                IF EXISTS(SELECT id FROM maasserver_staticipaddress WHERE id=OLD.staticipaddress_id) THEN
+                  SELECT host(ip) INTO ip_addr FROM maasserver_staticipaddress WHERE id=OLD.staticipaddress_id;
+                  PERFORM pg_notify('sys_dns_updates', 'DELETE ' || domain || ' ' || current_hostname || ' A ' || ip_addr);
+                  PERFORM pg_notify('sys_dns_updates', 'DELETE ' || domain || ' ' || iface_name || '.' || current_hostname || ' A ' || ip_addr);
+                ELSE 
+                  PERFORM pg_notify('sys_dns_updates', 'DELETE-IFACE-IP ' || domain || ' ' || current_hostname || ' A ' || OLD.interface_id);
+                  PERFORM pg_notify('sys_dns_updates', 'DELETE-IFACE-IP ' || domain || ' ' || current_hostname || ' AAAA ' || OLD.interface_id);
+                END IF;
+            END IF;
+          END IF;
+          RETURN NULL;
+        END;
+        $$ LANGUAGE plpgsql;
+        """
+    )
+
+
+dns_dynamic_update_static_ip_address_update = dedent(
+    """\
+    CREATE OR REPLACE FUNCTION sys_dns_updates_ip_update()
+    RETURNS trigger as $$
+    DECLARE
+      current_hostname text;
+      default_domain_id bigint;
+      domain text;
+      iface_name text;
+      address_ttl int;
+      current_node_config_id bigint;
+      current_node_id bigint;
+      current_interface_id bigint;
+    BEGIN
+      IF NEW IS DISTINCT FROM OLD THEN
+        IF EXISTS(SELECT id FROM maasserver_interface_ip_addresses WHERE staticipaddress_id=NEW.id) THEN
+          SELECT domain_id INTO default_domain_id FROM maasserver_globaldefault LIMIT 1;
+          SELECT name, COALESCE(ttl, 0) INTO domain, address_ttl FROM maasserver_domain WHERE id=default_domain_id;
+          SELECT interface_id INTO current_interface_id FROM maasserver_interface_ip_addresses WHERE staticipaddress_id=NEW.id;
+          SELECT node_config_id, name INTO current_node_config_id, iface_name FROM maasserver_interface WHERE id=current_interface_id;
+          SELECT node_id INTO current_node_id FROM maasserver_nodeconfig WHERE id=current_node_config_id;
+          SELECT hostname INTO current_hostname FROM maasserver_node WHERE id=current_node_id;
+          IF OLD.ip IS NOT NULL THEN
+            PERFORM pg_notify('sys_dns_updates', 'DELETE ' || domain || ' ' || current_hostname || ' A ' || host(OLD.ip));
+            PERFORM pg_notify('sys_dns_updates', 'DELETE ' || domain || ' ' || iface_name || '.' || current_hostname || ' A ' || host(OLD.ip));
+          END IF;
+          PERFORM pg_notify('sys_dns_updates', 'INSERT ' || domain || ' ' || current_hostname || ' A ' || address_ttl || ' ' || host(NEW.ip));
+          PERFORM pg_notify('sys_dns_updates', 'INSERT ' || domain || ' ' || iface_name || '.' || current_hostname || ' A ' || address_ttl || ' ' || host(NEW.ip));
+        END IF;
+      END IF;
+      RETURN NULL;
+    END;
+    $$ LANGUAGE plpgsql;
+    """
+)
+
+dns_dynamic_update_node_delete = dedent(
+    """\
+    CREATE OR REPLACE FUNCTION sys_dns_updates_maasserver_node_delete()
+    RETURNS trigger as $$
+    DECLARE
+      hostname text;
+      default_domain_id bigint;
+      domain text;
+      address_ttl int;
+    BEGIN
+      SELECT domain_id INTO default_domain_id FROM maasserver_globaldefault LIMIT 1;
+      SELECT name, COALESCE(ttl, 0) INTO domain, address_ttl FROM maasserver_domain WHERE id=default_domain_id;
+      PERFORM pg_notify('sys_dns_updates', 'DELETE ' || domain || ' ' || OLD.hostname || ' A');
+      RETURN NULL;
+    END;
+    $$ LANGUAGE plpgsql;
+    """
+)
+
+
+dns_dynamic_update_interface_delete = dedent(
+    """\
+    CREATE OR REPLACE FUNCTION sys_dns_updates_maasserver_interface_delete()
+    RETURNS trigger as $$
+    DECLARE
+      current_hostname text;
+      default_domain_id bigint;
+      domain text;
+      current_node_id bigint;
+    BEGIN
+      SELECT domain_id INTO default_domain_id FROM maasserver_globaldefault LIMIT 1;
+      SELECT name INTO domain FROM maasserver_domain WHERE id=default_domain_id;
+      IF EXISTS(SELECT id FROM maasserver_nodeconfig WHERE id=OLD.node_config_id) THEN
+        SELECT node_id INTO current_node_id FROM maasserver_nodeconfig WHERE id=OLD.node_config_id;
+        SELECT hostname INTO current_hostname FROM maasserver_node WHERE id=current_node_id;
+        PERFORM pg_notify('sys_dns_updates', 'DELETE ' || domain || ' ' || OLD.name || '.' || current_hostname || ' A');
+      END IF;
+      RETURN NULL;
+    END;
+    $$ LANGUAGE plpgsql;
+    """
+)
+
+
 def render_sys_proxy_procedure(proc_name, on_delete=False):
     """Render a database procedure with name `proc_name` that notifies that a
     proxy update is needed.
@@ -2403,3 +2531,33 @@ def register_system_triggers():
         "sys_dns_updates_maasserver_subnet_delete",
         "delete",
     )
+    register_procedure(render_dns_dynamic_update_interface_static_ip_address("insert"))
+    register_trigger(
+        "maasserver_interface_ip_addresses",
+        "sys_dns_updates_maasserver_interface_ip_insert",
+        "insert",
+    )
+    register_procedure(render_dns_dynamic_update_interface_static_ip_address("delete"))
+    register_trigger(
+        "maasserver_interface_ip_addresses",
+        "sys_dns_updates_maasserver_interface_ip_delete",
+        "delete",
+    )
+    register_procedure(dns_dynamic_update_static_ip_address_update)
+    register_trigger(
+        "maasserver_staticipaddress",
+        "sys_dns_updates_ip_update",
+        "update",
+    )
+    register_procedure(dns_dynamic_update_node_delete)
+    register_trigger(
+        "maasserver_node",
+        "sys_dns_updates_maasserver_node_delete",
+        "delete",
+    )
+    register_procedure(dns_dynamic_update_interface_delete)
+    register_trigger(
+        "maasserver_interface",
+        "sys_dns_updates_maasserver_interface_delete",
+        "delete",
+    )
diff --git a/src/maasserver/triggers/tests/test_system.py b/src/maasserver/triggers/tests/test_system.py
index d5222b4..f4e9280 100644
--- a/src/maasserver/triggers/tests/test_system.py
+++ b/src/maasserver/triggers/tests/test_system.py
@@ -7,6 +7,8 @@ from contextlib import closing
 from django.db import connection
 from twisted.internet.defer import inlineCallbacks
 
+from maasserver.enum import NODE_STATUS
+from maasserver.models import Domain
 from maasserver.models.dnspublication import zone_serial
 from maasserver.testing.factory import factory
 from maasserver.testing.testcase import (
@@ -367,3 +369,168 @@ class TestSysDNSUpdates(
         finally:
             self.stop_reading()
             yield self.postgres_listener_service.stopService()
+
+    @wait_for_reactor
+    @inlineCallbacks
+    def test_dns_dynamic_update_interface_static_ip_address_insert(self):
+        listener = self.make_listener_without_delay()
+        yield self.set_service(listener)
+        yield deferToDatabase(
+            self.register_trigger,
+            "maasserver_interface_ip_addresses",
+            "sys_dns_updates",
+            ops=("insert",),
+            trigger="sys_dns_updates_interface_ip_insert",
+        )
+        vlan = yield deferToDatabase(self.create_vlan)
+        subnet = yield deferToDatabase(self.create_subnet, params={"vlan": vlan})
+        self.start_reading()
+        try:
+            node = yield deferToDatabase(self.create_node_with_interface, params={"subnet": subnet, "status": NODE_STATUS.DEPLOYED})
+            domain = yield deferToDatabase(Domain.objects.get_default_domain)
+            expected_iface = yield deferToDatabase(lambda : node.current_config.interface_set.first())
+            expected_ip = yield deferToDatabase(
+                lambda : self.create_staticipaddress(params={
+                    "ip": subnet.get_next_ip_for_allocation()[0],
+                    "interface": expected_iface,
+                    "subnet": subnet,
+                })
+            )
+            msg1 = yield self.get_notify("sys_dns_updates")
+            self.assertEqual(msg1, f"INSERT {domain.name} {node.hostname} A 0 {expected_ip.ip}")
+            msg2 = yield self.get_notify("sys_dns_updates")
+            self.assertEqual(msg2, f"INSERT {domain.name} {expected_iface.name}.{node.hostname} A 0 {expected_ip.ip}")
+        finally:
+            self.stop_reading()
+            yield self.postgres_listener_service.stopService()
+
+    @wait_for_reactor
+    @inlineCallbacks
+    def test_dns_dynamic_update_interface_static_ip_address_delete(self):
+        listener = self.make_listener_without_delay()
+        yield self.set_service(listener)
+        yield deferToDatabase(
+            self.register_trigger,
+            "maasserver_interface_ip_addresses",
+            "sys_dns_updates",
+            ops=("delete",),
+            trigger="sys_dns_updates_interface_ip_delete",
+        )
+        vlan = yield deferToDatabase(self.create_vlan)
+        subnet = yield deferToDatabase(self.create_subnet, params={"vlan": vlan})
+        node = yield deferToDatabase(self.create_node_with_interface, params={"subnet": subnet, "status": NODE_STATUS.DEPLOYED})
+        domain = yield deferToDatabase(Domain.objects.get_default_domain)
+        iface = yield deferToDatabase(lambda : node.current_config.interface_set.first())
+        ip1 = yield deferToDatabase(
+            lambda : self.create_staticipaddress(params={
+                "ip": subnet.get_next_ip_for_allocation()[0],
+                "interface": iface,
+                "subnet": subnet,
+            })
+        )
+        ip2 = yield deferToDatabase(
+            lambda : self.create_staticipaddress(params={
+                "ip": subnet.get_next_ip_for_allocation()[0],
+                "interface": iface,
+                "subnet": subnet,
+            })
+        )
+        self.start_reading()
+        try:
+            yield deferToDatabase(iface.unlink_ip_address, ip1)
+            msg1 = yield self.get_notify("sys_dns_updates")
+            self.assertEqual(msg1, f"DELETE {domain.name} {node.hostname} A {ip1.ip}")
+            msg2 = yield self.get_notify("sys_dns_updates")
+            self.assertEqual(msg2, f"DELETE {domain.name} {iface.name}.{node.hostname} A {ip1.ip}")
+        finally:
+            self.stop_reading()
+            yield self.postgres_listener_service.stopService()
+
+    @wait_for_reactor
+    @inlineCallbacks
+    def test_dns_dynamc_update_ip_update(self):
+        listener = self.make_listener_without_delay()
+        yield self.set_service(listener)
+        yield deferToDatabase(
+            self.register_trigger,
+            "maasserver_staticipaddress",
+            "sys_dns_updates",
+            ops=("update",),
+            trigger="sys_dns_updates_ip_update",
+        )
+        vlan = yield deferToDatabase(self.create_vlan)
+        subnet = yield deferToDatabase(self.create_subnet, params={"vlan": vlan})
+        node = yield deferToDatabase(self.create_node_with_interface, params={"subnet": subnet, "status": NODE_STATUS.DEPLOYED})
+        domain = yield deferToDatabase(Domain.objects.get_default_domain)
+        iface = yield deferToDatabase(lambda : node.current_config.interface_set.first())
+        ip = yield deferToDatabase(
+            lambda : self.create_staticipaddress(params={
+                "ip": subnet.get_next_ip_for_allocation()[0],
+                "interface": iface,
+                "subnet": subnet,
+            })
+        )
+        old_ip = ip.ip
+        def _set_new_ip():
+            ip.ip = subnet.get_next_ip_for_allocation()[0]
+            ip.save()
+        self.start_reading()
+        try:
+            yield deferToDatabase(_set_new_ip)
+            msg1 = yield self.get_notify("sys_dns_updates")
+            self.assertEqual(msg1, f"DELETE {domain.name} {node.hostname} A {old_ip}")
+            msg2 = yield self.get_notify("sys_dns_updates")
+            self.assertEqual(msg2, f"DELETE {domain.name} {iface.name}.{node.hostname} A {old_ip}")
+            msg3 = yield self.get_notify("sys_dns_updates")
+            self.assertEqual(msg3, f"INSERT {domain.name} {node.hostname} A 0 {ip.ip}")
+            msg4 = yield self.get_notify("sys_dns_updates")
+            self.assertEqual(msg4, f"INSERT {domain.name} {iface.name}.{node.hostname} A 0 {ip.ip}")
+        finally:
+            self.stop_reading()
+            yield self.postgres_listener_service.stopService()
+
+    @wait_for_reactor
+    @inlineCallbacks
+    def test_dns_dynamic_update_node_delete(self):
+        listener = self.make_listener_without_delay()
+        yield self.set_service(listener)
+        yield deferToDatabase(
+            self.register_trigger,
+            "maasserver_node",
+            "sys_dns_updates",
+            ops=("delete",),
+        )
+        node = yield deferToDatabase(self.create_node)
+        domain = yield deferToDatabase(Domain.objects.get_default_domain)
+        self.start_reading()
+        try:
+            yield deferToDatabase(node.delete)
+            msg = yield self.get_notify("sys_dns_updates")
+            self.assertEqual(msg, f"DELETE {domain.name} {node.hostname} A")
+        finally:
+            self.stop_reading()
+            yield self.postgres_listener_service.stopService()
+
+    @wait_for_reactor
+    @inlineCallbacks
+    def test_dns_dynamic_update_interface_delete(self):
+        listener = self.make_listener_without_delay()
+        yield self.set_service(listener)
+        yield deferToDatabase(
+            self.register_trigger,
+            "maasserver_node",
+            "sys_dns_updates",
+            ops=("delete",),
+        )
+        subnet = yield deferToDatabase(self.create_subnet)
+        node = yield deferToDatabase(self.create_node_with_interface, params={"subnet": subnet})
+        domain = yield deferToDatabase(Domain.objects.get_default_domain)
+        iface = yield deferToDatabase(lambda : node.current_config.interface_set.first())
+        self.start_reading()
+        try:
+            yield deferToDatabase(iface.delete)
+            msg = yield self.get_notify("sys_dns_updates")
+            self.assertEqual(msg, f"DELETE {domain.name} {iface.name}.{node.hostname} A")
+        finally:
+            self.stop_reading()
+            yield self.postgres_listener_service.stopService()
diff --git a/src/provisioningserver/dns/zoneconfig.py b/src/provisioningserver/dns/zoneconfig.py
index 13dca69..905dd15 100644
--- a/src/provisioningserver/dns/zoneconfig.py
+++ b/src/provisioningserver/dns/zoneconfig.py
@@ -170,6 +170,19 @@ class DomainConfigBase:
         else:
             return True
 
+    def journal_file_exists(self, zone_info):
+        try:
+            os.stat(f"{zone_info.target_path}.jnl")
+        except FileNotFoundError:
+            return False
+        else:
+            return True
+
+
+    def _remove_journal_file(self, zone_info):
+        if self.journal_file_exists(zone_info):
+            os.unlink(f"{zone_info.target_path}.jnl")
+
     def dynamic_update(self, zone_info):
         nsupdate = NSUpdateCommand(
             zone_info.zone_name,
@@ -323,6 +336,7 @@ class DNSForwardZoneConfig(DomainConfigBase):
             if not self.force_config_write and self.zone_file_exists(zi):
                 self.dynamic_update(zi)
             else:
+                self._remove_journal_file(zi)
                 self.requires_reload = True
                 self.write_zone_file(
                     zi.target_path,
@@ -612,6 +626,7 @@ class DNSReverseZoneConfig(DomainConfigBase):
             if not self.force_config_write and self.zone_file_exists(zi):
                 self.dynamic_update(zi)
             else:
+                self._remove_journal_file(zi)
                 self.requires_reload = True
                 self.write_zone_file(
                     zi.target_path,

Follow ups