Przeglądaj źródła

Update Triggers and add new functions to triggers to handle certain cases

Daniel Sheppard 1 tydzień temu
rodzic
commit
42e2fd7fb3

+ 1 - 1
netbox/ipam/migrations/0087_ipaddress_iprange_prefix_parent.py

@@ -17,7 +17,7 @@ class Migration(migrations.Migration):
             field=models.ForeignKey(
                 blank=True,
                 null=True,
-                on_delete=django.db.models.deletion.SET_NULL,
+                on_delete=django.db.models.deletion.DO_NOTHING,
                 related_name='children',
                 to='ipam.prefix',
             ),

+ 0 - 25
netbox/ipam/migrations/0089_alter_prefix_parent.py

@@ -1,25 +0,0 @@
-# Generated by Django 5.2.5 on 2025-11-25 03:53
-
-import django.db.models.deletion
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('ipam', '0089_prefix_ipam_prefix_delete_prefix_ipam_prefix_insert'),
-    ]
-
-    operations = [
-        migrations.AlterField(
-            model_name='prefix',
-            name='parent',
-            field=models.ForeignKey(
-                blank=True,
-                null=True,
-                on_delete=django.db.models.deletion.DO_NOTHING,
-                related_name='children',
-                to='ipam.prefix',
-            ),
-        ),
-    ]

+ 0 - 43
netbox/ipam/migrations/0089_prefix_ipam_prefix_delete_prefix_ipam_prefix_insert.py

@@ -1,43 +0,0 @@
-# Generated by Django 5.2.5 on 2025-11-06 03:24
-
-import pgtrigger.compiler
-import pgtrigger.migrations
-from django.db import migrations
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('ipam', '0088_ipaddress_iprange_prefix_parent_data'),
-    ]
-
-    operations = [
-        pgtrigger.migrations.AddTrigger(
-            model_name='prefix',
-            trigger=pgtrigger.compiler.Trigger(
-                name='ipam_prefix_delete',
-                sql=pgtrigger.compiler.UpsertTriggerSql(
-                    func="\n-- Update Child Prefix's with Prefix's PARENT\nUPDATE ipam_prefix SET parent_id=OLD.parent_id WHERE parent_id=OLD.id;\nRETURN OLD;\n",  # noqa: E501
-                    hash='899e1943cb201118be7ef02f36f49747224774f2',
-                    operation='DELETE',
-                    pgid='pgtrigger_ipam_prefix_delete_e7810',
-                    table='ipam_prefix',
-                    when='BEFORE',
-                ),
-            ),
-        ),
-        pgtrigger.migrations.AddTrigger(
-            model_name='prefix',
-            trigger=pgtrigger.compiler.Trigger(
-                name='ipam_prefix_insert',
-                sql=pgtrigger.compiler.UpsertTriggerSql(
-                    func="\nUPDATE ipam_prefix\nSET parent_id=NEW.id \nWHERE \n    prefix << NEW.prefix\n    AND\n    (\n        (vrf_id = NEW.vrf_id OR (vrf_id IS NULL AND NEW.vrf_id IS NULL))\n        OR\n        (\n            NEW.vrf_id IS NULL\n            AND\n            NEW.status = 'container'\n            AND\n            NOT EXISTS(\n                SELECT 1 FROM ipam_prefix p WHERE p.prefix >> ipam_prefix.prefix AND p.vrf_id = ipam_prefix.vrf_id\n            )\n        )\n    )\n    AND id != NEW.id\n    AND NOT EXISTS (\n        SELECT 1 FROM ipam_prefix p\n        WHERE\n            p.prefix >> ipam_prefix.prefix\n            AND p.prefix << NEW.prefix\n            AND (\n                (p.vrf_id = ipam_prefix.vrf_id OR (p.vrf_id IS NULL AND ipam_prefix.vrf_id IS NULL))\n                OR\n                (p.vrf_id IS NULL AND p.status = 'container')\n            )\n            AND p.id != NEW.id\n    )\n;\nRETURN NEW;\n",  # noqa: E501
-                    hash='0e05bbe61861227a9eb710b6c94bae9e0cc7119e',
-                    operation='INSERT',
-                    pgid='pgtrigger_ipam_prefix_insert_46c72',
-                    table='ipam_prefix',
-                    when='AFTER',
-                ),
-            ),
-        ),
-    ]

