From 57373c9d6f3f9de5eb4ce9419cd3896e18f07b8c Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 2 Aug 2016 17:20:12 -0400 Subject: [PATCH 1/5] Initial work on #289 --- netbox/ipam/tables.py | 12 +++++++++++- netbox/ipam/views.py | 38 +++++++++++++++++++++++++++++++++++++- 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/netbox/ipam/tables.py b/netbox/ipam/tables.py index 8ac21e04c..800c0fb90 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 %} + {{ record.0 }} free IP{{ record.0|pluralize }} +{% else %} + {{ record.0 }} +{% endif %} +""" + STATUS_LABEL = """ {% if record.pk %} {{ record.get_status_display }} @@ -169,7 +179,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, diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index 95aa33b1e..86fc53c74 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -1,4 +1,4 @@ -from netaddr import IPSet +from netaddr import IPNetwork, IPSet from django_tables2 import RequestConfig from django.contrib.auth.mixins import PermissionRequiredMixin @@ -31,6 +31,41 @@ def add_available_prefixes(parent, prefix_list): return prefix_list +def add_available_ipaddresses(prefix, ipaddress_list): + """ + Create fake IPAddress objects for all unallocated space within a prefix. + """ + + # Find all unallocated space + available_ips = IPSet(prefix) - IPSet([str(ip.address.ip) for ip in ipaddress_list]) + available_ips = [IPAddress(address=IPNetwork('{}/{}'.format(ip, prefix.prefixlen))) for ip in available_ips] + + # Concatenate and sort complete list of children + ipaddress_list = list(ipaddress_list) + available_ips + ipaddress_list.sort(key=lambda ip: ip.address) + if not ipaddress_list: + return [] + + # Summarize free IPs in the list + computed_list = [] + count = 0 + prev_ip = ipaddress_list[0] + for ip in ipaddress_list: + if ip.pk: + if count: + computed_list.append((count, prev_ip)) + count = 0 + computed_list.append(ip) + continue + if not count: + prev_ip = ip + count += 1 + if count: + computed_list.append((count, prev_ip)) + + return computed_list + + # # VRFs # @@ -375,6 +410,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 From 81d955ab7de3cccd000fbcbc5bcd60d78134ced2 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 3 Aug 2016 12:00:35 -0400 Subject: [PATCH 2/5] Rewrote add_available_ipaddresses() to be much more efficient and IPv6-friendly --- netbox/ipam/tables.py | 8 +++++- netbox/ipam/views.py | 65 ++++++++++++++++++++++++++----------------- 2 files changed, 46 insertions(+), 27 deletions(-) diff --git a/netbox/ipam/tables.py b/netbox/ipam/tables.py index 800c0fb90..4c5bbee3a 100644 --- a/netbox/ipam/tables.py +++ b/netbox/ipam/tables.py @@ -43,7 +43,7 @@ IPADDRESS_LINK = """ {% if record.pk %} {{ record.address }} {% elif perms.ipam.add_ipaddress %} - {{ record.0 }} free IP{{ record.0|pluralize }} + {% if record.0 <= 65536 %}{{ record.0 }}{% else %}Lots of{% endif %} free IP{{ record.0|pluralize }} {% else %} {{ record.0 }} {% endif %} @@ -158,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): @@ -190,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 86fc53c74..68cab7429 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -1,4 +1,4 @@ -from netaddr import IPNetwork, 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 @@ -33,37 +33,50 @@ def add_available_prefixes(parent, prefix_list): def add_available_ipaddresses(prefix, ipaddress_list): """ - Create fake IPAddress objects for all unallocated space within a prefix. + Annotate ranges of available IP addresses within a given prefix. """ - # Find all unallocated space - available_ips = IPSet(prefix) - IPSet([str(ip.address.ip) for ip in ipaddress_list]) - available_ips = [IPAddress(address=IPNetwork('{}/{}'.format(ip, prefix.prefixlen))) for ip in available_ips] + output = [] + prev_ip = None + + # Determine first and last usable IP + if prefix.version == 6 or (prefix.version == 4 and prefix.prefixlen == 31): + first_ip_in_prefix = netaddr.IPAddress(prefix.first) + else: + first_ip_in_prefix = netaddr.IPAddress(prefix.first + 1) + if prefix.version == 4 and prefix.prefixlen == 31: + last_ip_in_prefix = netaddr.IPAddress(prefix.last) + else: + last_ip_in_prefix = netaddr.IPAddress(prefix.last - 1) - # Concatenate and sort complete list of children - ipaddress_list = list(ipaddress_list) + available_ips - ipaddress_list.sort(key=lambda ip: ip.address) if not ipaddress_list: - return [] + return [( + int(last_ip_in_prefix - first_ip_in_prefix + 1), + '{}/{}'.format(first_ip_in_prefix, prefix.prefixlen) + )] - # Summarize free IPs in the list - computed_list = [] - count = 0 - prev_ip = ipaddress_list[0] + # 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 ip.pk: - if count: - computed_list.append((count, prev_ip)) - count = 0 - computed_list.append(ip) - continue - if not count: - prev_ip = ip - count += 1 - if count: - computed_list.append((count, prev_ip)) + if prev_ip: + skipped_count = int(ip.address.ip - prev_ip.address.ip) + first_skipped = '{}/{}'.format(prev_ip.address.ip + 1, prefix.prefixlen) + output.append((skipped_count, first_skipped)) + output.append(ip) + prev_ip = ip - return computed_list + # 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 # From 533b4082d804fab68223464627baa880b6c82204 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 3 Aug 2016 12:06:17 -0400 Subject: [PATCH 3/5] Fixed calculation of last_ip_in_prefix for IPv6 --- netbox/ipam/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index 68cab7429..a9e84876e 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -44,7 +44,7 @@ def add_available_ipaddresses(prefix, ipaddress_list): first_ip_in_prefix = netaddr.IPAddress(prefix.first) else: first_ip_in_prefix = netaddr.IPAddress(prefix.first + 1) - if prefix.version == 4 and prefix.prefixlen == 31: + if prefix.version == 6 or (prefix.version == 4 and prefix.prefixlen == 31): last_ip_in_prefix = netaddr.IPAddress(prefix.last) else: last_ip_in_prefix = netaddr.IPAddress(prefix.last - 1) From 79b1bbb9e1f88328b240baf7c893339e3858cb46 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 3 Aug 2016 12:20:24 -0400 Subject: [PATCH 4/5] Fixed calculation of available IPs between two existing IPs --- netbox/ipam/views.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index a9e84876e..efed93989 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -64,9 +64,10 @@ def add_available_ipaddresses(prefix, ipaddress_list): # 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) - first_skipped = '{}/{}'.format(prev_ip.address.ip + 1, prefix.prefixlen) - output.append((skipped_count, first_skipped)) + 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 From 3b9ac3b9863478cc0fa06ae147255008baf078e6 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 3 Aug 2016 12:30:29 -0400 Subject: [PATCH 5/5] More intelligent handling of first/last IPs --- netbox/ipam/views.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index efed93989..ce3c2a8f9 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -39,15 +39,17 @@ def add_available_ipaddresses(prefix, ipaddress_list): output = [] prev_ip = None - # Determine first and last usable IP - if prefix.version == 6 or (prefix.version == 4 and prefix.prefixlen == 31): - first_ip_in_prefix = netaddr.IPAddress(prefix.first) - else: + # 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) - if prefix.version == 6 or (prefix.version == 4 and prefix.prefixlen == 31): - last_ip_in_prefix = netaddr.IPAddress(prefix.last) 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 [( @@ -56,7 +58,7 @@ def add_available_ipaddresses(prefix, ipaddress_list): )] # Account for any available IPs before the first real IP - if ipaddress_list[0].address.ip != first_ip_in_prefix: + 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)) @@ -72,7 +74,7 @@ def add_available_ipaddresses(prefix, ipaddress_list): prev_ip = ip # Include any remaining available IPs - if prev_ip.address.ip != last_ip_in_prefix: + 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))