From ecb56dda14c07fbdaaa61e7c2569094d379adbc5 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 2 Apr 2025 11:30:41 -0400 Subject: [PATCH] Replace add_available_ipaddresses() with annotate_ip_space() --- netbox/ipam/models/ip.py | 5 +- netbox/ipam/tables/template_code.py | 8 ++- netbox/ipam/utils.py | 102 ++++++++++++++++++---------- netbox/ipam/views.py | 4 +- 4 files changed, 77 insertions(+), 42 deletions(-) diff --git a/netbox/ipam/models/ip.py b/netbox/ipam/models/ip.py index 7ec4310d4..b38f04ecc 100644 --- a/netbox/ipam/models/ip.py +++ b/netbox/ipam/models/ip.py @@ -383,14 +383,15 @@ class Prefix(ContactsMixin, GetAvailablePrefixesMixin, CachedScopeMixin, Primary else: return Prefix.objects.filter(prefix__net_contained=str(self.prefix), vrf=self.vrf) - def get_child_ranges(self): + def get_child_ranges(self, **kwargs): """ Return all IPRanges within this Prefix and VRF. """ return IPRange.objects.filter( vrf=self.vrf, start_address__net_host_contained=str(self.prefix), - end_address__net_host_contained=str(self.prefix) + end_address__net_host_contained=str(self.prefix), + **kwargs ) def get_child_ips(self): diff --git a/netbox/ipam/tables/template_code.py b/netbox/ipam/tables/template_code.py index fb969345e..0182ba7de 100644 --- a/netbox/ipam/tables/template_code.py +++ b/netbox/ipam/tables/template_code.py @@ -26,12 +26,14 @@ PREFIX_LINK_WITH_DEPTH = """ """ + PREFIX_LINK IPADDRESS_LINK = """ -{% if record.pk %} +{% if record.address %} {{ record.address }} +{% elif record.start_address %} + {{ record }} {% elif perms.ipam.add_ipaddress %} - {% if record.0 <= 65536 %}{{ record.0 }}{% else %}Many{% endif %} IP{{ record.0|pluralize }} available + {% if record.size <= 65536 %}{{ record.size }}{% else %}Many{% endif %} IP{{ record.size|pluralize }} available {% else %} - {% if record.0 <= 65536 %}{{ record.0 }}{% else %}Many{% endif %} IP{{ record.0|pluralize }} available + {% if record.size <= 65536 %}{{ record.size }}{% else %}Many{% endif %} IP{{ record.size|pluralize }} available {% endif %} """ diff --git a/netbox/ipam/utils.py b/netbox/ipam/utils.py index 3297abd8f..dff12bda5 100644 --- a/netbox/ipam/utils.py +++ b/netbox/ipam/utils.py @@ -1,17 +1,25 @@ +from dataclasses import dataclass import netaddr from .constants import * from .models import Prefix, VLAN __all__ = ( - 'add_available_ipaddresses', + 'AvailableIPSpace', 'add_available_vlans', 'add_requested_prefixes', + 'annotate_ip_space', 'get_next_available_prefix', 'rebuild_prefixes', ) +@dataclass +class AvailableIPSpace: + size: int + first_ip: str + + def add_requested_prefixes(parent, prefix_list, show_available=True, show_assigned=True): """ Return a list of requested prefixes using show_available, show_assigned filters. If available prefixes are @@ -42,50 +50,74 @@ def add_requested_prefixes(parent, prefix_list, show_available=True, show_assign return child_prefixes -def add_available_ipaddresses(prefix, ipaddress_list, is_pool=False): - """ - Annotate ranges of available IP addresses within a given prefix. If is_pool is True, the first and last IP will be - considered usable (regardless of mask length). - """ +def annotate_ip_space(prefix): + # Compile child objects + records = [] + records.extend([ + (iprange.start_address.ip, iprange) for iprange in prefix.get_child_ranges(mark_populated=True) + ]) + records.extend([ + (ip.address.ip, ip) for ip in prefix.get_child_ips() + ]) + records = sorted(records, key=lambda x: x[0]) + + # Determine the first & last valid IP addresses in the prefix + if prefix.family == 4 and prefix.mask_length < 31 and not prefix.is_pool: + # Ignore the network and broadcast addresses for non-pool IPv4 prefixes larger than /31 + first_ip_in_prefix = netaddr.IPAddress(prefix.prefix.first + 1) + last_ip_in_prefix = netaddr.IPAddress(prefix.prefix.last - 1) + else: + first_ip_in_prefix = netaddr.IPAddress(prefix.prefix.first) + last_ip_in_prefix = netaddr.IPAddress(prefix.prefix.last) + + if not records: + return [ + AvailableIPSpace( + size=int(last_ip_in_prefix - first_ip_in_prefix + 1), + first_ip=f'{first_ip_in_prefix}/{prefix.mask_length}' + ) + ] output = [] prev_ip = None - # Ignore the network and broadcast addresses for non-pool IPv4 prefixes larger than /31. - if prefix.version == 4 and prefix.prefixlen < 31 and not is_pool: - first_ip_in_prefix = netaddr.IPAddress(prefix.first + 1) - last_ip_in_prefix = netaddr.IPAddress(prefix.last - 1) - else: - first_ip_in_prefix = netaddr.IPAddress(prefix.first) - last_ip_in_prefix = netaddr.IPAddress(prefix.last) - - if not ipaddress_list: - return [( - int(last_ip_in_prefix - first_ip_in_prefix + 1), - '{}/{}'.format(first_ip_in_prefix, prefix.prefixlen) - )] - # Account for any available IPs before the first real IP - if ipaddress_list[0].address.ip > first_ip_in_prefix: - skipped_count = int(ipaddress_list[0].address.ip - first_ip_in_prefix) - first_skipped = '{}/{}'.format(first_ip_in_prefix, prefix.prefixlen) - output.append((skipped_count, first_skipped)) + if records[0][0] > first_ip_in_prefix: + output.append(AvailableIPSpace( + size=int(records[0][0] - first_ip_in_prefix), + first_ip=f'{first_ip_in_prefix}/{prefix.mask_length}' + )) # Iterate through existing IPs and annotate free ranges - for ip in ipaddress_list: + for record in records: if prev_ip: - diff = int(ip.address.ip - prev_ip.address.ip) - if diff > 1: - first_skipped = '{}/{}'.format(prev_ip.address.ip + 1, prefix.prefixlen) - output.append((diff - 1, first_skipped)) - output.append(ip) - prev_ip = ip + # We've already listed a range which covers this IP + if record[0] < prev_ip: + continue + + # Annotate available space + if (diff := int(record[0] - prev_ip)) > 1: + first_skipped = f'{prev_ip + 1}/{prefix.mask_length}' + output.append(AvailableIPSpace( + size=diff - 1, + first_ip=first_skipped + )) + + output.append(record[1]) + + # Update the previous IP address + if hasattr(record[1], 'end_address'): + prev_ip = record[1].end_address.ip + else: + prev_ip = record[0] + print(f'prev_ip: {prev_ip}') # Include any remaining available IPs - if prev_ip.address.ip < last_ip_in_prefix: - skipped_count = int(last_ip_in_prefix - prev_ip.address.ip) - first_skipped = '{}/{}'.format(prev_ip.address.ip + 1, prefix.prefixlen) - output.append((skipped_count, first_skipped)) + if prev_ip < last_ip_in_prefix: + output.append(AvailableIPSpace( + size=int(last_ip_in_prefix - prev_ip), + first_ip=f'{prev_ip + 1}/{prefix.mask_length}' + )) return output diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index 3dde80b30..31c15ed69 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -21,7 +21,7 @@ from . import filtersets, forms, tables from .choices import PrefixStatusChoices from .constants import * from .models import * -from .utils import add_requested_prefixes, add_available_ipaddresses, add_available_vlans +from .utils import add_requested_prefixes, add_available_vlans, annotate_ip_space # @@ -635,7 +635,7 @@ class PrefixIPAddressesView(generic.ObjectChildrenView): def prep_table_data(self, request, queryset, parent): if not request.GET.get('q') and not get_table_ordering(request, self.table): - return add_available_ipaddresses(parent.prefix, queryset, parent.is_pool) + return annotate_ip_space(parent) return queryset def get_extra_context(self, request, instance):