From 57373c9d6f3f9de5eb4ce9419cd3896e18f07b8c Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 2 Aug 2016 17:20:12 -0400 Subject: [PATCH 01/25] 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 02/25] 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 03/25] 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 04/25] 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 05/25] 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)) From 72690bfd0a762f1be78cd7b7e98bd79516c763af Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 3 Aug 2016 14:24:09 -0400 Subject: [PATCH 06/25] Potential fix for #419: Ditch annotation in favor of discrete queries to gather Tenant stats --- netbox/templates/tenancy/tenant.html | 16 ++++++++-------- netbox/tenancy/views.py | 25 +++++++++++++++---------- 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/netbox/templates/tenancy/tenant.html b/netbox/templates/tenancy/tenant.html index 0d3d8fdaa..0ca380639 100644 --- a/netbox/templates/tenancy/tenant.html +++ b/netbox/templates/tenancy/tenant.html @@ -93,35 +93,35 @@ diff --git a/netbox/tenancy/views.py b/netbox/tenancy/views.py index 6b655ffd6..ec220b9be 100644 --- a/netbox/tenancy/views.py +++ b/netbox/tenancy/views.py @@ -2,6 +2,9 @@ from django.contrib.auth.mixins import PermissionRequiredMixin from django.db.models import Count from django.shortcuts import get_object_or_404, render +from circuits.models import Circuit +from dcim.models import Site, Rack, Device +from ipam.models import IPAddress, Prefix, VLAN, VRF from utilities.views import ( BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView, ) @@ -50,19 +53,21 @@ class TenantListView(ObjectListView): def tenant(request, slug): - tenant = get_object_or_404(Tenant.objects.annotate( - site_count=Count('sites', distinct=True), - rack_count=Count('racks', distinct=True), - device_count=Count('devices', distinct=True), - vrf_count=Count('vrfs', distinct=True), - prefix_count=Count('prefixes', distinct=True), - ipaddress_count=Count('ip_addresses', distinct=True), - vlan_count=Count('vlans', distinct=True), - circuit_count=Count('circuits', distinct=True), - ), slug=slug) + tenant = get_object_or_404(Tenant, slug=slug) + stats = { + 'site_count': Site.objects.filter(tenant=tenant).count(), + 'rack_count': Rack.objects.filter(tenant=tenant).count(), + 'device_count': Device.objects.filter(tenant=tenant).count(), + 'vrf_count': VRF.objects.filter(tenant=tenant).count(), + 'prefix_count': Prefix.objects.filter(tenant=tenant).count(), + 'ipaddress_count': IPAddress.objects.filter(tenant=tenant).count(), + 'vlan_count': VLAN.objects.filter(tenant=tenant).count(), + 'circuit_count': Circuit.objects.filter(tenant=tenant).count(), + } return render(request, 'tenancy/tenant.html', { 'tenant': tenant, + 'stats': stats, }) From 7944ee64193f37886f52a79f9f13bf3ef7733cfd Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 3 Aug 2016 17:33:15 -0400 Subject: [PATCH 07/25] Fixes #422: Added ability to encapsulate within double quotes values which contain commas --- netbox/utilities/forms.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/netbox/utilities/forms.py b/netbox/utilities/forms.py index 3b4e93f10..836fe633f 100644 --- a/netbox/utilities/forms.py +++ b/netbox/utilities/forms.py @@ -1,3 +1,4 @@ +import csv import re from django import forms @@ -118,7 +119,8 @@ class Livesearch(forms.TextInput): class CSVDataField(forms.CharField): """ - A field for comma-separated values (CSV) + A field for comma-separated values (CSV). Values containing commas should be encased within double quotes. Example: + '"New York, NY",new-york-ny,Other stuff' => ['New York, NY', 'new-york-ny', 'Other stuff'] """ csv_form = None @@ -136,16 +138,16 @@ class CSVDataField(forms.CharField): def to_python(self, value): # Return a list of dictionaries, each representing an individual record records = [] - for i, row in enumerate(value.split('\n'), start=1): - if row.strip(): - values = row.strip().split(',') - if len(values) < len(self.columns): + reader = csv.reader(value.splitlines()) + for i, row in enumerate(reader, start=1): + if row: + if len(row) < len(self.columns): raise forms.ValidationError("Line {}: Field(s) missing (found {}; expected {})" - .format(i, len(values), len(self.columns))) - elif len(values) > len(self.columns): + .format(i, len(row), len(self.columns))) + elif len(row) > len(self.columns): raise forms.ValidationError("Line {}: Too many fields (found {}; expected {})" - .format(i, len(values), len(self.columns))) - record = dict(zip(self.columns, values)) + .format(i, len(row), len(self.columns))) + record = dict(zip(self.columns, row)) records.append(record) return records From 4ab40c4489a03568f5a8650ddb1489f70e14da48 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 3 Aug 2016 17:39:57 -0400 Subject: [PATCH 08/25] Release v1.4.1 --- netbox/netbox/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index e2a8ea3ef..4c5abcd56 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -12,7 +12,7 @@ except ImportError: "the documentation.") -VERSION = '1.4.1-dev' +VERSION = '1.4.1' # Import local configuration for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']: From 4ee63f4ff8cd052c1f0e776ed697804239dae235 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 3 Aug 2016 17:49:08 -0400 Subject: [PATCH 09/25] Post-release version bump --- netbox/netbox/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 4c5abcd56..e2a8ea3ef 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -12,7 +12,7 @@ except ImportError: "the documentation.") -VERSION = '1.4.1' +VERSION = '1.4.1-dev' # Import local configuration for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']: From ea2e734ba8b5cce8da5cc57dff6b8b62738aaa0d Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 3 Aug 2016 17:49:28 -0400 Subject: [PATCH 10/25] Post-release version bump --- netbox/netbox/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index e2a8ea3ef..364cf0c09 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -12,7 +12,7 @@ except ImportError: "the documentation.") -VERSION = '1.4.1-dev' +VERSION = '1.4.2-dev' # Import local configuration for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']: From e413012cbbabe9aea701237e4e86a051caa6b0fe Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 4 Aug 2016 11:48:30 -0400 Subject: [PATCH 11/25] Fixes #427: Prevent error when duplicate IPs are present in a prefix's IP list --- netbox/ipam/views.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index ce3c2a8f9..3ff0ff76e 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -66,10 +66,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 - 1) - if skipped_count: + diff = int(ip.address.ip - prev_ip.address.ip) + if diff > 1: first_skipped = '{}/{}'.format(prev_ip.address.ip + 1, prefix.prefixlen) - output.append((skipped_count, first_skipped)) + output.append((diff - 1, first_skipped)) output.append(ip) prev_ip = ip From 6184eb6664ebd3355f50e54e138115e0427003af Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 4 Aug 2016 11:52:55 -0400 Subject: [PATCH 12/25] Fixes #425: Ignore leading and trailing periods when generating a slug --- netbox/project-static/js/forms.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/netbox/project-static/js/forms.js b/netbox/project-static/js/forms.js index fd510af3f..f8e3a2b20 100644 --- a/netbox/project-static/js/forms.js +++ b/netbox/project-static/js/forms.js @@ -13,11 +13,11 @@ $(document).ready(function() { // Slugify function slugify(s, num_chars) { - s = s.replace(/[^\-\.\w\s]/g, ''); // Remove unneeded chars - s = s.replace(/^\s+|\s+$/g, ''); // Trim leading/trailing spaces - s = s.replace(/[\-\.\s]+/g, '-'); // Convert spaces and decimals to hyphens - s = s.toLowerCase(); // Convert to lowercase - return s.substring(0, num_chars); // Trim to first num_chars chars + s = s.replace(/[^\-\.\w\s]/g, ''); // Remove unneeded chars + s = s.replace(/^[\s\.]+|[\s\.]+$/g, ''); // Trim leading/trailing spaces + s = s.replace(/[\-\.\s]+/g, '-'); // Convert spaces and decimals to hyphens + s = s.toLowerCase(); // Convert to lowercase + return s.substring(0, num_chars); // Trim to first num_chars chars } var slug_field = $('#id_slug'); slug_field.change(function() { From 76b9a1c3af02d114462a4ed3cd950d24ea0a586a Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 4 Aug 2016 13:38:45 -0400 Subject: [PATCH 13/25] #167: Added new interface form factors --- netbox/dcim/models.py | 49 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 41 insertions(+), 8 deletions(-) diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index 39d075a94..3202c81f8 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -57,20 +57,53 @@ DEVICE_ROLE_COLOR_CHOICES = [ IFACE_FF_VIRTUAL = 0 IFACE_FF_100M_COPPER = 800 IFACE_FF_1GE_COPPER = 1000 +IFACE_FF_GBIC = 1050 IFACE_FF_SFP = 1100 IFACE_FF_10GE_COPPER = 1150 IFACE_FF_SFP_PLUS = 1200 IFACE_FF_XFP = 1300 IFACE_FF_QSFP_PLUS = 1400 +IFACE_FF_CFP = 1500 +IFACE_FF_QSFP28 = 1600 +IFACE_FF_T1 = 4000 +IFACE_FF_E1 = 4010 +IFACE_FF_T3 = 4040 +IFACE_FF_E3 = 4050 +IFACE_FF_STACKWISE = 5000 +IFACE_FF_STACKWISE_PLUS = 5050 IFACE_FF_CHOICES = [ - [IFACE_FF_VIRTUAL, 'Virtual'], - [IFACE_FF_100M_COPPER, '10/100M (100BASE-TX)'], - [IFACE_FF_1GE_COPPER, '1GE (1000BASE-T)'], - [IFACE_FF_SFP, '1GE (SFP)'], - [IFACE_FF_10GE_COPPER, '10GE (10GBASE-T)'], - [IFACE_FF_SFP_PLUS, '10GE (SFP+)'], - [IFACE_FF_XFP, '10GE (XFP)'], - [IFACE_FF_QSFP_PLUS, '40GE (QSFP+)'], + ['Virtual interfaces', [ + [IFACE_FF_VIRTUAL, 'Virtual'], + ] + ], + ['Ethernet', [ + [IFACE_FF_100M_COPPER, '100BASE-TX (10/100M)'], + [IFACE_FF_1GE_COPPER, '1000BASE-T (1GE)'], + [IFACE_FF_10GE_COPPER, '10GBASE-T (10GE)'], + ] + ], + ['Modular', [ + [IFACE_FF_GBIC, 'GBIC (1GE)'], + [IFACE_FF_SFP, 'SFP (1GE)'], + [IFACE_FF_XFP, 'XFP (10GE)'], + [IFACE_FF_SFP_PLUS, 'SFP+ (10GE)'], + [IFACE_FF_QSFP_PLUS, 'QSFP+ (40GE)'], + [IFACE_FF_CFP, 'CFP (100GE)'], + [IFACE_FF_QSFP28, 'QSFP28 (100GE)'], + ] + ], + ['Serial', [ + [IFACE_FF_T1, 'T1 (1.544 Mbps)'], + [IFACE_FF_E1, 'E1 (2.048 Mbps)'], + [IFACE_FF_T3, 'T3 (45 Mbps)'], + [IFACE_FF_E3, 'E3 (34 Mbps)'], + ] + ], + ['Stacking', [ + [IFACE_FF_STACKWISE, 'Cisco StackWise'], + [IFACE_FF_STACKWISE_PLUS, 'Cisco StackWise Plus'], + ] + ], ] STATUS_ACTIVE = True From 29c4394e64543ebb4fab8e9f82830f7ece24a36b Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 4 Aug 2016 14:37:38 -0400 Subject: [PATCH 14/25] Fixes #429: Correct redirection of user when adding a secret to a device --- netbox/secrets/views.py | 2 +- netbox/templates/secrets/secret_edit.html | 14 ++------------ 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/netbox/secrets/views.py b/netbox/secrets/views.py index 6e50d83fd..351871675 100644 --- a/netbox/secrets/views.py +++ b/netbox/secrets/views.py @@ -92,7 +92,7 @@ def secret_add(request, pk): messages.success(request, "Added new secret: {0}".format(secret)) if '_addanother' in request.POST: - return redirect('secrets:secret_add') + return redirect('dcim:device_addsecret', pk=device.pk) else: return redirect('secrets:secret', pk=secret.pk) diff --git a/netbox/templates/secrets/secret_edit.html b/netbox/templates/secrets/secret_edit.html index 0698e7184..9a3df1a45 100644 --- a/netbox/templates/secrets/secret_edit.html +++ b/netbox/templates/secrets/secret_edit.html @@ -2,19 +2,15 @@ {% load static from staticfiles %} {% load form_helpers %} -{% block title %}{% if secret.pk %}Editing Secret: {{ secret }}{% else %}Add a Secret{% endif %}{% endblock %} +{% block title %}{% if secret.pk %}Editing {{ secret }}{% else %}Add a Secret{% endif %}{% endblock %} {% block content %} -{% if secret.pk %} -

