From a9af75bbd114c036f273b21963175da5dfcec6c9 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 13 Dec 2017 11:49:36 -0500 Subject: [PATCH 01/22] Fixes #1767: Use proper template for 404 responses --- netbox/utilities/middleware.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/netbox/utilities/middleware.py b/netbox/utilities/middleware.py index b9c95fc5e..64fb70a07 100644 --- a/netbox/utilities/middleware.py +++ b/netbox/utilities/middleware.py @@ -4,7 +4,7 @@ import sys from django.conf import settings from django.db import ProgrammingError -from django.http import HttpResponseRedirect +from django.http import Http404, HttpResponseRedirect from django.shortcuts import render from django.urls import reverse @@ -61,6 +61,10 @@ class ExceptionHandlingMiddleware(object): if settings.DEBUG: return + # Ignore Http404s (defer to Django's built-in 404 handling) + if isinstance(exception, Http404): + return + # Determine the type of exception if isinstance(exception, ProgrammingError): template_name = 'exceptions/programming_error.html' From 06810bff91f33b6f61542f9b354e2e26020e6bee Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 13 Dec 2017 11:55:31 -0500 Subject: [PATCH 02/22] Fixes #1764: Fixed typos in export buttons --- netbox/templates/dcim/devicetype_list.html | 2 +- netbox/templates/dcim/rackgroup_list.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/netbox/templates/dcim/devicetype_list.html b/netbox/templates/dcim/devicetype_list.html index f246f60b5..69807fcff 100644 --- a/netbox/templates/dcim/devicetype_list.html +++ b/netbox/templates/dcim/devicetype_list.html @@ -13,7 +13,7 @@ Import device types {% endif %} - {% include 'inc/export_button.html' with obj_type='devicetypes' %} + {% include 'inc/export_button.html' with obj_type='device types' %}

{% block title %}Device Types{% endblock %}

diff --git a/netbox/templates/dcim/rackgroup_list.html b/netbox/templates/dcim/rackgroup_list.html index f8b53df57..d5853f11c 100644 --- a/netbox/templates/dcim/rackgroup_list.html +++ b/netbox/templates/dcim/rackgroup_list.html @@ -13,7 +13,7 @@ Import rack groups {% endif %} - {% include 'inc/export_button.html' with obj_type='rackgroups' %} + {% include 'inc/export_button.html' with obj_type='rack groups' %}

{% block title %}Rack Groups{% endblock %}

