From f05897d61a30f6b42a6444897809a23c24f1c051 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 14 Jul 2025 10:28:30 -0400 Subject: [PATCH] Closes #18811: Match full-form IPv6 addresses in global search (#19873) * Closes #18811: Match full-form IPv6 addresses in global search * Fix typo --- netbox/extras/lookups.py | 16 +++++++++++++++- netbox/ipam/models/ip.py | 15 +++++++++++++++ netbox/netbox/search/backends.py | 8 +++++--- 3 files changed, 35 insertions(+), 4 deletions(-) diff --git a/netbox/extras/lookups.py b/netbox/extras/lookups.py index c496cce78..9e1fe4a0b 100644 --- a/netbox/extras/lookups.py +++ b/netbox/extras/lookups.py @@ -18,9 +18,22 @@ class Empty(Lookup): return f"CAST(LENGTH({sql}) AS BOOLEAN) IS TRUE", params +class NetHost(Lookup): + """ + Similar to ipam.lookups.NetHost, but casts the field to INET. + """ + lookup_name = 'net_host' + + 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 'HOST(CAST(%s AS INET)) = HOST(%s)' % (lhs, rhs), params + + class NetContainsOrEquals(Lookup): """ - This lookup has the same functionality as the one from the ipam app except lhs is cast to inet + Similar to ipam.lookups.NetContainsOrEquals, but casts the field to INET. """ lookup_name = 'net_contains_or_equals' @@ -32,4 +45,5 @@ class NetContainsOrEquals(Lookup): CharField.register_lookup(Empty) +CachedValueField.register_lookup(NetHost) CachedValueField.register_lookup(NetContainsOrEquals) diff --git a/netbox/ipam/models/ip.py b/netbox/ipam/models/ip.py index ab2481d90..db116e9e4 100644 --- a/netbox/ipam/models/ip.py +++ b/netbox/ipam/models/ip.py @@ -162,6 +162,11 @@ class Aggregate(ContactsMixin, GetAvailablePrefixesMixin, PrimaryModel): return self.prefix.version return None + @property + def ipv6_full(self): + if self.prefix and self.prefix.version == 6: + return netaddr.IPAddress(self.prefix).format(netaddr.ipv6_full) + def get_child_prefixes(self): """ Return all Prefixes within this Aggregate @@ -330,6 +335,11 @@ class Prefix(ContactsMixin, GetAvailablePrefixesMixin, CachedScopeMixin, Primary def mask_length(self): return self.prefix.prefixlen if self.prefix else None + @property + def ipv6_full(self): + if self.prefix and self.prefix.version == 6: + return netaddr.IPAddress(self.prefix).format(netaddr.ipv6_full) + @property def depth(self): return self._depth @@ -808,6 +818,11 @@ class IPAddress(ContactsMixin, PrimaryModel): self._original_assigned_object_id = self.__dict__.get('assigned_object_id') self._original_assigned_object_type_id = self.__dict__.get('assigned_object_type_id') + @property + def ipv6_full(self): + if self.address and self.address.version == 6: + return netaddr.IPAddress(self.address).format(netaddr.ipv6_full) + def get_duplicates(self): return IPAddress.objects.filter( vrf=self.vrf, diff --git a/netbox/netbox/search/backends.py b/netbox/netbox/search/backends.py index 12243e9b6..cb08ab4af 100644 --- a/netbox/netbox/search/backends.py +++ b/netbox/netbox/search/backends.py @@ -115,11 +115,13 @@ class CachedValueSearchBackend(SearchBackend): if lookup in (LookupTypes.STARTSWITH, LookupTypes.ENDSWITH): # "Starts/ends with" matches are valid only on string values query_filter &= Q(type=FieldTypes.STRING) - elif lookup == LookupTypes.PARTIAL: + elif lookup in (LookupTypes.PARTIAL, LookupTypes.EXACT): try: - # If the value looks like an IP address, add an extra match for CIDR values + # If the value looks like an IP address, add extra filters for CIDR/INET values address = str(netaddr.IPNetwork(value.strip()).cidr) - query_filter |= Q(type=FieldTypes.CIDR) & Q(value__net_contains_or_equals=address) + query_filter |= Q(type=FieldTypes.INET) & Q(value__net_host=address) + if lookup == LookupTypes.PARTIAL: + query_filter |= Q(type=FieldTypes.CIDR) & Q(value__net_contains_or_equals=address) except (AddrFormatError, ValueError): pass