From 0f42f6317c2873c1334a2c576cbcb7d9533cf967 Mon Sep 17 00:00:00 2001 From: BelpHegoR17 Date: Tue, 9 Dec 2025 20:52:56 +0530 Subject: [PATCH] [base] Add hierarchical subnet IP duplicate validation #116 Updates AbstractIpAddress.clean() to check for duplicate IP addresses not only within the same subnet but also across any subnet that contains the IP (parent, or child). Added a corresponding test to ensure a ValidationError is raised when an IP exists in any related subnet. Fixes #116 --- openwisp_ipam/base/models.py | 31 ++++++++++++++++++- .../tests/test_hierarchy_validation.py | 15 +++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 openwisp_ipam/tests/test_hierarchy_validation.py diff --git a/openwisp_ipam/base/models.py b/openwisp_ipam/base/models.py index c3a8be6..a7f6364 100644 --- a/openwisp_ipam/base/models.py +++ b/openwisp_ipam/base/models.py @@ -294,7 +294,9 @@ def __str__(self): def clean(self): if not self.ip_address or not self.subnet_id: return - if ip_address(self.ip_address) not in self.subnet.subnet: + + subnet_obj = ip_network(self.subnet.subnet, strict=False) + if ip_address(self.ip_address) not in subnet_obj: raise ValidationError( {"ip_address": _("IP address does not belong to the subnet")} ) @@ -307,3 +309,30 @@ def clean(self): for ip in addresses: if ip_address(self.ip_address) == ip_address(ip["ip_address"]): raise ValidationError({"ip_address": _("IP address already used.")}) + + related_subnet_ids = [] + for subnet in load_model("openwisp_ipam", "Subnet").objects.all(): + try: + net = ip_network(subnet.subnet, strict=False) + except ValueError: + continue + if ip_address(self.ip_address) in net: + related_subnet_ids.append(subnet.id) + + duplicate = ( + load_model("openwisp_ipam", "IpAddress") + .objects.filter( + ip_address=self.ip_address, subnet_id__in=related_subnet_ids + ) + .exclude(pk=self.pk) + .exists() + ) + + if duplicate: + raise ValidationError( + { + "ip_address": _( + "IP address already exists in a related subnet (parent, child, or overlapping)." + ) + } + ) diff --git a/openwisp_ipam/tests/test_hierarchy_validation.py b/openwisp_ipam/tests/test_hierarchy_validation.py new file mode 100644 index 0000000..63f6ca6 --- /dev/null +++ b/openwisp_ipam/tests/test_hierarchy_validation.py @@ -0,0 +1,15 @@ +from django.test import TestCase +from django.core.exceptions import ValidationError +from openwisp_ipam.models import Subnet, IpAddress + +class HierarchyTest(TestCase): + def test_hierarchy_duplicate(self): + parent = Subnet.objects.create(name='parent',subnet ="10.0.0.0/16") + child = Subnet.objects.create(name='child',subnet="10.0.1.0/24") + + IpAddress.objects.create(ip_address="10.0.1.10", subnet=child) + + with self.assertRaises(ValidationError): + obj=IpAddress(ip_address="10.0.1.10", subnet=parent) + obj.full_clean() + obj.save() \ No newline at end of file