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):