From ad3df60fdd381a2cd5d6369ba4be40869a8551d7 Mon Sep 17 00:00:00 2001 From: Brian Tiemann Date: Thu, 5 Sep 2024 15:56:30 -0400 Subject: [PATCH 1/6] Add new INET lookups for net_host_lt/gt/lte/gte comparisons irrespective of subnet inclusion --- netbox/ipam/fields.py | 4 ++++ netbox/ipam/lookups.py | 40 ++++++++++++++++++++++++++++++++ netbox/ipam/models/ip.py | 14 +++++------ netbox/ipam/tests/test_models.py | 29 +++++++++++++++++++++++ 4 files changed, 80 insertions(+), 7 deletions(-) diff --git a/netbox/ipam/fields.py b/netbox/ipam/fields.py index 20341005d98..a8682e6004c 100644 --- a/netbox/ipam/fields.py +++ b/netbox/ipam/fields.py @@ -105,6 +105,10 @@ def db_type(self, connection): IPAddressField.register_lookup(lookups.NetHostContained) IPAddressField.register_lookup(lookups.NetFamily) IPAddressField.register_lookup(lookups.NetMaskLength) +IPAddressField.register_lookup(lookups.NetHostLessThan) +IPAddressField.register_lookup(lookups.NetHostLessThanOrEqual) +IPAddressField.register_lookup(lookups.NetHostGreaterThan) +IPAddressField.register_lookup(lookups.NetHostGreaterThanOrEqual) class ASNField(models.BigIntegerField): diff --git a/netbox/ipam/lookups.py b/netbox/ipam/lookups.py index c6abb5a26a3..95fd1ce0504 100644 --- a/netbox/ipam/lookups.py +++ b/netbox/ipam/lookups.py @@ -154,6 +154,46 @@ def as_sql(self, qn, connection): return 'CAST(HOST(%s) AS INET) <<= %s' % (lhs, rhs), params +class NetHostGreaterThan(Lookup): + lookup_name = 'net_host_gt' + + def as_sql(self, qn, connection): + lhs, lhs_params = self.process_lhs(qn, connection) + rhs, rhs_params = self.process_rhs(qn, connection) + params = lhs_params + rhs_params + return 'CAST(HOST(%s) AS INET) > INET %s' % (lhs, rhs), params + + +class NetHostLessThan(Lookup): + lookup_name = 'net_host_lt' + + def as_sql(self, qn, connection): + lhs, lhs_params = self.process_lhs(qn, connection) + rhs, rhs_params = self.process_rhs(qn, connection) + params = lhs_params + rhs_params + return 'CAST(HOST(%s) AS INET) < INET %s' % (lhs, rhs), params + + +class NetHostGreaterThanOrEqual(Lookup): + lookup_name = 'net_host_gte' + + def as_sql(self, qn, connection): + lhs, lhs_params = self.process_lhs(qn, connection) + rhs, rhs_params = self.process_rhs(qn, connection) + params = lhs_params + rhs_params + return 'CAST(HOST(%s) AS INET) >= INET %s' % (lhs, rhs), params + + +class NetHostLessThanOrEqual(Lookup): + lookup_name = 'net_host_lte' + + def as_sql(self, qn, connection): + lhs, lhs_params = self.process_lhs(qn, connection) + rhs, rhs_params = self.process_rhs(qn, connection) + params = lhs_params + rhs_params + return 'CAST(HOST(%s) AS INET) <= INET %s' % (lhs, rhs), params + + class NetFamily(Transform): lookup_name = 'family' function = 'FAMILY' diff --git a/netbox/ipam/models/ip.py b/netbox/ipam/models/ip.py index 0b8e3a8dfc8..09d0ba1e4e9 100644 --- a/netbox/ipam/models/ip.py +++ b/netbox/ipam/models/ip.py @@ -580,15 +580,15 @@ def clean(self): }) # Check for overlapping ranges - overlapping_range = IPRange.objects.exclude(pk=self.pk).filter(vrf=self.vrf).filter( - Q(start_address__gte=self.start_address, start_address__lte=self.end_address) | # Starts inside - Q(end_address__gte=self.start_address, end_address__lte=self.end_address) | # Ends inside - Q(start_address__lte=self.start_address, end_address__gte=self.end_address) # Starts & ends outside - ).first() - if overlapping_range: + overlapping_ranges = IPRange.objects.exclude(pk=self.pk).filter(vrf=self.vrf).filter( + Q(start_address__net_host_gte=self.start_address.ip, start_address__net_host_lte=self.end_address.ip) | # Starts inside + Q(end_address__net_host_gte=self.start_address.ip, end_address__net_host_lte=self.end_address.ip) | # Ends inside + Q(start_address__net_host_lte=self.start_address.ip, end_address__net_host_gte=self.end_address.ip) # Starts & ends outside + ) + if overlapping_ranges.exists(): raise ValidationError( _("Defined addresses overlap with range {overlapping_range} in VRF {vrf}").format( - overlapping_range=overlapping_range, + overlapping_range=overlapping_ranges.first(), vrf=self.vrf )) diff --git a/netbox/ipam/tests/test_models.py b/netbox/ipam/tests/test_models.py index 39eb33a4fcb..5c2c7c79a64 100644 --- a/netbox/ipam/tests/test_models.py +++ b/netbox/ipam/tests/test_models.py @@ -36,6 +36,35 @@ def test_get_utilization(self): self.assertEqual(aggregate.get_utilization(), 100) +class TestIPRange(TestCase): + + def test_overlapping_range(self): + iprange_192_168 = IPRange.objects.create(start_address=IPNetwork('192.168.0.1/22'), end_address=IPNetwork('192.168.0.49/22')) + iprange_192_168.clean() + iprange_3_1_99 = IPRange.objects.create(start_address=IPNetwork('1.2.3.1/24'), end_address=IPNetwork('1.2.3.99/24')) + iprange_3_1_99.clean() + iprange_3_100_199 = IPRange.objects.create(start_address=IPNetwork('1.2.3.100/24'), end_address=IPNetwork('1.2.3.199/24')) + iprange_3_100_199.clean() + iprange_3_200_255 = IPRange.objects.create(start_address=IPNetwork('1.2.3.200/24'), end_address=IPNetwork('1.2.3.255/24')) + iprange_3_200_255.clean() + iprange_4_1_99 = IPRange.objects.create(start_address=IPNetwork('1.2.4.1/24'), end_address=IPNetwork('1.2.4.99/24')) + iprange_4_1_99.clean() + iprange_4_200 = IPRange.objects.create(start_address=IPNetwork('1.2.4.200/24'), end_address=IPNetwork('1.2.4.255/24')) + iprange_4_200.clean() + # Overlapping range entirely within existing + with self.assertRaises(ValidationError): + iprange_3_123_124 = IPRange.objects.create(start_address=IPNetwork('1.2.3.123/26'), end_address=IPNetwork('1.2.3.124/26')) + iprange_3_123_124.clean() + # Overlapping range starting within existing + with self.assertRaises(ValidationError): + iprange_4_98_101 = IPRange.objects.create(start_address=IPNetwork('1.2.4.98/24'), end_address=IPNetwork('1.2.4.101/24')) + iprange_4_98_101.clean() + # Overlapping range ending within existing + with self.assertRaises(ValidationError): + iprange_4_198_201 = IPRange.objects.create(start_address=IPNetwork('1.2.4.198/24'), end_address=IPNetwork('1.2.4.201/24')) + iprange_4_198_201.clean() + + class TestPrefix(TestCase): def test_get_duplicates(self): From fcfbb1e67f1d0974b729845817fc5fbc39aa3d50 Mon Sep 17 00:00:00 2001 From: Brian Tiemann Date: Mon, 9 Sep 2024 14:15:05 -0400 Subject: [PATCH 2/6] Refactor Lookup subclasses to be more DRY --- netbox/ipam/lookups.py | 47 +++++++++++++++++++++++------------------- 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/netbox/ipam/lookups.py b/netbox/ipam/lookups.py index 95fd1ce0504..314d048ad88 100644 --- a/netbox/ipam/lookups.py +++ b/netbox/ipam/lookups.py @@ -154,44 +154,49 @@ def as_sql(self, qn, connection): return 'CAST(HOST(%s) AS INET) <<= %s' % (lhs, rhs), params -class NetHostGreaterThan(Lookup): - lookup_name = 'net_host_gt' +class NetHostComparison(Lookup): + + @property + def comparison_sql(self): + raise NotImplementedError def as_sql(self, qn, connection): lhs, lhs_params = self.process_lhs(qn, connection) rhs, rhs_params = self.process_rhs(qn, connection) params = lhs_params + rhs_params - return 'CAST(HOST(%s) AS INET) > INET %s' % (lhs, rhs), params + return self.comparison_sql % (lhs, rhs), params + + +class NetHostGreaterThan(NetHostComparison): + lookup_name = 'net_host_gt' + + @property + def comparison_sql(self): + return 'CAST(HOST(%s) AS INET) > INET %s' -class NetHostLessThan(Lookup): +class NetHostLessThan(NetHostComparison): lookup_name = 'net_host_lt' - def as_sql(self, qn, connection): - lhs, lhs_params = self.process_lhs(qn, connection) - rhs, rhs_params = self.process_rhs(qn, connection) - params = lhs_params + rhs_params - return 'CAST(HOST(%s) AS INET) < INET %s' % (lhs, rhs), params + @property + def comparison_sql(self): + return 'CAST(HOST(%s) AS INET) < INET %s' -class NetHostGreaterThanOrEqual(Lookup): +class NetHostGreaterThanOrEqual(NetHostComparison): lookup_name = 'net_host_gte' - def as_sql(self, qn, connection): - lhs, lhs_params = self.process_lhs(qn, connection) - rhs, rhs_params = self.process_rhs(qn, connection) - params = lhs_params + rhs_params - return 'CAST(HOST(%s) AS INET) >= INET %s' % (lhs, rhs), params + @property + def comparison_sql(self): + return 'CAST(HOST(%s) AS INET) >= INET %s' -class NetHostLessThanOrEqual(Lookup): +class NetHostLessThanOrEqual(NetHostComparison): lookup_name = 'net_host_lte' - def as_sql(self, qn, connection): - lhs, lhs_params = self.process_lhs(qn, connection) - rhs, rhs_params = self.process_rhs(qn, connection) - params = lhs_params + rhs_params - return 'CAST(HOST(%s) AS INET) <= INET %s' % (lhs, rhs), params + @property + def comparison_sql(self): + return 'CAST(HOST(%s) AS INET) <= INET %s' class NetFamily(Transform): From d4a910e06a5d9524a1fe7fef1d5f1b27933e96da Mon Sep 17 00:00:00 2001 From: Brian Tiemann Date: Mon, 9 Sep 2024 14:19:32 -0400 Subject: [PATCH 3/6] Move comparison_sql to class attribute --- netbox/ipam/lookups.py | 25 +++++-------------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/netbox/ipam/lookups.py b/netbox/ipam/lookups.py index 314d048ad88..38cbbc8cf88 100644 --- a/netbox/ipam/lookups.py +++ b/netbox/ipam/lookups.py @@ -155,10 +155,7 @@ def as_sql(self, qn, connection): class NetHostComparison(Lookup): - - @property - def comparison_sql(self): - raise NotImplementedError + comparison_sql = None def as_sql(self, qn, connection): lhs, lhs_params = self.process_lhs(qn, connection) @@ -169,34 +166,22 @@ def as_sql(self, qn, connection): class NetHostGreaterThan(NetHostComparison): lookup_name = 'net_host_gt' - - @property - def comparison_sql(self): - return 'CAST(HOST(%s) AS INET) > INET %s' + comparison_sql = 'CAST(HOST(%s) AS INET) > INET %s' class NetHostLessThan(NetHostComparison): lookup_name = 'net_host_lt' - - @property - def comparison_sql(self): - return 'CAST(HOST(%s) AS INET) < INET %s' + comparison_sql = 'CAST(HOST(%s) AS INET) < INET %s' class NetHostGreaterThanOrEqual(NetHostComparison): lookup_name = 'net_host_gte' - - @property - def comparison_sql(self): - return 'CAST(HOST(%s) AS INET) >= INET %s' + comparison_sql = 'CAST(HOST(%s) AS INET) >= INET %s' class NetHostLessThanOrEqual(NetHostComparison): lookup_name = 'net_host_lte' - - @property - def comparison_sql(self): - return 'CAST(HOST(%s) AS INET) <= INET %s' + comparison_sql = 'CAST(HOST(%s) AS INET) <= INET %s' class NetFamily(Transform): From 789ca6b560b18777ad47898539fcbee7c4cbac26 Mon Sep 17 00:00:00 2001 From: Brian Tiemann Date: Wed, 11 Sep 2024 15:47:37 -0400 Subject: [PATCH 4/6] Add HostAsInet(Transform) to perform cast --- netbox/ipam/fields.py | 1 + netbox/ipam/lookups.py | 5 +++++ netbox/ipam/models/ip.py | 6 +++--- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/netbox/ipam/fields.py b/netbox/ipam/fields.py index a8682e6004c..99dff081a78 100644 --- a/netbox/ipam/fields.py +++ b/netbox/ipam/fields.py @@ -109,6 +109,7 @@ def db_type(self, connection): IPAddressField.register_lookup(lookups.NetHostLessThanOrEqual) IPAddressField.register_lookup(lookups.NetHostGreaterThan) IPAddressField.register_lookup(lookups.NetHostGreaterThanOrEqual) +IPAddressField.register_lookup(lookups.HostAsInet) class ASNField(models.BigIntegerField): diff --git a/netbox/ipam/lookups.py b/netbox/ipam/lookups.py index 38cbbc8cf88..4bb8e9b8db2 100644 --- a/netbox/ipam/lookups.py +++ b/netbox/ipam/lookups.py @@ -207,6 +207,11 @@ class Host(Transform): lookup_name = 'host' +class HostAsInet(Transform): + lookup_name = 'host_as_inet' + template = 'CAST(HOST( %(expressions)s ) AS INET)' + + class Inet(Transform): function = 'INET' lookup_name = 'inet' diff --git a/netbox/ipam/models/ip.py b/netbox/ipam/models/ip.py index 09d0ba1e4e9..d6b563f72d8 100644 --- a/netbox/ipam/models/ip.py +++ b/netbox/ipam/models/ip.py @@ -581,9 +581,9 @@ def clean(self): # Check for overlapping ranges overlapping_ranges = IPRange.objects.exclude(pk=self.pk).filter(vrf=self.vrf).filter( - Q(start_address__net_host_gte=self.start_address.ip, start_address__net_host_lte=self.end_address.ip) | # Starts inside - Q(end_address__net_host_gte=self.start_address.ip, end_address__net_host_lte=self.end_address.ip) | # Ends inside - Q(start_address__net_host_lte=self.start_address.ip, end_address__net_host_gte=self.end_address.ip) # Starts & ends outside + Q(start_address__host_as_inet__gte=self.start_address.ip, start_address__host_as_inet__lte=self.end_address.ip) | # Starts inside + Q(end_address__host_as_inet__gte=self.start_address.ip, end_address__host_as_inet__lte=self.end_address.ip) | # Ends inside + Q(start_address__host_as_inet__lte=self.start_address.ip, end_address__host_as_inet__gte=self.end_address.ip) # Starts & ends outside ) if overlapping_ranges.exists(): raise ValidationError( From 1a1156ebf1058485b8108f8e745054b2c1eabf3d Mon Sep 17 00:00:00 2001 From: Brian Tiemann Date: Wed, 18 Sep 2024 21:29:12 -0400 Subject: [PATCH 5/6] Remove unnecessary Lookup comparison classes --- netbox/ipam/fields.py | 4 ---- netbox/ipam/lookups.py | 30 ------------------------------ 2 files changed, 34 deletions(-) diff --git a/netbox/ipam/fields.py b/netbox/ipam/fields.py index 99dff081a78..26fbc8fdca2 100644 --- a/netbox/ipam/fields.py +++ b/netbox/ipam/fields.py @@ -105,10 +105,6 @@ def db_type(self, connection): IPAddressField.register_lookup(lookups.NetHostContained) IPAddressField.register_lookup(lookups.NetFamily) IPAddressField.register_lookup(lookups.NetMaskLength) -IPAddressField.register_lookup(lookups.NetHostLessThan) -IPAddressField.register_lookup(lookups.NetHostLessThanOrEqual) -IPAddressField.register_lookup(lookups.NetHostGreaterThan) -IPAddressField.register_lookup(lookups.NetHostGreaterThanOrEqual) IPAddressField.register_lookup(lookups.HostAsInet) diff --git a/netbox/ipam/lookups.py b/netbox/ipam/lookups.py index 4bb8e9b8db2..1a5f7e0eb6e 100644 --- a/netbox/ipam/lookups.py +++ b/netbox/ipam/lookups.py @@ -154,36 +154,6 @@ def as_sql(self, qn, connection): return 'CAST(HOST(%s) AS INET) <<= %s' % (lhs, rhs), params -class NetHostComparison(Lookup): - comparison_sql = None - - def as_sql(self, qn, connection): - lhs, lhs_params = self.process_lhs(qn, connection) - rhs, rhs_params = self.process_rhs(qn, connection) - params = lhs_params + rhs_params - return self.comparison_sql % (lhs, rhs), params - - -class NetHostGreaterThan(NetHostComparison): - lookup_name = 'net_host_gt' - comparison_sql = 'CAST(HOST(%s) AS INET) > INET %s' - - -class NetHostLessThan(NetHostComparison): - lookup_name = 'net_host_lt' - comparison_sql = 'CAST(HOST(%s) AS INET) < INET %s' - - -class NetHostGreaterThanOrEqual(NetHostComparison): - lookup_name = 'net_host_gte' - comparison_sql = 'CAST(HOST(%s) AS INET) >= INET %s' - - -class NetHostLessThanOrEqual(NetHostComparison): - lookup_name = 'net_host_lte' - comparison_sql = 'CAST(HOST(%s) AS INET) <= INET %s' - - class NetFamily(Transform): lookup_name = 'family' function = 'FAMILY' From 66319f9cf0ba47d503efe2498a59069342674616 Mon Sep 17 00:00:00 2001 From: Brian Tiemann Date: Wed, 23 Oct 2024 10:35:01 -0400 Subject: [PATCH 6/6] Chain Host and Inet instead of making a new transform --- netbox/ipam/fields.py | 3 ++- netbox/ipam/lookups.py | 5 ----- netbox/ipam/models/ip.py | 6 +++--- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/netbox/ipam/fields.py b/netbox/ipam/fields.py index 26fbc8fdca2..a829763269d 100644 --- a/netbox/ipam/fields.py +++ b/netbox/ipam/fields.py @@ -105,7 +105,8 @@ def db_type(self, connection): IPAddressField.register_lookup(lookups.NetHostContained) IPAddressField.register_lookup(lookups.NetFamily) IPAddressField.register_lookup(lookups.NetMaskLength) -IPAddressField.register_lookup(lookups.HostAsInet) +IPAddressField.register_lookup(lookups.Host) +IPAddressField.register_lookup(lookups.Inet) class ASNField(models.BigIntegerField): diff --git a/netbox/ipam/lookups.py b/netbox/ipam/lookups.py index 1a5f7e0eb6e..c6abb5a26a3 100644 --- a/netbox/ipam/lookups.py +++ b/netbox/ipam/lookups.py @@ -177,11 +177,6 @@ class Host(Transform): lookup_name = 'host' -class HostAsInet(Transform): - lookup_name = 'host_as_inet' - template = 'CAST(HOST( %(expressions)s ) AS INET)' - - class Inet(Transform): function = 'INET' lookup_name = 'inet' diff --git a/netbox/ipam/models/ip.py b/netbox/ipam/models/ip.py index d6b563f72d8..868b924500a 100644 --- a/netbox/ipam/models/ip.py +++ b/netbox/ipam/models/ip.py @@ -581,9 +581,9 @@ def clean(self): # Check for overlapping ranges overlapping_ranges = IPRange.objects.exclude(pk=self.pk).filter(vrf=self.vrf).filter( - Q(start_address__host_as_inet__gte=self.start_address.ip, start_address__host_as_inet__lte=self.end_address.ip) | # Starts inside - Q(end_address__host_as_inet__gte=self.start_address.ip, end_address__host_as_inet__lte=self.end_address.ip) | # Ends inside - Q(start_address__host_as_inet__lte=self.start_address.ip, end_address__host_as_inet__gte=self.end_address.ip) # Starts & ends outside + Q(start_address__host__inet__gte=self.start_address.ip, start_address__host__inet__lte=self.end_address.ip) | # Starts inside + Q(end_address__host__inet__gte=self.start_address.ip, end_address__host__inet__lte=self.end_address.ip) | # Ends inside + Q(start_address__host__inet__lte=self.start_address.ip, end_address__host__inet__gte=self.end_address.ip) # Starts & ends outside ) if overlapping_ranges.exists(): raise ValidationError(