From da3935ff367132012fe5e8ddf12f7db004ab781f Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 13 Dec 2017 15:23:35 -0500 Subject: [PATCH 03/22] Fixes #1766: Fixed display of "select all" button on device power outlets list --- netbox/templates/dcim/device.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/templates/dcim/device.html b/netbox/templates/dcim/device.html index 549b93465..349a3c96d 100644 --- a/netbox/templates/dcim/device.html +++ b/netbox/templates/dcim/device.html @@ -535,7 +535,7 @@
Power Outlets
- {% if perms.dcim.change_poweroutlet and cs_ports|length > 1 %} + {% if perms.dcim.change_poweroutlet and power_outlets|length > 1 %} From 89bfb4f722d4aec5a772d964b97f6894ce8d4afb Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 14 Dec 2017 13:05:26 -0500 Subject: [PATCH 04/22] Closes #1771: Added name filter for racks --- netbox/dcim/filters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index e466723bd..b0da4ad61 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -163,7 +163,7 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet): class Meta: model = Rack - fields = ['serial', 'type', 'width', 'u_height', 'desc_units'] + fields = ['name', 'serial', 'type', 'width', 'u_height', 'desc_units'] def search(self, queryset, name, value): if not value.strip(): From c97f7041a77cfdc0f36a16e91ea727d79193144b Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 14 Dec 2017 13:12:04 -0500 Subject: [PATCH 05/22] Closes #1772: Added position filter for devices --- netbox/dcim/filters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index b0da4ad61..7fff0720c 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -455,7 +455,7 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet): class Meta: model = Device - fields = ['serial'] + fields = ['serial', 'position'] def search(self, queryset, name, value): if not value.strip(): From 1cebc1248b37cb120f78836760329eb624403f18 Mon Sep 17 00:00:00 2001 From: Mike Culbertson Date: Sat, 16 Dec 2017 12:28:37 -0500 Subject: [PATCH 06/22] Fix for #1778. This will set initial values for visible bulk-add form fields from query args. --- netbox/utilities/views.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/netbox/utilities/views.py b/netbox/utilities/views.py index d8ba3712a..d37332bef 100644 --- a/netbox/utilities/views.py +++ b/netbox/utilities/views.py @@ -308,8 +308,14 @@ class BulkCreateView(View): def get(self, request): + # Set initial values for visible form fields from query args + initial = {} + for field in getattr(self.model_form._meta, 'fields', []): + if request.GET.get(field): + initial[field] = request.GET[field] + form = self.form() - model_form = self.model_form() + model_form = self.model_form(initial=initial) return render(request, self.template_name, { 'obj_type': self.model_form._meta.model._meta.verbose_name, From c10481b99dbfaef42ba7d6045c058ce186d4d693 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 19 Dec 2017 09:37:26 -0500 Subject: [PATCH 07/22] Fixes #1783: Added vm_role filter for device roles --- netbox/dcim/filters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index 7fff0720c..e56a12ac0 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -330,7 +330,7 @@ class DeviceRoleFilter(django_filters.FilterSet): class Meta: model = DeviceRole - fields = ['name', 'slug', 'color'] + fields = ['name', 'slug', 'color', 'vm_role'] class PlatformFilter(django_filters.FilterSet): From c3599bacf20343d8f5f2dbb939ea3a326105dadc Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 19 Dec 2017 15:30:55 -0500 Subject: [PATCH 08/22] Fixes #1785: Omit filter forms from browsable API --- netbox/netbox/api.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/netbox/netbox/api.py b/netbox/netbox/api.py index 0bac06977..28a0d7685 100644 --- a/netbox/netbox/api.py +++ b/netbox/netbox/api.py @@ -20,6 +20,9 @@ class FormlessBrowsableAPIRenderer(BrowsableAPIRenderer): def show_form_for_method(self, *args, **kwargs): return False + def get_filter_form(self, data, view, request): + return None + # # Authentication From 6d9c8fd85bcff2f9570288dfc33e17f9a54dbeed Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 20 Dec 2017 13:18:30 -0500 Subject: [PATCH 09/22] Fixes #1787: Added missing site field to virtualization cluster CSV export --- netbox/virtualization/models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/netbox/virtualization/models.py b/netbox/virtualization/models.py index edb35f4cb..8152c5d57 100644 --- a/netbox/virtualization/models.py +++ b/netbox/virtualization/models.py @@ -139,6 +139,7 @@ class Cluster(CreatedUpdatedModel, CustomFieldModel): self.name, self.type.name, self.group.name if self.group else None, + self.site.name if self.site else None, self.comments, ]) From 50395aa8212b7fb05c52601bc80f4b81482a7982 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 20 Dec 2017 14:01:37 -0500 Subject: [PATCH 10/22] Closes #1773: Moved child prefixes table to its own view --- netbox/ipam/models.py | 43 ++++++++++++------- netbox/ipam/urls.py | 1 + netbox/ipam/views.py | 33 +++++++++----- netbox/templates/ipam/inc/prefix_header.html | 10 ++++- netbox/templates/ipam/prefix.html | 11 ----- netbox/templates/ipam/prefix_ipaddresses.html | 10 ++--- netbox/templates/ipam/prefix_prefixes.html | 12 ++++++ 7 files changed, 77 insertions(+), 43 deletions(-) create mode 100644 netbox/templates/ipam/prefix_prefixes.html diff --git a/netbox/ipam/models.py b/netbox/ipam/models.py index 0ce0afbdf..bdaec4fd8 100644 --- a/netbox/ipam/models.py +++ b/netbox/ipam/models.py @@ -281,12 +281,28 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel): def get_duplicates(self): return Prefix.objects.filter(vrf=self.vrf, prefix=str(self.prefix)).exclude(pk=self.pk) + def get_child_prefixes(self): + """ + Return all Prefixes within this Prefix and VRF. + """ + return Prefix.objects.filter(prefix__net_contained=str(self.prefix), vrf=self.vrf) + def get_child_ips(self): """ - Return all IPAddresses within this Prefix. + Return all IPAddresses within this Prefix and VRF. """ return IPAddress.objects.filter(address__net_host_contained=str(self.prefix), vrf=self.vrf) + def get_available_prefixes(self): + """ + Return all available Prefixes within this prefix as an IPSet. + """ + prefix = netaddr.IPSet(self.prefix) + child_prefixes = netaddr.IPSet([child.prefix for child in self.get_child_prefixes()]) + available_prefixes = prefix - child_prefixes + + return available_prefixes + def get_available_ips(self): """ Return all available IPs within this prefix as an IPSet. @@ -304,15 +320,23 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel): return available_ips + def get_first_available_prefix(self): + """ + Return the first available child prefix within the prefix (or None). + """ + available_prefixes = self.get_available_prefixes() + if not available_prefixes: + return None + return available_prefixes.iter_cidrs()[0] + def get_first_available_ip(self): """ Return the first available IP within the prefix (or None). """ available_ips = self.get_available_ips() - if available_ips: - return '{}/{}'.format(next(available_ips.__iter__()), self.prefix.prefixlen) - else: + if not available_ips: return None + return '{}/{}'.format(next(available_ips.__iter__()), self.prefix.prefixlen) def get_utilization(self): """ @@ -330,17 +354,6 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel): prefix_size -= 2 return int(float(child_count) / prefix_size * 100) - @property - def new_subnet(self): - if self.family == 4: - if self.prefix.prefixlen <= 30: - return netaddr.IPNetwork('{}/{}'.format(self.prefix.network, self.prefix.prefixlen + 1)) - return None - if self.family == 6: - if self.prefix.prefixlen <= 126: - return netaddr.IPNetwork('{}/{}'.format(self.prefix.network, self.prefix.prefixlen + 1)) - return None - class IPAddressManager(models.Manager): diff --git a/netbox/ipam/urls.py b/netbox/ipam/urls.py index a67e9e865..383b13d8f 100644 --- a/netbox/ipam/urls.py +++ b/netbox/ipam/urls.py @@ -51,6 +51,7 @@ urlpatterns = [ url(r'^prefixes/(?P\d+)/$', views.PrefixView.as_view(), name='prefix'), url(r'^prefixes/(?P\d+)/edit/$', views.PrefixEditView.as_view(), name='prefix_edit'), url(r'^prefixes/(?P\d+)/delete/$', views.PrefixDeleteView.as_view(), name='prefix_delete'), + url(r'^prefixes/(?P\d+)/prefixes/$', views.PrefixPrefixesView.as_view(), name='prefix_prefixes'), url(r'^prefixes/(?P\d+)/ip-addresses/$', views.PrefixIPAddressesView.as_view(), name='prefix_ipaddresses'), # IP addresses diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index e36162938..25475eec8 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -476,6 +476,20 @@ class PrefixView(View): duplicate_prefix_table = tables.PrefixTable(list(duplicate_prefixes), orderable=False) duplicate_prefix_table.exclude = ('vrf',) + return render(request, 'ipam/prefix.html', { + 'prefix': prefix, + 'aggregate': aggregate, + 'parent_prefix_table': parent_prefix_table, + 'duplicate_prefix_table': duplicate_prefix_table, + }) + + +class PrefixPrefixesView(View): + + def get(self, request, pk): + + prefix = get_object_or_404(Prefix.objects.all(), pk=pk) + # Child prefixes table child_prefixes = Prefix.objects.filter( vrf=prefix.vrf, prefix__net_contained=str(prefix.prefix) @@ -484,15 +498,16 @@ class PrefixView(View): ).annotate_depth(limit=0) if child_prefixes: child_prefixes = add_available_prefixes(prefix.prefix, child_prefixes) - child_prefix_table = tables.PrefixDetailTable(child_prefixes) + + prefix_table = tables.PrefixDetailTable(child_prefixes) if request.user.has_perm('ipam.change_prefix') or request.user.has_perm('ipam.delete_prefix'): - child_prefix_table.columns.show('pk') + prefix_table.columns.show('pk') paginate = { 'klass': EnhancedPaginator, 'per_page': request.GET.get('per_page', settings.PAGINATE_COUNT) } - RequestConfig(request, paginate).configure(child_prefix_table) + RequestConfig(request, paginate).configure(prefix_table) # Compile permissions list for rendering the object table permissions = { @@ -501,15 +516,12 @@ class PrefixView(View): 'delete': request.user.has_perm('ipam.delete_prefix'), } - return render(request, 'ipam/prefix.html', { + return render(request, 'ipam/prefix_prefixes.html', { 'prefix': prefix, - 'aggregate': aggregate, - 'parent_prefix_table': parent_prefix_table, - 'child_prefix_table': child_prefix_table, - 'duplicate_prefix_table': duplicate_prefix_table, - 'bulk_querystring': 'vrf_id={}&within={}'.format(prefix.vrf or '0', prefix.prefix), + 'first_available_prefix': prefix.get_first_available_prefix(), + 'prefix_table': prefix_table, 'permissions': permissions, - 'return_url': prefix.get_absolute_url(), + 'bulk_querystring': 'vrf_id={}&within={}'.format(prefix.vrf.pk if prefix.vrf else '0', prefix.prefix), }) @@ -544,6 +556,7 @@ class PrefixIPAddressesView(View): return render(request, 'ipam/prefix_ipaddresses.html', { 'prefix': prefix, + 'first_available_ip': prefix.get_first_available_ip(), 'ip_table': ip_table, 'permissions': permissions, 'bulk_querystring': 'vrf_id={}&parent={}'.format(prefix.vrf.pk if prefix.vrf else '0', prefix.prefix), diff --git a/netbox/templates/ipam/inc/prefix_header.html b/netbox/templates/ipam/inc/prefix_header.html index 553af1a66..f3c694c64 100644 --- a/netbox/templates/ipam/inc/prefix_header.html +++ b/netbox/templates/ipam/inc/prefix_header.html @@ -22,8 +22,13 @@
- {% if perms.ipam.add_ipaddress %} - + {% if perms.ipam.add_prefix and active_tab == 'prefixes' and first_available_prefix %} + + Add Child Prefix + + {% endif %} + {% if perms.ipam.add_ipaddress and active_tab == 'ip-addresses' and first_available_ip %} + Add an IP Address @@ -45,5 +50,6 @@ {% include 'inc/created_updated.html' with obj=prefix %} diff --git a/netbox/templates/ipam/prefix.html b/netbox/templates/ipam/prefix.html index 3447f2ad9..5c168e247 100644 --- a/netbox/templates/ipam/prefix.html +++ b/netbox/templates/ipam/prefix.html @@ -139,15 +139,4 @@ {% include 'panel_table.html' with table=parent_prefix_table heading='Parent Prefixes' %}
-
-
- {% if child_prefix_table.rows %} - {% include 'utilities/obj_table.html' with table=child_prefix_table table_template='panel_table.html' heading='Child Prefixes' parent=prefix bulk_edit_url='ipam:prefix_bulk_edit' bulk_delete_url='ipam:prefix_bulk_delete' %} - {% elif prefix.new_subnet %} - - Add Child Prefix - - {% endif %} -
-
{% endblock %} diff --git a/netbox/templates/ipam/prefix_ipaddresses.html b/netbox/templates/ipam/prefix_ipaddresses.html index 1b777417d..02e90569d 100644 --- a/netbox/templates/ipam/prefix_ipaddresses.html +++ b/netbox/templates/ipam/prefix_ipaddresses.html @@ -3,10 +3,10 @@ {% block title %}{{ prefix }} - IP Addresses{% endblock %} {% block content %} -{% include 'ipam/inc/prefix_header.html' with active_tab='ip-addresses' %} -
-
- {% include 'utilities/obj_table.html' with table=ip_table table_template='panel_table.html' heading='IP Addresses' bulk_edit_url='ipam:ipaddress_bulk_edit' bulk_delete_url='ipam:ipaddress_bulk_delete' %} + {% include 'ipam/inc/prefix_header.html' with active_tab='ip-addresses' %} +
+
+ {% include 'utilities/obj_table.html' with table=ip_table table_template='panel_table.html' heading='IP Addresses' bulk_edit_url='ipam:ipaddress_bulk_edit' bulk_delete_url='ipam:ipaddress_bulk_delete' %} +
-
{% endblock %} diff --git a/netbox/templates/ipam/prefix_prefixes.html b/netbox/templates/ipam/prefix_prefixes.html new file mode 100644 index 000000000..4951942c3 --- /dev/null +++ b/netbox/templates/ipam/prefix_prefixes.html @@ -0,0 +1,12 @@ +{% extends '_base.html' %} + +{% block title %}{{ prefix }} - Prefixes{% endblock %} + +{% block content %} + {% include 'ipam/inc/prefix_header.html' with active_tab='prefixes' %} +
+
+ {% include 'utilities/obj_table.html' with table=prefix_table table_template='panel_table.html' heading='Child Prefixes' bulk_edit_url='ipam:prefix_bulk_edit' bulk_delete_url='ipam:prefix_bulk_delete' %} +
+
+{% endblock %} From a598035236812176117afa7f6ccd1885f6b5103e Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 20 Dec 2017 14:09:52 -0500 Subject: [PATCH 11/22] Closes #1774: Include a button to refine search results for all object types under global search --- netbox/templates/search.html | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/netbox/templates/search.html b/netbox/templates/search.html index afd4293ca..36c0fb5ad 100644 --- a/netbox/templates/search.html +++ b/netbox/templates/search.html @@ -13,12 +13,14 @@ {% for obj_type in results %}