+ 0 - 65
netbox/ipam/migrations/0090_update_trigger.py

@@ -1,65 +0,0 @@
-# Generated by Django 5.2.5 on 2025-11-25 06:00
-
-import pgtrigger.compiler
-import pgtrigger.migrations
-from django.db import migrations
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('ipam', '0089_alter_prefix_parent'),
-    ]
-
-    operations = [
-        pgtrigger.migrations.RemoveTrigger(
-            model_name='prefix',
-            name='ipam_prefix_delete',
-        ),
-        pgtrigger.migrations.RemoveTrigger(
-            model_name='prefix',
-            name='ipam_prefix_insert',
-        ),
-        pgtrigger.migrations.AddTrigger(
-            model_name='prefix',
-            trigger=pgtrigger.compiler.Trigger(
-                name='ipam_prefix_delete',
-                sql=pgtrigger.compiler.UpsertTriggerSql(
-                    func="\n-- Update Child Prefix's with Prefix's PARENT  This is a safe assumption based on the fact that the parent would be the\n-- next direct parent for anything else that could contain this prefix\nUPDATE ipam_prefix SET parent_id=OLD.parent_id WHERE parent_id=OLD.id;\nRETURN OLD;\n",  # noqa: E501
-                    hash='ee3f890009c05a3617428158e7b6f3d77317885d',
-                    operation='DELETE',
-                    pgid='pgtrigger_ipam_prefix_delete_e7810',
-                    table='ipam_prefix',
-                    when='BEFORE',
-                ),
-            ),
-        ),
-        pgtrigger.migrations.AddTrigger(
-            model_name='prefix',
-            trigger=pgtrigger.compiler.Trigger(
-                name='ipam_prefix_insert',
-                sql=pgtrigger.compiler.UpsertTriggerSql(
-                    func="\n-- Update the prefix with the new parent if the parent is the most appropriate prefix\nUPDATE ipam_prefix\nSET parent_id=NEW.id\nWHERE\n    prefix << NEW.prefix\n    AND\n    (\n        (vrf_id = NEW.vrf_id OR (vrf_id IS NULL AND NEW.vrf_id IS NULL))\n        OR\n        (\n            NEW.vrf_id IS NULL\n            AND\n            NEW.status = 'container'\n            AND\n            NOT EXISTS(\n                SELECT 1 FROM ipam_prefix p WHERE p.prefix >> ipam_prefix.prefix AND p.vrf_id = ipam_prefix.vrf_id\n            )\n        )\n    )\n    AND id != NEW.id\n    AND NOT EXISTS (\n        SELECT 1 FROM ipam_prefix p\n        WHERE\n            p.prefix >> ipam_prefix.prefix\n            AND p.prefix << NEW.prefix\n            AND (\n                (p.vrf_id = ipam_prefix.vrf_id OR (p.vrf_id IS NULL AND ipam_prefix.vrf_id IS NULL))\n                OR\n                (p.vrf_id IS NULL AND p.status = 'container')\n            )\n            AND p.id != NEW.id\n    )\n;\nRETURN NEW;\n",  # noqa: E501
-                    hash='1d71498f09e767183d3b0d29c06c9ac9e2cc000a',
-                    operation='INSERT',
-                    pgid='pgtrigger_ipam_prefix_insert_46c72',
-                    table='ipam_prefix',
-                    when='AFTER',
-                ),
-            ),
-        ),
-        pgtrigger.migrations.AddTrigger(
-            model_name='prefix',
-            trigger=pgtrigger.compiler.Trigger(
-                name='ipam_prefix_update',
-                sql=pgtrigger.compiler.UpsertTriggerSql(
-                    func="\n-- When a prefix changes, reassign any IPAddresses that no longer\n-- fall within the new prefix range to the parent prefix (or set null if no parent exists)\nUPDATE ipam_prefix\nSET parent_id = OLD.parent_id\nWHERE\n    parent_id = NEW.id\n    -- IP address no longer contained within the updated prefix\n    AND NOT (prefix << NEW.prefix);\n\n-- Update the prefix with the new parent if the parent is the most appropriate prefix\nUPDATE ipam_prefix\nSET parent_id=NEW.id\nWHERE\n    prefix << NEW.prefix\n    AND\n    (\n        (vrf_id = NEW.vrf_id OR (vrf_id IS NULL AND NEW.vrf_id IS NULL))\n        OR\n        (\n            NEW.vrf_id IS NULL\n            AND\n            NEW.status = 'container'\n            AND\n            NOT EXISTS(\n                SELECT 1 FROM ipam_prefix p WHERE p.prefix >> ipam_prefix.prefix AND p.vrf_id = ipam_prefix.vrf_id\n            )\n        )\n    )\n    AND id != NEW.id\n    AND NOT EXISTS (\n        SELECT 1 FROM ipam_prefix p\n        WHERE\n            p.prefix >> ipam_prefix.prefix\n            AND p.prefix << NEW.prefix\n            AND (\n                (p.vrf_id = ipam_prefix.vrf_id OR (p.vrf_id IS NULL AND ipam_prefix.vrf_id IS NULL))\n                OR\n                (p.vrf_id IS NULL AND p.status = 'container')\n            )\n            AND p.id != NEW.id\n    )\n;\nRETURN NEW;\n",  # noqa: E501
-                    hash='747230a84703df5a4aa3d32e7f45b5a32525b799',
-                    operation='UPDATE',
-                    pgid='pgtrigger_ipam_prefix_update_e5fca',
-                    table='ipam_prefix',
-                    when='AFTER',
-                ),
-            ),
-        ),
-    ]