Editing Secret: {{ secret }}

-{% else %} -

Add a Secret

-{% endif %}
{% csrf_token %} {{ form.private_key }}
+

{% if secret.pk %}Editing {{ secret }}{% else %}Add a Secret{% endif %}

{% if form.non_field_errors %}
Errors
@@ -23,10 +19,6 @@
{% endif %} -
- -
-
Secret Attributes
@@ -41,8 +33,6 @@ {% render_field form.userkeys %}
-
-
Secret Data
From c3c3b80cd9c6a9bc8813481c5da74a10a63a82ac Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 4 Aug 2016 14:42:34 -0400 Subject: [PATCH 15/25] Fixed toggling of secret lock/unlock buttons --- netbox/project-static/js/secrets.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/netbox/project-static/js/secrets.js b/netbox/project-static/js/secrets.js index 181984b71..7f67cdcda 100644 --- a/netbox/project-static/js/secrets.js +++ b/netbox/project-static/js/secrets.js @@ -83,8 +83,8 @@ $(document).ready(function() { }, success: function (response, status) { $('#secret_' + secret_id).html(response.plaintext); - $('button.unlock-secret').hide(); - $('button.lock-secret').show(); + $('button.unlock-secret[secret-id=' + secret_id + ']').hide(); + $('button.lock-secret[secret-id=' + secret_id + ']').show(); }, error: function (xhr, ajaxOptions, thrownError) { if (xhr.status == 403) { From 04c9ebd46dfda7cb8dee06749cc46f6a47d85d2e Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Sat, 6 Aug 2016 15:35:13 -0400 Subject: [PATCH 16/25] Fixes #434: Increased user actions history on home page from 15 to 50; restored admin UI access but disabled bulk deletion function --- netbox/extras/admin.py | 6 ++++++ netbox/netbox/views.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/netbox/extras/admin.py b/netbox/extras/admin.py index c5aec6732..f7ddbbae2 100644 --- a/netbox/extras/admin.py +++ b/netbox/extras/admin.py @@ -19,3 +19,9 @@ class TopologyMapAdmin(admin.ModelAdmin): prepopulated_fields = { 'slug': ['name'], } + + +@admin.register(UserAction) +class UserActionAdmin(admin.ModelAdmin): + actions = None + list_display = ['user', 'action', 'content_type', 'object_id', 'message'] diff --git a/netbox/netbox/views.py b/netbox/netbox/views.py index 801a0a8b9..2da97a2cf 100644 --- a/netbox/netbox/views.py +++ b/netbox/netbox/views.py @@ -43,7 +43,7 @@ def home(request): return render(request, 'home.html', { 'stats': stats, - 'recent_activity': UserAction.objects.select_related('user')[:15] + 'recent_activity': UserAction.objects.select_related('user')[:50] }) From ac2aa7ea897c4189714ba6f277743388d308448d Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Sat, 6 Aug 2016 15:44:28 -0400 Subject: [PATCH 17/25] Fixes #435: Added a "add prefix" button to the VLAN view --- netbox/ipam/views.py | 2 +- netbox/templates/ipam/vlan.html | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index 3ff0ff76e..6509f8cae 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -373,7 +373,7 @@ class PrefixEditView(PermissionRequiredMixin, ObjectEditView): permission_required = 'ipam.change_prefix' model = Prefix form_class = forms.PrefixForm - fields_initial = ['site', 'vrf', 'prefix'] + fields_initial = ['vrf', 'tenant', 'site', 'prefix', 'vlan'] cancel_url = 'ipam:prefix_list' diff --git a/netbox/templates/ipam/vlan.html b/netbox/templates/ipam/vlan.html index 4e5037c7c..d27184824 100644 --- a/netbox/templates/ipam/vlan.html +++ b/netbox/templates/ipam/vlan.html @@ -125,6 +125,14 @@ Prefixes
{% render_table prefix_table %} + {% if perms.ipam.add_prefix %} + + {% endif %}
From ededd3f4647cd2b8e60fafeddf3c89d3894eef0c Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Sat, 6 Aug 2016 16:02:57 -0400 Subject: [PATCH 18/25] Fixes #253: Added ability to search by prefix to IP address filters --- netbox/ipam/filters.py | 14 ++++++++++++++ netbox/ipam/forms.py | 7 ++++++- netbox/utilities/forms.py | 3 ++- 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/netbox/ipam/filters.py b/netbox/ipam/filters.py index de8a240bb..3a73caf84 100644 --- a/netbox/ipam/filters.py +++ b/netbox/ipam/filters.py @@ -191,6 +191,10 @@ class IPAddressFilter(django_filters.FilterSet): action='search', label='Search', ) + parent = django_filters.MethodFilter( + action='search_by_parent', + label='Parent prefix', + ) vrf = django_filters.MethodFilter( action='_vrf', label='VRF', @@ -238,6 +242,16 @@ class IPAddressFilter(django_filters.FilterSet): pass return queryset.filter(qs_filter) + def search_by_parent(self, queryset, value): + value = value.strip() + if not value: + return queryset + try: + query = str(IPNetwork(value).cidr) + return queryset.filter(address__net_contained_or_equal=query) + except AddrFormatError: + return queryset.none() + def _vrf(self, queryset, value): if str(value) == '': return queryset diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index 134c2933c..666b2ee81 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -293,7 +293,9 @@ def prefix_role_choices(): class PrefixFilterForm(forms.Form, BootstrapMixin): - parent = forms.CharField(required=False, label='Search Within') + parent = forms.CharField(required=False, label='Search Within', widget=forms.TextInput(attrs={ + 'placeholder': 'Network', + })) vrf = forms.MultipleChoiceField(required=False, choices=prefix_vrf_choices, label='VRF', widget=forms.SelectMultiple(attrs={'size': 6})) tenant = forms.MultipleChoiceField(required=False, choices=tenant_choices, label='Tenant', @@ -444,6 +446,9 @@ def ipaddress_vrf_choices(): class IPAddressFilterForm(forms.Form, BootstrapMixin): + parent = forms.CharField(required=False, label='Search Within', widget=forms.TextInput(attrs={ + 'placeholder': 'Prefix', + })) family = forms.ChoiceField(required=False, choices=ipaddress_family_choices, label='Address Family') vrf = forms.MultipleChoiceField(required=False, choices=ipaddress_vrf_choices, label='VRF', widget=forms.SelectMultiple(attrs={'size': 6})) diff --git a/netbox/utilities/forms.py b/netbox/utilities/forms.py index 836fe633f..979bdd0ad 100644 --- a/netbox/utilities/forms.py +++ b/netbox/utilities/forms.py @@ -231,7 +231,8 @@ class BootstrapMixin(forms.BaseForm): field.widget.attrs['class'] = 'form-control' if field.required: field.widget.attrs['required'] = 'required' - field.widget.attrs['placeholder'] = field.label + if 'placeholder' not in field.widget.attrs: + field.widget.attrs['placeholder'] = field.label class ConfirmationForm(forms.Form, BootstrapMixin): From efe7b460216f61f856f4f932f6c3443353d3cef5 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Sat, 6 Aug 2016 16:16:35 -0400 Subject: [PATCH 19/25] Release v1.4.2 --- netbox/netbox/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 364cf0c09..d1272f4a2 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -12,7 +12,7 @@ except ImportError: "the documentation.") -VERSION = '1.4.2-dev' +VERSION = '1.4.2' # Import local configuration for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']: From c19e358eefa5f8a366c39f1ba878361c30eebc09 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Sat, 6 Aug 2016 16:19:44 -0400 Subject: [PATCH 20/25] Making PEP8 happy --- netbox/dcim/models.py | 54 +++++++++++++++++++++++++------------------ 1 file changed, 32 insertions(+), 22 deletions(-) diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index 3202c81f8..119336569 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -72,36 +72,46 @@ IFACE_FF_E3 = 4050 IFACE_FF_STACKWISE = 5000 IFACE_FF_STACKWISE_PLUS = 5050 IFACE_FF_CHOICES = [ - ['Virtual interfaces', [ - [IFACE_FF_VIRTUAL, 'Virtual'], + [ + 'Virtual interfaces', + [ + [IFACE_FF_VIRTUAL, 'Virtual'], ] ], - ['Ethernet', [ - [IFACE_FF_100M_COPPER, '100BASE-TX (10/100M)'], - [IFACE_FF_1GE_COPPER, '1000BASE-T (1GE)'], - [IFACE_FF_10GE_COPPER, '10GBASE-T (10GE)'], + [ + 'Ethernet', + [ + [IFACE_FF_100M_COPPER, '100BASE-TX (10/100M)'], + [IFACE_FF_1GE_COPPER, '1000BASE-T (1GE)'], + [IFACE_FF_10GE_COPPER, '10GBASE-T (10GE)'], ] ], - ['Modular', [ - [IFACE_FF_GBIC, 'GBIC (1GE)'], - [IFACE_FF_SFP, 'SFP (1GE)'], - [IFACE_FF_XFP, 'XFP (10GE)'], - [IFACE_FF_SFP_PLUS, 'SFP+ (10GE)'], - [IFACE_FF_QSFP_PLUS, 'QSFP+ (40GE)'], - [IFACE_FF_CFP, 'CFP (100GE)'], - [IFACE_FF_QSFP28, 'QSFP28 (100GE)'], + [ + 'Modular', + [ + [IFACE_FF_GBIC, 'GBIC (1GE)'], + [IFACE_FF_SFP, 'SFP (1GE)'], + [IFACE_FF_XFP, 'XFP (10GE)'], + [IFACE_FF_SFP_PLUS, 'SFP+ (10GE)'], + [IFACE_FF_QSFP_PLUS, 'QSFP+ (40GE)'], + [IFACE_FF_CFP, 'CFP (100GE)'], + [IFACE_FF_QSFP28, 'QSFP28 (100GE)'], ] ], - ['Serial', [ - [IFACE_FF_T1, 'T1 (1.544 Mbps)'], - [IFACE_FF_E1, 'E1 (2.048 Mbps)'], - [IFACE_FF_T3, 'T3 (45 Mbps)'], - [IFACE_FF_E3, 'E3 (34 Mbps)'], + [ + 'Serial', + [ + [IFACE_FF_T1, 'T1 (1.544 Mbps)'], + [IFACE_FF_E1, 'E1 (2.048 Mbps)'], + [IFACE_FF_T3, 'T3 (45 Mbps)'], + [IFACE_FF_E3, 'E3 (34 Mbps)'], ] ], - ['Stacking', [ - [IFACE_FF_STACKWISE, 'Cisco StackWise'], - [IFACE_FF_STACKWISE_PLUS, 'Cisco StackWise Plus'], + [ + 'Stacking', + [ + [IFACE_FF_STACKWISE, 'Cisco StackWise'], + [IFACE_FF_STACKWISE_PLUS, 'Cisco StackWise Plus'], ] ], ] From e55acf8c636cd719cc8b27185a9d96e3d055cec2 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Sat, 6 Aug 2016 16:27:00 -0400 Subject: [PATCH 21/25] Migration for new interface form factors added in #167 --- .../0013_add_interface_form_factors.py | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 netbox/dcim/migrations/0013_add_interface_form_factors.py diff --git a/netbox/dcim/migrations/0013_add_interface_form_factors.py b/netbox/dcim/migrations/0013_add_interface_form_factors.py new file mode 100644 index 000000000..310eb1eb6 --- /dev/null +++ b/netbox/dcim/migrations/0013_add_interface_form_factors.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.8 on 2016-08-06 20:24 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dcim', '0012_site_rack_device_add_tenant'), + ] + + operations = [ + migrations.AlterField( + model_name='interface', + name='form_factor', + field=models.PositiveSmallIntegerField(choices=[[b'Virtual interfaces', [[0, b'Virtual']]], [b'Ethernet', [[800, b'100BASE-TX (10/100M)'], [1000, b'1000BASE-T (1GE)'], [1150, b'10GBASE-T (10GE)']]], [b'Modular', [[1050, b'GBIC (1GE)'], [1100, b'SFP (1GE)'], [1300, b'XFP (10GE)'], [1200, b'SFP+ (10GE)'], [1400, b'QSFP+ (40GE)'], [1500, b'CFP (100GE)'], [1600, b'QSFP28 (100GE)']]], [b'Serial', [[4000, b'T1 (1.544 Mbps)'], [4010, b'E1 (2.048 Mbps)'], [4040, b'T3 (45 Mbps)'], [4050, b'E3 (34 Mbps)']]], [b'Stacking', [[5000, b'Cisco StackWise'], [5050, b'Cisco StackWise Plus']]]], default=1200), + ), + migrations.AlterField( + model_name='interfacetemplate', + name='form_factor', + field=models.PositiveSmallIntegerField(choices=[[b'Virtual interfaces', [[0, b'Virtual']]], [b'Ethernet', [[800, b'100BASE-TX (10/100M)'], [1000, b'1000BASE-T (1GE)'], [1150, b'10GBASE-T (10GE)']]], [b'Modular', [[1050, b'GBIC (1GE)'], [1100, b'SFP (1GE)'], [1300, b'XFP (10GE)'], [1200, b'SFP+ (10GE)'], [1400, b'QSFP+ (40GE)'], [1500, b'CFP (100GE)'], [1600, b'QSFP28 (100GE)']]], [b'Serial', [[4000, b'T1 (1.544 Mbps)'], [4010, b'E1 (2.048 Mbps)'], [4040, b'T3 (45 Mbps)'], [4050, b'E3 (34 Mbps)']]], [b'Stacking', [[5000, b'Cisco StackWise'], [5050, b'Cisco StackWise Plus']]]], default=1200), + ), + ] From 69debfdefa992ca365638dd876f96cbcb6ceb581 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Sat, 6 Aug 2016 16:32:54 -0400 Subject: [PATCH 22/25] Post-release version bump --- netbox/netbox/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index d1272f4a2..22fcfe3f0 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -12,7 +12,7 @@ except ImportError: "the documentation.") -VERSION = '1.4.2' +VERSION = '1.4.3-dev' # Import local configuration for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']: From 324a5e10d74abe852ef6775f66b711e263baf8e7 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 8 Aug 2016 09:45:44 -0400 Subject: [PATCH 23/25] Fixes #433: Correct form validation when editing child devices --- netbox/dcim/forms.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index fef87ded0..5fa133bf5 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -402,7 +402,7 @@ class DeviceForm(forms.ModelForm, BootstrapMixin): self.fields['primary_ip6'].widget.attrs['readonly'] = True # Limit rack choices - if self.is_bound: + if self.is_bound and self.data.get('site'): self.fields['rack'].queryset = Rack.objects.filter(site__pk=self.data['site']) elif self.initial.get('site'): self.fields['rack'].queryset = Rack.objects.filter(site=self.initial['site']) @@ -443,6 +443,8 @@ class DeviceForm(forms.ModelForm, BootstrapMixin): if pk and self.instance.device_type.is_child_device and hasattr(self.instance, 'parent_bay'): self.fields['site'].disabled = True self.fields['rack'].disabled = True + self.initial['site'] = self.instance.parent_bay.device.rack.site_id + self.initial['rack'] = self.instance.parent_bay.device.rack_id class BaseDeviceFromCSVForm(forms.ModelForm): From b131fbd7744375dfb5572a0d9ca04513e11b94d9 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 8 Aug 2016 12:04:20 -0400 Subject: [PATCH 24/25] Corred typo in HTML --- netbox/templates/circuits/circuit_import.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/templates/circuits/circuit_import.html b/netbox/templates/circuits/circuit_import.html index a6cd33ecd..3679dd5f3 100644 --- a/netbox/templates/circuits/circuit_import.html +++ b/netbox/templates/circuits/circuit_import.html @@ -60,7 +60,7 @@ Port Speed - Physical speed in Kbps/td> + Physical speed in Kbps 10000 From 5116db33443dc40fe80f3c556f1d4d89d9509c42 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 8 Aug 2016 12:28:38 -0400 Subject: [PATCH 25/25] Fixes #442: Correct child device import instructions --- netbox/templates/dcim/device_import_child.html | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/netbox/templates/dcim/device_import_child.html b/netbox/templates/dcim/device_import_child.html index 5b9a14541..d01c37653 100644 --- a/netbox/templates/dcim/device_import_child.html +++ b/netbox/templates/dcim/device_import_child.html @@ -36,6 +36,11 @@ Functional role of device Blade Server + + Tenant + Name of tenant (optional) + Pied Piper + Device manufacturer Hardware manufacturer @@ -69,7 +74,7 @@

Example

-
Blade12,Blade Server,Dell,BS2000T,Linux,CAB00577291,Server101,Slot4
+
Blade12,Blade Server,Pied Piper,Dell,BS2000T,Linux,CAB00577291,Server101,Slot4
{% endblock %}