diff --git a/netbox/ipam/tables.py b/netbox/ipam/tables.py
index 8ac21e04c..4c5bbee3a 100644
--- a/netbox/ipam/tables.py
+++ b/netbox/ipam/tables.py
@@ -39,6 +39,16 @@ PREFIX_LINK_BRIEF = """
"""
+IPADDRESS_LINK = """
+{% if record.pk %}
+ {{ record.address }}
+{% elif perms.ipam.add_ipaddress %}
+ {% if record.0 <= 65536 %}{{ record.0 }}{% else %}Lots of{% endif %} free IP{{ record.0|pluralize }}
+{% else %}
+ {{ record.0 }}
+{% endif %}
+"""
+
STATUS_LABEL = """
{% if record.pk %}
{{ record.get_status_display }}
@@ -148,6 +158,9 @@ class PrefixTable(BaseTable):
class Meta(BaseTable.Meta):
model = Prefix
fields = ('pk', 'prefix', 'status', 'vrf', 'tenant', 'site', 'role', 'description')
+ row_attrs = {
+ 'class': lambda record: 'success' if not record.pk else '',
+ }
class PrefixBriefTable(BaseTable):
@@ -169,7 +182,7 @@ class PrefixBriefTable(BaseTable):
class IPAddressTable(BaseTable):
pk = ToggleColumn()
- address = tables.LinkColumn('ipam:ipaddress', args=[Accessor('pk')], verbose_name='IP Address')
+ address = tables.TemplateColumn(IPADDRESS_LINK, verbose_name='IP Address')
vrf = tables.LinkColumn('ipam:vrf', args=[Accessor('vrf.pk')], default='Global', verbose_name='VRF')
tenant = tables.TemplateColumn(TENANT_LINK, verbose_name='Tenant')
device = tables.LinkColumn('dcim:device', args=[Accessor('interface.device.pk')], orderable=False,
@@ -180,6 +193,9 @@ class IPAddressTable(BaseTable):
class Meta(BaseTable.Meta):
model = IPAddress
fields = ('pk', 'address', 'vrf', 'tenant', 'device', 'interface', 'description')
+ row_attrs = {
+ 'class': lambda record: 'success' if not isinstance(record, IPAddress) else '',
+ }
class IPAddressBriefTable(BaseTable):
diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py
index 95aa33b1e..ce3c2a8f9 100644
--- a/netbox/ipam/views.py
+++ b/netbox/ipam/views.py
@@ -1,4 +1,4 @@
-from netaddr import IPSet
+import netaddr
from django_tables2 import RequestConfig
from django.contrib.auth.mixins import PermissionRequiredMixin
@@ -21,7 +21,7 @@ def add_available_prefixes(parent, prefix_list):
"""
# Find all unallocated space
- available_prefixes = IPSet(parent) ^ IPSet([p.prefix for p in prefix_list])
+ available_prefixes = netaddr.IPSet(parent) ^ netaddr.IPSet([p.prefix for p in prefix_list])
available_prefixes = [Prefix(prefix=p) for p in available_prefixes.iter_cidrs()]
# Concatenate and sort complete list of children
@@ -31,6 +31,57 @@ def add_available_prefixes(parent, prefix_list):
return prefix_list
+def add_available_ipaddresses(prefix, ipaddress_list):
+ """
+ Annotate ranges of available IP addresses within a given prefix.
+ """
+
+ output = []
+ prev_ip = None
+
+ # Ignore the "network address" for IPv4 prefixes larger than /31
+ if prefix.version == 4 and prefix.prefixlen < 31:
+ first_ip_in_prefix = netaddr.IPAddress(prefix.first + 1)
+ else:
+ first_ip_in_prefix = netaddr.IPAddress(prefix.first)
+
+ # Ignore the broadcast address for IPv4 prefixes larger than /31
+ if prefix.version == 4 and prefix.prefixlen < 31:
+ last_ip_in_prefix = netaddr.IPAddress(prefix.last - 1)
+ else:
+ 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))
+
+ # Iterate through existing IPs and annotate free ranges
+ for ip in ipaddress_list:
+ if prev_ip:
+ skipped_count = int(ip.address.ip - prev_ip.address.ip - 1)
+ if skipped_count:
+ first_skipped = '{}/{}'.format(prev_ip.address.ip + 1, prefix.prefixlen)
+ output.append((skipped_count, first_skipped))
+ output.append(ip)
+ prev_ip = 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))
+
+ return output
+
+
#
# VRFs
#
@@ -375,6 +426,7 @@ def prefix_ipaddresses(request, pk):
# Find all IPAddresses belonging to this Prefix
ipaddresses = IPAddress.objects.filter(vrf=prefix.vrf, address__net_contained_or_equal=str(prefix.prefix))\
.select_related('vrf', 'interface__device', 'primary_ip4_for', 'primary_ip6_for')
+ ipaddresses = add_available_ipaddresses(prefix.prefix, ipaddresses)
ip_table = tables.IPAddressTable(ipaddresses)
ip_table.model = IPAddress