+ 57 - 9
netbox/ipam/tests/test_models.py

@@ -262,15 +262,6 @@ class TestPrefix(TestCase):
         # Global container should return all children
         self.assertSetEqual(child_ip_pks, {ips[0].pk, ips[1].pk, ips[2].pk, ips[3].pk})
 
-        parent_prefix.vrf = vrfs[0]
-        parent_prefix.save()
-
-        parent_prefix.refresh_from_db()
-        child_ip_pks = {p.pk for p in parent_prefix.ip_addresses.all()}
-
-        # VRF container is limited to its own VRF
-        self.assertSetEqual(child_ip_pks, {ips[1].pk})
-
     def test_get_available_prefixes(self):
 
         prefixes = Prefix.objects.bulk_create((
@@ -417,6 +408,63 @@ class TestPrefix(TestCase):
         duplicate_prefix = Prefix(vrf=vrf, prefix=IPNetwork('192.0.2.0/24'))
         self.assertRaises(ValidationError, duplicate_prefix.clean)
 
+    def test_parent_container_prefix_change(self):
+        vrfs = VRF.objects.bulk_create((
+            VRF(name='VRF 1'),
+            VRF(name='VRF 2'),
+            VRF(name='VRF 3'),
+        ))
+        parent_prefix = Prefix.objects.create(
+            prefix=IPNetwork('10.0.0.0/16'), status=PrefixStatusChoices.STATUS_CONTAINER
+        )
+        ips = IPAddress.objects.bulk_create((
+            IPAddress(prefix=parent_prefix, address=IPNetwork('10.0.0.1/24'), vrf=None),
+            IPAddress(prefix=parent_prefix, address=IPNetwork('10.0.1.1/24'), vrf=vrfs[0]),
+            IPAddress(prefix=parent_prefix, address=IPNetwork('10.0.2.1/24'), vrf=vrfs[1]),
+            IPAddress(prefix=parent_prefix, address=IPNetwork('10.0.3.1/24'), vrf=vrfs[2]),
+        ))
+        child_ip_pks = {p.pk for p in parent_prefix.ip_addresses.all()}
+
+        # Global container should return all children
+        self.assertSetEqual(child_ip_pks, {ips[0].pk, ips[1].pk, ips[2].pk, ips[3].pk})
+
+        parent_prefix.vrf = vrfs[0]
+        parent_prefix.save()
+
+        parent_prefix.refresh_from_db()
+        child_ip_pks = {p.pk for p in parent_prefix.ip_addresses.all()}
+
+        # VRF container is limited to its own VRF
+        self.assertSetEqual(child_ip_pks, {ips[1].pk})
+
+    def test_parent_container_vrf_change(self):
+        vrfs = VRF.objects.bulk_create((
+            VRF(name='VRF 1'),
+            VRF(name='VRF 2'),
+            VRF(name='VRF 3'),
+        ))
+        parent_prefix = Prefix.objects.create(
+            prefix=IPNetwork('10.0.0.0/16'), status=PrefixStatusChoices.STATUS_CONTAINER
+        )
+        ips = IPAddress.objects.bulk_create((
+            IPAddress(prefix=parent_prefix, address=IPNetwork('10.0.0.1/24'), vrf=None),
+            IPAddress(prefix=parent_prefix, address=IPNetwork('10.0.1.1/24'), vrf=vrfs[0]),
+            IPAddress(prefix=parent_prefix, address=IPNetwork('10.0.2.1/24'), vrf=vrfs[1]),
+            IPAddress(prefix=parent_prefix, address=IPNetwork('10.0.3.1/24'), vrf=vrfs[2]),
+        ))
+        child_ip_pks = {p.pk for p in parent_prefix.ip_addresses.all()}
+
+        # Global container should return all children
+        self.assertSetEqual(child_ip_pks, {ips[0].pk, ips[1].pk, ips[2].pk, ips[3].pk})
+
+        parent_prefix.prefix = '10.0.0.0/25'
+        parent_prefix.save()
+
+        parent_prefix.refresh_from_db()
+        child_ip_pks = {p.pk for p in parent_prefix.ip_addresses.all()}
+
+        self.assertSetEqual(child_ip_pks, {ips[0].pk, ips[1].pk})
+
 
 class TestPrefixHierarchy(TestCase):
     """

+ 156 - 27
netbox/ipam/triggers.py

@@ -45,7 +45,7 @@ RETURN NEW;
 
 
 ipam_prefix_update_adjust_prefix_parent = """
--- When a prefix changes, reassign any IPAddresses that no longer
+-- When a prefix changes, reassign any child prefixes that no longer
 -- fall within the new prefix range to the parent prefix (or set null if no parent exists)
 UPDATE ipam_prefix
 SET parent_id = OLD.parent_id
@@ -54,38 +54,167 @@ WHERE
     -- IP address no longer contained within the updated prefix
     AND NOT (prefix << NEW.prefix);
 
--- Update the prefix with the new parent if the parent is the most appropriate prefix
-UPDATE ipam_prefix
-SET parent_id=NEW.id
+-- When a prefix changes, reassign any ip addresses that no longer
+-- fall within the new prefix range to the parent prefix (or set null if no parent exists)
+UPDATE ipam_ipaddress
+SET prefix_id = OLD.parent_id
 WHERE
-    prefix << NEW.prefix
+    prefix_id = NEW.id
+    -- IP address no longer contained within the updated prefix
     AND
-    (
-        (vrf_id = NEW.vrf_id OR (vrf_id IS NULL AND NEW.vrf_id IS NULL))
-        OR
+    NOT (address << NEW.prefix)
+;
+
+-- When a prefix changes, reassign any ip ranges that no longer
+-- fall within the new prefix range to the parent prefix (or set null if no parent exists)
+UPDATE ipam_iprange
+SET prefix_id = OLD.parent_id
+WHERE
+    prefix_id = NEW.id
+    -- IP address no longer contained within the updated prefix
+    AND
+    NOT (start_address << NEW.prefix)
+    AND
+    NOT (end_address << NEW.prefix)
+;
+
+-- When a prefix changes, reassign any ip addresses that are in-scope but
+-- no longer within the same VRF
+UPDATE ipam_ipaddress
+    SET prefix_id = OLD.parent_id
+    WHERE
+        prefix_id = NEW.id
+        AND
+        address << OLD.prefix
+        AND
         (
-            NEW.vrf_id IS NULL
-            AND
-            NEW.status = 'container'
-            AND
-            NOT EXISTS(
-                SELECT 1 FROM ipam_prefix p WHERE p.prefix >> ipam_prefix.prefix AND p.vrf_id = ipam_prefix.vrf_id
+            NOT address << NEW.prefix
+            OR
+            (
+                vrf_id is NULL
+                AND
+                NEW.vrf_id IS NOT NULL
+            )
+            OR
+            (
+                OLD.vrf_id IS NULL
+                AND
+                NEW.vrf_id IS NOT NULL
+                AND
+                NEW.vrf_id != vrf_id
             )
         )
-    )
-    AND id != NEW.id
-    AND NOT EXISTS (
-        SELECT 1 FROM ipam_prefix p
-        WHERE
-            p.prefix >> ipam_prefix.prefix
-            AND p.prefix << NEW.prefix
-            AND (
-                (p.vrf_id = ipam_prefix.vrf_id OR (p.vrf_id IS NULL AND ipam_prefix.vrf_id IS NULL))
-                OR
-                (p.vrf_id IS NULL AND p.status = 'container')
+;
+
+-- When a prefix changes, reassign any ip ranges that are in-scope but
+-- no longer within the same VRF
+UPDATE ipam_iprange
+    SET prefix_id = OLD.parent_id
+    WHERE
+        prefix_id = NEW.id
+        AND
+        start_address << OLD.prefix
+        AND
+        end_address << OLD.prefix
+        AND
+        (
+            NOT start_address << NEW.prefix
+            OR
+            NOT end_address << NEW.prefix
+            OR
+            (
+                vrf_id is NULL
+                AND
+                NEW.vrf_id IS NOT NULL
             )
-            AND p.id != NEW.id
-    )
+            OR
+            (
+                OLD.vrf_id IS NULL
+                AND
+                NEW.vrf_id IS NOT NULL
+                AND
+                NEW.vrf_id != vrf_id
+            )
+        )
+;
+
+-- Update the prefix with the new parent if the parent is the most appropriate prefix
+UPDATE ipam_prefix
+    SET parent_id=NEW.id
+    WHERE
+        prefix << NEW.prefix
+        AND
+        (
+            (vrf_id = NEW.vrf_id OR (vrf_id IS NULL AND NEW.vrf_id IS NULL))
+            OR
+            (
+                NEW.vrf_id IS NULL
+                AND
+                NEW.status = 'container'
+                AND
+                NOT EXISTS(
+                    SELECT 1 FROM ipam_prefix p WHERE p.prefix >> prefix AND p.vrf_id = vrf_id
+                )
+            )
+        )
+        AND id != NEW.id
+        AND NOT EXISTS (
+            SELECT 1 FROM ipam_prefix p
+            WHERE
+                p.prefix >> ipam_prefix.prefix
+                AND p.prefix << NEW.prefix
+                AND (
+                    (p.vrf_id = ipam_prefix.vrf_id OR (p.vrf_id IS NULL AND ipam_prefix.vrf_id IS NULL))
+                    OR
+                    (p.vrf_id IS NULL AND p.status = 'container')
+                )
+                AND p.id != NEW.id
+        )
+;
+UPDATE ipam_ipaddress
+    SET prefix_id = NEW.id
+    WHERE
+        prefix_id != NEW.id
+        AND
+        address << NEW.prefix
+        AND (
+            (vrf_id = NEW.vrf_id OR (vrf_id IS NULL AND NEW.vrf_id IS NULL))
+            OR (
+                NEW.vrf_id IS NULL
+                AND
+                NEW.status = 'container'
+                AND
+                NOT EXISTS(
+                    SELECT 1 FROM ipam_prefix p WHERE p.prefix >> address AND p.vrf_id = vrf_id
+                )
+            )
+        )
+;
+UPDATE ipam_iprange
+    SET prefix_id = NEW.id
+    WHERE
+        prefix_id != NEW.id
+        AND
+        start_address << NEW.prefix
+        AND
+        end_address << NEW.prefix
+        AND (
+            (vrf_id = NEW.vrf_id OR (vrf_id IS NULL AND NEW.vrf_id IS NULL))
+            OR (
+                NEW.vrf_id IS NULL
+                AND
+                NEW.status = 'container'
+                AND
+                NOT EXISTS(
+                    SELECT 1 FROM ipam_prefix p WHERE
+                        p.prefix >> start_address
+                        AND
+                        p.prefix >> end_address
+                        AND
+                        p.vrf_id = vrf_id
+                )
+            )
+        )
 ;
 RETURN NEW;
 """