{{ obj_type.name|bettertitle }}

{% include 'panel_table.html' with table=obj_type.table hide_paginator=True %} - {% if obj_type.table.page.has_next %} - - + + + {% if obj_type.table.page.has_next %} See all {{ obj_type.table.page.paginator.count }} results - - {% endif %} + {% else %} + Refine search + {% endif %} +
{% endfor %}
From 779d6853355b6ed2a72847b9a54e1a6576083ceb Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 20 Dec 2017 14:24:12 -0500 Subject: [PATCH 12/22] Closes #1784: Added cluster_type filters for virtual machines --- netbox/virtualization/filters.py | 11 +++++++++++ netbox/virtualization/forms.py | 5 +++++ 2 files changed, 16 insertions(+) diff --git a/netbox/virtualization/filters.py b/netbox/virtualization/filters.py index 1563e57a8..bd3e19400 100644 --- a/netbox/virtualization/filters.py +++ b/netbox/virtualization/filters.py @@ -84,6 +84,17 @@ class VirtualMachineFilter(CustomFieldFilterSet): to_field_name='slug', label='Cluster group (slug)', ) + cluster_type_id = django_filters.ModelMultipleChoiceFilter( + name='cluster__type', + queryset=ClusterType.objects.all(), + label='Cluster type (ID)', + ) + cluster_type = django_filters.ModelMultipleChoiceFilter( + name='cluster__type__slug', + queryset=ClusterType.objects.all(), + to_field_name='slug', + label='Cluster type (slug)', + ) cluster_id = django_filters.ModelMultipleChoiceFilter( queryset=Cluster.objects.all(), label='Cluster (ID)', diff --git a/netbox/virtualization/forms.py b/netbox/virtualization/forms.py index 50e2816f1..16b33962c 100644 --- a/netbox/virtualization/forms.py +++ b/netbox/virtualization/forms.py @@ -340,6 +340,11 @@ class VirtualMachineFilterForm(BootstrapMixin, CustomFieldFilterForm): to_field_name='slug', null_option=(0, 'None') ) + cluster_type = FilterChoiceField( + queryset=ClusterType.objects.all(), + to_field_name='slug', + null_option=(0, 'None') + ) cluster_id = FilterChoiceField( queryset=Cluster.objects.annotate(filter_count=Count('virtual_machines')), label='Cluster' From ac71416eb9435419dc2d8be4a77cbe44eff72f2b Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 20 Dec 2017 14:48:42 -0500 Subject: [PATCH 13/22] Closes #1775: Added instructions for enabling STARTTLS for LDAP authentication --- docs/installation/ldap.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/installation/ldap.md b/docs/installation/ldap.md index 3cbc0d32c..bd6bcc81f 100644 --- a/docs/installation/ldap.md +++ b/docs/installation/ldap.md @@ -24,7 +24,7 @@ sudo pip install django-auth-ldap # Configuration -Create a file in the same directory as `configuration.py` (typically `netbox/netbox/`) named `ldap_config.py`. Define all of the parameters required below in `ldap_config.py`. +Create a file in the same directory as `configuration.py` (typically `netbox/netbox/`) named `ldap_config.py`. Define all of the parameters required below in `ldap_config.py`. Complete documentation of all `django-auth-ldap` configuration options is included in the project's [official documentation](http://django-auth-ldap.readthedocs.io/). ## General Server Configuration @@ -52,6 +52,8 @@ AUTH_LDAP_BIND_PASSWORD = "demo" LDAP_IGNORE_CERT_ERRORS = True ``` +STARTTLS can be configured by setting `AUTH_LDAP_START_TLS = True` and using the `ldap://` URI scheme. + ## User Authentication !!! info @@ -78,7 +80,7 @@ AUTH_LDAP_USER_ATTR_MAP = { ``` # User Groups for Permissions -!!! Info +!!! info When using Microsoft Active Directory, Support for nested Groups can be activated by using `GroupOfNamesType()` instead of `NestedGroupOfNamesType()` for `AUTH_LDAP_GROUP_TYPE`. ```python From 841471104b9e8a97bdbb9f21baed461d3be54aa3 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 20 Dec 2017 15:24:07 -0500 Subject: [PATCH 14/22] Release v2.2.8 --- 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 88b25e1de..e3c3025f9 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -13,7 +13,7 @@ except ImportError: ) -VERSION = '2.2.8-dev' +VERSION = '2.2.8' BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) From b4a842d9dac1455ea1fb73728fe801709d124707 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 20 Dec 2017 15:32:57 -0500 Subject: [PATCH 15/22] 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 e3c3025f9..158848aa5 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -13,7 +13,7 @@ except ImportError: ) -VERSION = '2.2.8' +VERSION = '2.2.9-dev' BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) From 78ed85943bc6072d411d80720473890183eea335 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 26 Dec 2017 12:08:22 -0500 Subject: [PATCH 16/22] Fixes #1765: Improved rendering of null options for model choice fields in filter forms --- netbox/circuits/forms.py | 2 +- netbox/dcim/forms.py | 16 ++++++++-------- netbox/ipam/forms.py | 29 ++++++++++++++++------------- netbox/tenancy/forms.py | 2 +- netbox/utilities/filters.py | 2 +- netbox/utilities/forms.py | 31 ++++++++++++++++++------------- netbox/virtualization/forms.py | 16 ++++++++-------- 7 files changed, 53 insertions(+), 45 deletions(-) diff --git a/netbox/circuits/forms.py b/netbox/circuits/forms.py index 26d38d56a..8acad4bb9 100644 --- a/netbox/circuits/forms.py +++ b/netbox/circuits/forms.py @@ -174,7 +174,7 @@ class CircuitFilterForm(BootstrapMixin, CustomFieldFilterForm): tenant = FilterChoiceField( queryset=Tenant.objects.annotate(filter_count=Count('circuits')), to_field_name='slug', - null_option=(0, 'None') + null_label='-- None --' ) site = FilterChoiceField( queryset=Site.objects.annotate(filter_count=Count('circuit_terminations')), diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 1f5d50c4d..e051e33e5 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -163,7 +163,7 @@ class SiteFilterForm(BootstrapMixin, CustomFieldFilterForm): tenant = FilterChoiceField( queryset=Tenant.objects.annotate(filter_count=Count('sites')), to_field_name='slug', - null_option=(0, 'None') + null_label='-- None --' ) @@ -359,17 +359,17 @@ class RackFilterForm(BootstrapMixin, CustomFieldFilterForm): group_id = FilterChoiceField( queryset=RackGroup.objects.select_related('site').annotate(filter_count=Count('racks')), label='Rack group', - null_option=(0, 'None') + null_label='-- None --' ) tenant = FilterChoiceField( queryset=Tenant.objects.annotate(filter_count=Count('racks')), to_field_name='slug', - null_option=(0, 'None') + null_label='-- None --' ) role = FilterChoiceField( queryset=RackRole.objects.annotate(filter_count=Count('racks')), to_field_name='slug', - null_option=(0, 'None') + null_label='-- None --' ) @@ -411,7 +411,7 @@ class RackReservationFilterForm(BootstrapMixin, forms.Form): group_id = FilterChoiceField( queryset=RackGroup.objects.select_related('site').annotate(filter_count=Count('racks__reservations')), label='Rack group', - null_option=(0, 'None') + null_label='-- None --' ) @@ -1031,7 +1031,7 @@ class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm): rack_id = FilterChoiceField( queryset=Rack.objects.annotate(filter_count=Count('devices')), label='Rack', - null_option=(0, 'None'), + null_label='-- None --', ) role = FilterChoiceField( queryset=DeviceRole.objects.annotate(filter_count=Count('devices')), @@ -1040,7 +1040,7 @@ class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm): tenant = FilterChoiceField( queryset=Tenant.objects.annotate(filter_count=Count('devices')), to_field_name='slug', - null_option=(0, 'None'), + null_label='-- None --', ) manufacturer_id = FilterChoiceField(queryset=Manufacturer.objects.all(), label='Manufacturer') device_type_id = FilterChoiceField( @@ -1052,7 +1052,7 @@ class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm): platform = FilterChoiceField( queryset=Platform.objects.annotate(filter_count=Count('devices')), to_field_name='slug', - null_option=(0, 'None'), + null_label='-- None --', ) status = forms.MultipleChoiceField(choices=device_status_choices, required=False) mac_address = forms.CharField(required=False, label='MAC address') diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index a5b0a7e3c..c67921e3e 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -78,8 +78,11 @@ class VRFBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): class VRFFilterForm(BootstrapMixin, CustomFieldFilterForm): model = VRF q = forms.CharField(required=False, label='Search') - tenant = FilterChoiceField(queryset=Tenant.objects.annotate(filter_count=Count('vrfs')), to_field_name='slug', - null_option=(0, None)) + tenant = FilterChoiceField( + queryset=Tenant.objects.annotate(filter_count=Count('vrfs')), + to_field_name='slug', + null_label='-- None --' + ) # @@ -368,23 +371,23 @@ class PrefixFilterForm(BootstrapMixin, CustomFieldFilterForm): queryset=VRF.objects.annotate(filter_count=Count('prefixes')), to_field_name='rd', label='VRF', - null_option=(0, 'Global') + null_label='-- Global --' ) tenant = FilterChoiceField( queryset=Tenant.objects.annotate(filter_count=Count('prefixes')), to_field_name='slug', - null_option=(0, 'None') + null_label='-- None --' ) status = forms.MultipleChoiceField(choices=prefix_status_choices, required=False) site = FilterChoiceField( queryset=Site.objects.annotate(filter_count=Count('prefixes')), to_field_name='slug', - null_option=(0, 'None') + null_label='-- None --' ) role = FilterChoiceField( queryset=Role.objects.annotate(filter_count=Count('prefixes')), to_field_name='slug', - null_option=(0, 'None') + null_label='-- None --' ) expand = forms.BooleanField(required=False, label='Expand prefix hierarchy') @@ -719,12 +722,12 @@ class IPAddressFilterForm(BootstrapMixin, CustomFieldFilterForm): queryset=VRF.objects.annotate(filter_count=Count('ip_addresses')), to_field_name='rd', label='VRF', - null_option=(0, 'Global') + null_label='-- Global --' ) tenant = FilterChoiceField( queryset=Tenant.objects.annotate(filter_count=Count('ip_addresses')), to_field_name='slug', - null_option=(0, 'None') + null_label='-- None --' ) status = forms.MultipleChoiceField(choices=ipaddress_status_choices, required=False) role = forms.MultipleChoiceField(choices=ipaddress_role_choices, required=False) @@ -766,7 +769,7 @@ class VLANGroupFilterForm(BootstrapMixin, forms.Form): site = FilterChoiceField( queryset=Site.objects.annotate(filter_count=Count('vlan_groups')), to_field_name='slug', - null_option=(0, 'Global') + null_label='-- Global --' ) @@ -896,23 +899,23 @@ class VLANFilterForm(BootstrapMixin, CustomFieldFilterForm): site = FilterChoiceField( queryset=Site.objects.annotate(filter_count=Count('vlans')), to_field_name='slug', - null_option=(0, 'Global') + null_label='-- Global --' ) group_id = FilterChoiceField( queryset=VLANGroup.objects.annotate(filter_count=Count('vlans')), label='VLAN group', - null_option=(0, 'None') + null_label='-- None --' ) tenant = FilterChoiceField( queryset=Tenant.objects.annotate(filter_count=Count('vlans')), to_field_name='slug', - null_option=(0, 'None') + null_label='-- None --' ) status = forms.MultipleChoiceField(choices=vlan_status_choices, required=False) role = FilterChoiceField( queryset=Role.objects.annotate(filter_count=Count('vlans')), to_field_name='slug', - null_option=(0, 'None') + null_label='-- None --' ) diff --git a/netbox/tenancy/forms.py b/netbox/tenancy/forms.py index 5eb3bda61..00194d4e8 100644 --- a/netbox/tenancy/forms.py +++ b/netbox/tenancy/forms.py @@ -81,7 +81,7 @@ class TenantFilterForm(BootstrapMixin, CustomFieldFilterForm): group = FilterChoiceField( queryset=TenantGroup.objects.annotate(filter_count=Count('tenants')), to_field_name='slug', - null_option=(0, 'None') + null_label='-- None --' ) diff --git a/netbox/utilities/filters.py b/netbox/utilities/filters.py index 647ecb723..3e403e676 100644 --- a/netbox/utilities/filters.py +++ b/netbox/utilities/filters.py @@ -42,7 +42,7 @@ class NullableModelMultipleChoiceField(forms.ModelMultipleChoiceField): """ iterator = forms.models.ModelChoiceIterator - def __init__(self, null_value=0, null_label='None', *args, **kwargs): + def __init__(self, null_value=0, null_label='-- None --', *args, **kwargs): self.null_value = null_value self.null_label = null_label super(NullableModelMultipleChoiceField, self).__init__(*args, **kwargs) diff --git a/netbox/utilities/forms.py b/netbox/utilities/forms.py index 6ba49d02c..1817cd9a9 100644 --- a/netbox/utilities/forms.py +++ b/netbox/utilities/forms.py @@ -407,11 +407,25 @@ class SlugField(forms.SlugField): self.widget.attrs['slug-source'] = slug_source -class FilterChoiceFieldMixin(object): - iterator = forms.models.ModelChoiceIterator +class FilterChoiceIterator(forms.models.ModelChoiceIterator): - def __init__(self, null_option=None, *args, **kwargs): - self.null_option = null_option + def __iter__(self): + # Filter on "empty" choice using FILTERS_NULL_CHOICE_VALUE (instead of an empty string) + if self.field.null_label is not None: + yield (settings.FILTERS_NULL_CHOICE_VALUE, self.field.null_label) + queryset = self.queryset.all() + # Can't use iterator() when queryset uses prefetch_related() + if not queryset._prefetch_related_lookups: + queryset = queryset.iterator() + for obj in queryset: + yield self.choice(obj) + + +class FilterChoiceFieldMixin(object): + iterator = FilterChoiceIterator + + def __init__(self, null_label=None, *args, **kwargs): + self.null_label = null_label if 'required' not in kwargs: kwargs['required'] = False if 'widget' not in kwargs: @@ -424,15 +438,6 @@ class FilterChoiceFieldMixin(object): return '{} ({})'.format(label, obj.filter_count) return label - def _get_choices(self): - if hasattr(self, '_choices'): - return self._choices - if self.null_option is not None: - return itertools.chain([self.null_option], self.iterator(self)) - return self.iterator(self) - - choices = property(_get_choices, forms.ChoiceField._set_choices) - class FilterChoiceField(FilterChoiceFieldMixin, forms.ModelMultipleChoiceField): pass diff --git a/netbox/virtualization/forms.py b/netbox/virtualization/forms.py index 16b33962c..d697de755 100644 --- a/netbox/virtualization/forms.py +++ b/netbox/virtualization/forms.py @@ -137,13 +137,13 @@ class ClusterFilterForm(BootstrapMixin, CustomFieldFilterForm): group = FilterChoiceField( queryset=ClusterGroup.objects.annotate(filter_count=Count('clusters')), to_field_name='slug', - null_option=(0, 'None'), + null_label='-- None --', required=False, ) site = FilterChoiceField( queryset=Site.objects.annotate(filter_count=Count('clusters')), to_field_name='slug', - null_option=(0, 'None'), + null_label='-- None --', required=False, ) @@ -338,12 +338,12 @@ class VirtualMachineFilterForm(BootstrapMixin, CustomFieldFilterForm): cluster_group = FilterChoiceField( queryset=ClusterGroup.objects.all(), to_field_name='slug', - null_option=(0, 'None') + null_label='-- None --' ) cluster_type = FilterChoiceField( queryset=ClusterType.objects.all(), to_field_name='slug', - null_option=(0, 'None') + null_label='-- None --' ) cluster_id = FilterChoiceField( queryset=Cluster.objects.annotate(filter_count=Count('virtual_machines')), @@ -352,23 +352,23 @@ class VirtualMachineFilterForm(BootstrapMixin, CustomFieldFilterForm): site = FilterChoiceField( queryset=Site.objects.annotate(filter_count=Count('clusters__virtual_machines')), to_field_name='slug', - null_option=(0, 'None') + null_label='-- None --' ) role = FilterChoiceField( queryset=DeviceRole.objects.filter(vm_role=True).annotate(filter_count=Count('virtual_machines')), to_field_name='slug', - null_option=(0, 'None') + null_label='-- None --' ) status = forms.MultipleChoiceField(choices=vm_status_choices, required=False) tenant = FilterChoiceField( queryset=Tenant.objects.annotate(filter_count=Count('virtual_machines')), to_field_name='slug', - null_option=(0, 'None') + null_label='-- None --' ) platform = FilterChoiceField( queryset=Platform.objects.annotate(filter_count=Count('virtual_machines')), to_field_name='slug', - null_option=(0, 'None') + null_label='-- None --' ) From 935da0d51f0ca6cda8fd9c5fbfece34868bca4c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20Fr=C3=BChwirth?= Date: Fri, 29 Dec 2017 13:21:32 +0100 Subject: [PATCH 17/22] Fixes #1802: Typo in ldap.md --- docs/installation/ldap.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation/ldap.md b/docs/installation/ldap.md index bd6bcc81f..5aeec0eb1 100644 --- a/docs/installation/ldap.md +++ b/docs/installation/ldap.md @@ -81,7 +81,7 @@ AUTH_LDAP_USER_ATTR_MAP = { # User Groups for Permissions !!! info - When using Microsoft Active Directory, Support for nested Groups can be activated by using `GroupOfNamesType()` instead of `NestedGroupOfNamesType()` for `AUTH_LDAP_GROUP_TYPE`. + When using Microsoft Active Directory, support for nested groups can be activated by using `NestedGroupOfNamesType()` instead of `GroupOfNamesType()` for `AUTH_LDAP_GROUP_TYPE`. ```python from django_auth_ldap.config import LDAPSearch, GroupOfNamesType From bb653e733cb8b70fd3eabca8867954792df03844 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 5 Jan 2018 15:19:27 -0500 Subject: [PATCH 18/22] Fixes #1621: Tweaked LLDP interface name evaluation logic --- netbox/templates/dcim/device_lldp_neighbors.html | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/netbox/templates/dcim/device_lldp_neighbors.html b/netbox/templates/dcim/device_lldp_neighbors.html index c79cf8955..4fe914f64 100644 --- a/netbox/templates/dcim/device_lldp_neighbors.html +++ b/netbox/templates/dcim/device_lldp_neighbors.html @@ -58,9 +58,10 @@ $(document).ready(function() { // Glean configured hostnames/interfaces from the DOM var configured_device = row.children('td.configured_device').attr('data'); var configured_interface = row.children('td.configured_interface').attr('data'); + var configured_interface_short = null; if (configured_interface) { // Match long-form IOS names against short ones (e.g. Gi0/1 == GigabitEthernet0/1). - configured_interface = configured_interface.replace(/^([A-Z][a-z])[^0-9]*([0-9\/]+)$/, "$1$2"); + configured_interface_short = configured_interface.replace(/^([A-Z][a-z])[^0-9]*([0-9\/]+)$/, "$1$2"); } // Clean up hostnames/interfaces learned via LLDP @@ -76,6 +77,8 @@ $(document).ready(function() { row.addClass('info'); } else if (configured_device == lldp_device && configured_interface == lldp_interface) { row.addClass('success'); + } else if (configured_device == lldp_device && configured_interface_short == lldp_interface) { + row.addClass('success'); } else { row.addClass('danger'); } From e58d1ac87e7629f7b77a58d5befa60c04b270298 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 5 Jan 2018 15:31:48 -0500 Subject: [PATCH 19/22] Fixes #1807: Populate VRF from parent when creating a new prefix --- netbox/templates/ipam/prefix_prefixes.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/templates/ipam/prefix_prefixes.html b/netbox/templates/ipam/prefix_prefixes.html index 4951942c3..2535b672d 100644 --- a/netbox/templates/ipam/prefix_prefixes.html +++ b/netbox/templates/ipam/prefix_prefixes.html @@ -6,7 +6,7 @@ {% include 'ipam/inc/prefix_header.html' with active_tab='prefixes' %}
- {% include 'utilities/obj_table.html' with table=prefix_table table_template='panel_table.html' heading='Child Prefixes' bulk_edit_url='ipam:prefix_bulk_edit' bulk_delete_url='ipam:prefix_bulk_delete' %} + {% include 'utilities/obj_table.html' with table=prefix_table table_template='panel_table.html' heading='Child Prefixes' bulk_edit_url='ipam:prefix_bulk_edit' bulk_delete_url='ipam:prefix_bulk_delete' parent=prefix %}
{% endblock %} From d5ecfe7bef6ffb4e6d3a9ee56c0179edee9d2332 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 10 Jan 2018 09:38:55 -0500 Subject: [PATCH 20/22] Fixes #1809: Populate tenant assignment from parent when creating a new prefix --- netbox/ipam/tables.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/netbox/ipam/tables.py b/netbox/ipam/tables.py index ebb86731c..8d7d29b96 100644 --- a/netbox/ipam/tables.py +++ b/netbox/ipam/tables.py @@ -48,13 +48,7 @@ PREFIX_LINK = """ {% else %} {% endif %} - {{ record.prefix }} - -""" - -PREFIX_LINK_BRIEF = """ - - {{ record.prefix }} + {{ record.prefix }} """ From 7ac27b59c645d1edc6b6dc0ecfce56ba9a8d4304 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 19 Jan 2018 09:25:16 -0500 Subject: [PATCH 21/22] Closes #1824: Add virtual machine count to platforms list --- netbox/dcim/tables.py | 10 +++++++--- netbox/dcim/views.py | 5 ++++- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/netbox/dcim/tables.py b/netbox/dcim/tables.py index e95277704..cb3b1ff3b 100644 --- a/netbox/dcim/tables.py +++ b/netbox/dcim/tables.py @@ -381,13 +381,17 @@ class PlatformTable(BaseTable): pk = ToggleColumn() name = tables.LinkColumn(verbose_name='Name') device_count = tables.Column(verbose_name='Devices') + vm_count = tables.Column(verbose_name='VMs') slug = tables.Column(verbose_name='Slug') - actions = tables.TemplateColumn(template_code=PLATFORM_ACTIONS, attrs={'td': {'class': 'text-right'}}, - verbose_name='') + actions = tables.TemplateColumn( + template_code=PLATFORM_ACTIONS, + attrs={'td': {'class': 'text-right'}}, + verbose_name='' + ) class Meta(BaseTable.Meta): model = Platform - fields = ('pk', 'name', 'device_count', 'slug', 'napalm_driver', 'actions') + fields = ('pk', 'name', 'device_count', 'vm_count', 'slug', 'napalm_driver', 'actions') # diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 9b681e4a7..0dc393cfb 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -754,7 +754,10 @@ class DeviceRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): # class PlatformListView(ObjectListView): - queryset = Platform.objects.annotate(device_count=Count('devices')) + queryset = Platform.objects.annotate( + device_count=Count('devices', distinct=True), + vm_count=Count('virtual_machines', distinct=True) + ) table = tables.PlatformTable template_name = 'dcim/platform_list.html' From 5262156e1a1679adf8c6e971a92b1e291a012dd7 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 19 Jan 2018 10:30:26 -0500 Subject: [PATCH 22/22] Fixes #1818: InventoryItem API serializer no longer requires specifying a null value for items with no parent --- netbox/dcim/api/serializers.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 8f6b3ada8..5204e6a0e 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -733,6 +733,8 @@ class InventoryItemSerializer(serializers.ModelSerializer): class WritableInventoryItemSerializer(ValidatedModelSerializer): + # Provide a default value to satisfy UniqueTogetherValidator + parent = serializers.PrimaryKeyRelatedField(queryset=InventoryItem.objects.all(), allow_null=True, default=None) class Meta: model = InventoryItem