From 8f5e73a5987ae5bdb83b84c0d87369269a6acb9e Mon Sep 17 00:00:00 2001 From: Daniel Sheppard Date: Fri, 6 Sep 2019 11:42:56 -0500 Subject: [PATCH 01/22] Add filter for has local context data (#3159) * Add filter for has local context data * Broke out filter and form for re-use * Fix missing StaticSelect2 import * Fix missing BOOLEAN_WITH_BLANK_CHOICES import * Fix class resolution * Fix field ordering * Fix PEP8 errors --- netbox/dcim/filters.py | 4 ++-- netbox/dcim/forms.py | 6 ++++-- netbox/extras/filters.py | 14 ++++++++++++++ netbox/extras/forms.py | 17 ++++++++++++++++- 4 files changed, 36 insertions(+), 5 deletions(-) diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index be1b02703..0499dcd59 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -3,7 +3,7 @@ from django.contrib.auth.models import User from django.core.exceptions import ObjectDoesNotExist from django.db.models import Q -from extras.filters import CustomFieldFilterSet +from extras.filters import CustomFieldFilterSet, LocalConfigContextFilter from tenancy.filtersets import TenancyFilterSet from tenancy.models import Tenant from utilities.constants import COLOR_CHOICES @@ -424,7 +424,7 @@ class PlatformFilter(NameSlugSearchFilterSet): fields = ['id', 'name', 'slug', 'napalm_driver'] -class DeviceFilter(TenancyFilterSet, CustomFieldFilterSet): +class DeviceFilter(LocalConfigContextFilter, TenancyFilterSet, CustomFieldFilterSet): id__in = NumericInFilter( field_name='id', lookup_expr='in' diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 4abbcdd71..ed2f40e35 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -13,7 +13,9 @@ from taggit.forms import TagField from timezone_field import TimeZoneFormField from circuits.models import Circuit, Provider -from extras.forms import AddRemoveTagsForm, CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm +from extras.forms import ( + AddRemoveTagsForm, CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm, LocalConfigContextFilterForm +) from ipam.models import IPAddress, VLAN, VLANGroup from tenancy.forms import TenancyForm from tenancy.forms import TenancyFilterForm @@ -1675,7 +1677,7 @@ class DeviceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF ] -class DeviceFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): +class DeviceFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilterForm, CustomFieldFilterForm): model = Device field_order = [ 'q', 'region', 'site', 'rack_group_id', 'rack_id', 'status', 'role', 'tenant_group', 'tenant', diff --git a/netbox/extras/filters.py b/netbox/extras/filters.py index 49e879fe4..b31271230 100644 --- a/netbox/extras/filters.py +++ b/netbox/extras/filters.py @@ -207,6 +207,20 @@ class ConfigContextFilter(django_filters.FilterSet): ) +# +# Filter for Local Config Context Data +# + +class LocalConfigContextFilter(django_filters.FilterSet): + local_context_data = django_filters.BooleanFilter( + method='_local_context_data', + label='Has local config context data', + ) + + def _local_context_data(self, queryset, name, value): + return queryset.exclude(local_context_data__isnull=value) + + class ObjectChangeFilter(django_filters.FilterSet): q = django_filters.CharFilter( method='search', diff --git a/netbox/extras/forms.py b/netbox/extras/forms.py index b06d53423..19f55c345 100644 --- a/netbox/extras/forms.py +++ b/netbox/extras/forms.py @@ -11,7 +11,8 @@ from tenancy.models import Tenant, TenantGroup from utilities.constants import COLOR_CHOICES from utilities.forms import ( add_blank_choice, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ColorSelect, - CommentField, ContentTypeSelect, FilterChoiceField, LaxURLField, JSONField, SlugField, + CommentField, ContentTypeSelect, FilterChoiceField, LaxURLField, JSONField, SlugField, StaticSelect2, + BOOLEAN_WITH_BLANK_CHOICES, ) from .constants import ( CF_FILTER_DISABLED, CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT, CF_TYPE_URL, @@ -349,6 +350,20 @@ class ConfigContextFilterForm(BootstrapMixin, forms.Form): ) +# +# Filter form for local config context data +# + +class LocalConfigContextFilterForm(forms.Form): + local_context_data = forms.NullBooleanField( + required=False, + label='Has local config context data', + widget=StaticSelect2( + choices=BOOLEAN_WITH_BLANK_CHOICES + ) + ) + + # # Image attachments # From 9c6dbd73376d0596d906686c5f3f287f28137d5f Mon Sep 17 00:00:00 2001 From: Daniel Sheppard Date: Fri, 6 Sep 2019 12:45:37 -0500 Subject: [PATCH 02/22] Add in in-line vlan editing and Bulk vlan editing (#3350) * Fixes #3341 - Added in-line vlan editing * Fixes #2160 - Added bulk vlan editing Inconsequential behaviour changes: * APISelect can now take "full=True" to return a non-brief set * Select2 will no group by "group & site, group, site, global" if full=True is set in APISelect --- netbox/dcim/forms.py | 186 +++++++++------------- netbox/dcim/urls.py | 1 - netbox/dcim/views.py | 6 - netbox/project-static/js/forms.js | 73 +++++++-- netbox/templates/dcim/interface_edit.html | 34 +--- netbox/utilities/forms.py | 3 + netbox/utilities/views.py | 9 +- 7 files changed, 155 insertions(+), 157 deletions(-) diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index ed2f40e35..2a37de4fc 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -56,6 +56,25 @@ def get_device_by_name_or_pk(name): return device +class InterfaceCommonForm: + def clean(self): + + super().clean() + + # Validate VLAN assignments + tagged_vlans = self.cleaned_data['tagged_vlans'] + + # Untagged interfaces cannot be assigned tagged VLANs + if self.cleaned_data['mode'] == IFACE_MODE_ACCESS and tagged_vlans: + raise forms.ValidationError({ + 'mode': "An access interface cannot have tagged VLANs assigned." + }) + + # Remove all tagged VLAN assignments from "tagged all" interfaces + elif self.cleaned_data['mode'] == IFACE_MODE_TAGGED_ALL: + self.cleaned_data['tagged_vlans'] = [] + + class BulkRenameForm(forms.Form): """ An extendable form to be used for renaming device components in bulk. @@ -2110,7 +2129,26 @@ class PowerOutletBulkDisconnectForm(ConfirmationForm): # Interfaces # -class InterfaceForm(BootstrapMixin, forms.ModelForm): +class InterfaceForm(InterfaceCommonForm, BootstrapMixin, forms.ModelForm): + untagged_vlan = forms.ModelChoiceField( + queryset=VLAN.objects.all(), + required=False, + widget=APISelect( + api_url="/api/ipam/vlans/", + display_field='display_name', + full=True + ) + ) + tagged_vlans = forms.ModelMultipleChoiceField( + queryset=VLAN.objects.all(), + required=False, + widget=APISelectMultiple( + api_url="/api/ipam/vlans/", + display_field='display_name', + full=True + ) + ) + tags = TagField( required=False ) @@ -2149,112 +2187,8 @@ class InterfaceForm(BootstrapMixin, forms.ModelForm): device__in=[self.instance.device, self.instance.device.get_vc_master()], type=IFACE_TYPE_LAG ) - def clean(self): - super().clean() - - # Validate VLAN assignments - tagged_vlans = self.cleaned_data['tagged_vlans'] - - # Untagged interfaces cannot be assigned tagged VLANs - if self.cleaned_data['mode'] == IFACE_MODE_ACCESS and tagged_vlans: - raise forms.ValidationError({ - 'mode': "An access interface cannot have tagged VLANs assigned." - }) - - # Remove all tagged VLAN assignments from "tagged all" interfaces - elif self.cleaned_data['mode'] == IFACE_MODE_TAGGED_ALL: - self.cleaned_data['tagged_vlans'] = [] - - -class InterfaceAssignVLANsForm(BootstrapMixin, forms.ModelForm): - vlans = forms.MultipleChoiceField( - choices=[], - label='VLANs', - widget=StaticSelect2Multiple( - attrs={ - 'size': 20, - } - ) - ) - tagged = forms.BooleanField( - required=False, - initial=True - ) - - class Meta: - model = Interface - fields = [] - - def __init__(self, *args, **kwargs): - - super().__init__(*args, **kwargs) - - if self.instance.mode == IFACE_MODE_ACCESS: - self.initial['tagged'] = False - - # Find all VLANs already assigned to the interface for exclusion from the list - assigned_vlans = [v.pk for v in self.instance.tagged_vlans.all()] - if self.instance.untagged_vlan is not None: - assigned_vlans.append(self.instance.untagged_vlan.pk) - - # Compile VLAN choices - vlan_choices = [] - - # Add non-grouped global VLANs - global_vlans = VLAN.objects.filter(site=None, group=None).exclude(pk__in=assigned_vlans) - vlan_choices.append( - ('Global', [(vlan.pk, vlan) for vlan in global_vlans]) - ) - - # Add grouped global VLANs - for group in VLANGroup.objects.filter(site=None): - global_group_vlans = VLAN.objects.filter(group=group).exclude(pk__in=assigned_vlans) - vlan_choices.append( - (group.name, [(vlan.pk, vlan) for vlan in global_group_vlans]) - ) - - site = getattr(self.instance.parent, 'site', None) - if site is not None: - - # Add non-grouped site VLANs - site_vlans = VLAN.objects.filter(site=site, group=None).exclude(pk__in=assigned_vlans) - vlan_choices.append((site.name, [(vlan.pk, vlan) for vlan in site_vlans])) - - # Add grouped site VLANs - for group in VLANGroup.objects.filter(site=site): - site_group_vlans = VLAN.objects.filter(group=group).exclude(pk__in=assigned_vlans) - vlan_choices.append(( - '{} / {}'.format(group.site.name, group.name), - [(vlan.pk, vlan) for vlan in site_group_vlans] - )) - - self.fields['vlans'].choices = vlan_choices - - def clean(self): - - super().clean() - - # Only untagged VLANs permitted on an access interface - if self.instance.mode == IFACE_MODE_ACCESS and len(self.cleaned_data['vlans']) > 1: - raise forms.ValidationError("Only one VLAN may be assigned to an access interface.") - - # 'tagged' is required if more than one VLAN is selected - if not self.cleaned_data['tagged'] and len(self.cleaned_data['vlans']) > 1: - raise forms.ValidationError("Only one untagged VLAN may be selected.") - - def save(self, *args, **kwargs): - - if self.cleaned_data['tagged']: - for vlan in self.cleaned_data['vlans']: - self.instance.tagged_vlans.add(vlan) - else: - self.instance.untagged_vlan_id = self.cleaned_data['vlans'][0] - - return super().save(*args, **kwargs) - - -class InterfaceCreateForm(ComponentForm, forms.Form): +class InterfaceCreateForm(InterfaceCommonForm, ComponentForm, forms.Form): name_pattern = ExpandableNameField( label='Name' ) @@ -2298,6 +2232,24 @@ class InterfaceCreateForm(ComponentForm, forms.Form): tags = TagField( required=False ) + untagged_vlan = forms.ModelChoiceField( + queryset=VLAN.objects.all(), + required=False, + widget=APISelect( + api_url="/api/ipam/vlans/", + display_field='display_name', + full=True + ) + ) + tagged_vlans = forms.ModelMultipleChoiceField( + queryset=VLAN.objects.all(), + required=False, + widget=APISelectMultiple( + api_url="/api/ipam/vlans/", + display_field='display_name', + full=True + ) + ) def __init__(self, *args, **kwargs): @@ -2316,7 +2268,7 @@ class InterfaceCreateForm(ComponentForm, forms.Form): self.fields['lag'].queryset = Interface.objects.none() -class InterfaceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm): +class InterfaceBulkEditForm(InterfaceCommonForm, BootstrapMixin, AddRemoveTagsForm, BulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=Interface.objects.all(), widget=forms.MultipleHiddenInput() @@ -2360,10 +2312,28 @@ class InterfaceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm): required=False, widget=StaticSelect2() ) + untagged_vlan = forms.ModelChoiceField( + queryset=VLAN.objects.all(), + required=False, + widget=APISelect( + api_url="/api/ipam/vlans/", + display_field='display_name', + full=True + ) + ) + tagged_vlans = forms.ModelMultipleChoiceField( + queryset=VLAN.objects.all(), + required=False, + widget=APISelectMultiple( + api_url="/api/ipam/vlans/", + display_field='display_name', + full=True + ) + ) class Meta: nullable_fields = [ - 'lag', 'mac_address', 'mtu', 'description', 'mode', + 'lag', 'mac_address', 'mtu', 'description', 'mode', 'untagged_vlan', 'tagged_vlans' ] def __init__(self, *args, **kwargs): diff --git a/netbox/dcim/urls.py b/netbox/dcim/urls.py index ae1f05757..43316baf4 100644 --- a/netbox/dcim/urls.py +++ b/netbox/dcim/urls.py @@ -209,7 +209,6 @@ urlpatterns = [ path(r'interfaces//connect//', views.CableCreateView.as_view(), name='interface_connect', kwargs={'termination_a_type': Interface}), path(r'interfaces//', views.InterfaceView.as_view(), name='interface'), path(r'interfaces//edit/', views.InterfaceEditView.as_view(), name='interface_edit'), - path(r'interfaces//assign-vlans/', views.InterfaceAssignVLANsView.as_view(), name='interface_assign_vlans'), path(r'interfaces//delete/', views.InterfaceDeleteView.as_view(), name='interface_delete'), path(r'interfaces//changelog/', ObjectChangeLogView.as_view(), name='interface_changelog', kwargs={'model': Interface}), path(r'interfaces//trace/', views.CableTraceView.as_view(), name='interface_trace', kwargs={'model': Interface}), diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index fe98c93f3..f953f95c2 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -1348,12 +1348,6 @@ class InterfaceEditView(PermissionRequiredMixin, ObjectEditView): template_name = 'dcim/interface_edit.html' -class InterfaceAssignVLANsView(PermissionRequiredMixin, ObjectEditView): - permission_required = 'dcim.change_interface' - model = Interface - model_form = forms.InterfaceAssignVLANsForm - - class InterfaceDeleteView(PermissionRequiredMixin, ObjectDeleteView): permission_required = 'dcim.delete_interface' model = Interface diff --git a/netbox/project-static/js/forms.js b/netbox/project-static/js/forms.js index b1a08c7f4..2469c0f8d 100644 --- a/netbox/project-static/js/forms.js +++ b/netbox/project-static/js/forms.js @@ -143,11 +143,13 @@ $(document).ready(function() { // Base query params var parameters = { q: params.term, - brief: 1, limit: 50, offset: offset, }; + // Allow for controlling the brief setting from within APISelect + parameters.brief = ( $('#id_untagged_vlan').is('[data-full]') ? undefined : true ); + // filter-for fields from a chain var attr_name = "data-filter-for-" + $(element).attr("name"); var form = $(element).closest('form'); @@ -194,18 +196,41 @@ $(document).ready(function() { processResults: function (data) { var element = this.$element[0]; - // Clear any disabled options $(element).children('option').attr('disabled', false); - var results = $.map(data.results, function (obj) { - obj.text = obj[element.getAttribute('display-field')] || obj.name; - obj.id = obj[element.getAttribute('value-field')] || obj.id; + var results = data.results; - if(element.getAttribute('disabled-indicator') && obj[element.getAttribute('disabled-indicator')]) { + results = results.reduce((results,record) => { + record.text = record[element.getAttribute('display-field')] || record.name; + record.id = record[element.getAttribute('value-field')] || record.id; + if(element.getAttribute('disabled-indicator') && record[element.getAttribute('disabled-indicator')]) { // The disabled-indicator equated to true, so we disable this option - obj.disabled = true; + record.disabled = true; } - return obj; - }); + + if( record.group !== undefined && record.group !== null && record.site !== undefined && record.site !== null ) { + results[record.site.name + ":" + record.group.name] = results[record.site.name + ":" + record.group.name] || { text: record.site.name + " / " + record.group.name, children: [] } + results[record.site.name + ":" + record.group.name].children.push(record); + } + else if( record.group !== undefined && record.group !== null ) { + results[record.group.name] = results[record.group.name] || { text: record.group.name, children: [] } + results[record.group.name].children.push(record); + } + else if( record.site !== undefined && record.site !== null ) { + results[record.site.name] = results[record.site.name] || { text: record.site.name, children: [] } + results[record.site.name].children.push(record); + } + else if ( (record.group !== undefined || record.group == null) && (record.site !== undefined || record.site === null) ) { + results['global'] = results['global'] || { text: 'Global', children: [] } + results['global'].children.push(record); + } + else { + results[record.id] = record + } + + return results; + },Object.create(null)); + + results = Object.values(results); // Handle the null option, but only add it once if (element.getAttribute('data-null-option') && data.previous === null) { @@ -300,4 +325,34 @@ $(document).ready(function() { $('#id_tags').append(option).trigger('change'); } }); + + if( $('select#id_mode').length > 0 ) { + $('select#id_mode').on('change', function () { + if ($(this).val() == '') { + $('select#id_untagged_vlan').val(); + $('select#id_untagged_vlan').trigger('change'); + $('select#id_tagged_vlans').val([]); + $('select#id_tagged_vlans').trigger('change'); + $('select#id_untagged_vlan').parent().parent().hide(); + $('select#id_tagged_vlans').parent().parent().hide(); + } + else if ($(this).val() == 100) { + $('select#id_tagged_vlans').val([]); + $('select#id_tagged_vlans').trigger('change'); + $('select#id_untagged_vlan').parent().parent().show(); + $('select#id_tagged_vlans').parent().parent().hide(); + } + else if ($(this).val() == 200) { + $('select#id_untagged_vlan').parent().parent().show(); + $('select#id_tagged_vlans').parent().parent().show(); + } + else if ($(this).val() == 300) { + $('select#id_tagged_vlans').val([]); + $('select#id_tagged_vlans').trigger('change'); + $('select#id_untagged_vlan').parent().parent().show(); + $('select#id_tagged_vlans').parent().parent().hide(); + } + }); + $('select#id_mode').trigger('change'); + } }); diff --git a/netbox/templates/dcim/interface_edit.html b/netbox/templates/dcim/interface_edit.html index fedb1b61a..a80b7c592 100644 --- a/netbox/templates/dcim/interface_edit.html +++ b/netbox/templates/dcim/interface_edit.html @@ -14,6 +14,8 @@ {% render_field form.mgmt_only %} {% render_field form.description %} {% render_field form.mode %} + {% render_field form.untagged_vlan %} + {% render_field form.tagged_vlans %}
@@ -22,21 +24,6 @@ {% render_field form.tags %}
-
-
802.1Q VLANs
- {% if obj.mode %} - {% include 'dcim/inc/interface_vlans_table.html' %} - - {% else %} -
-

802.1Q mode not set

-
- {% endif %} -
{% endblock %} {% block buttons %} @@ -48,19 +35,4 @@ {% endif %} Cancel -{% endblock %} - -{% block javascript %} - -{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/netbox/utilities/forms.py b/netbox/utilities/forms.py index 1d4671bf6..aeba4d53d 100644 --- a/netbox/utilities/forms.py +++ b/netbox/utilities/forms.py @@ -298,6 +298,7 @@ class APISelect(SelectWithDisabled): conditional_query_params=None, additional_query_params=None, null_option=False, + full=False, *args, **kwargs ): @@ -306,6 +307,8 @@ class APISelect(SelectWithDisabled): self.attrs['class'] = 'netbox-select2-api' self.attrs['data-url'] = '/{}{}'.format(settings.BASE_PATH, api_url.lstrip('/')) # Inject BASE_PATH + if full: + self.attrs['data-full'] = full if display_field: self.attrs['display-field'] = display_field if value_field: diff --git a/netbox/utilities/views.py b/netbox/utilities/views.py index bbc58f134..1aa358fba 100644 --- a/netbox/utilities/views.py +++ b/netbox/utilities/views.py @@ -7,6 +7,7 @@ from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ValidationError from django.db import transaction, IntegrityError from django.db.models import Count, ProtectedError +from django.db.models.query import QuerySet from django.forms import CharField, Form, ModelMultipleChoiceField, MultipleHiddenInput, Textarea from django.http import HttpResponse, HttpResponseServerError from django.shortcuts import get_object_or_404, redirect, render @@ -530,9 +531,13 @@ class BulkEditView(GetReturnURLMixin, View): # Update standard fields. If a field is listed in _nullify, delete its value. for name in standard_fields: - if name in form.nullable_fields and name in nullified_fields: + if name in form.nullable_fields and name in nullified_fields and isinstance(form.cleaned_data[name], QuerySet): + getattr(obj, name).set([]) + elif name in form.nullable_fields and name in nullified_fields: setattr(obj, name, '' if isinstance(form.fields[name], CharField) else None) - elif form.cleaned_data[name] not in (None, ''): + elif isinstance(form.cleaned_data[name], QuerySet) and form.cleaned_data[name]: + getattr(obj, name).set(form.cleaned_data[name]) + elif form.cleaned_data[name] not in (None, '') and not isinstance(form.cleaned_data[name], QuerySet): setattr(obj, name, form.cleaned_data[name]) obj.full_clean() obj.save() From 050f2478d3692467c8ec152645e651d7d8ac99fa Mon Sep 17 00:00:00 2001 From: Daniel Sheppard Date: Fri, 6 Sep 2019 13:01:27 -0500 Subject: [PATCH 03/22] Fixes: #3318 - Increases length of platform name and slug to 64 characters (#3353) --- netbox/dcim/forms.py | 4 +++- ...ncrease_field_length_platform_name_slug.py | 23 +++++++++++++++++++ netbox/dcim/models.py | 5 ++-- netbox/project-static/js/forms.js | 3 ++- 4 files changed, 31 insertions(+), 4 deletions(-) create mode 100644 netbox/dcim/migrations/0074_increase_field_length_platform_name_slug.py diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 2a37de4fc..774b69741 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -1242,7 +1242,9 @@ class DeviceRoleCSVForm(forms.ModelForm): # class PlatformForm(BootstrapMixin, forms.ModelForm): - slug = SlugField() + slug = SlugField( + max_length=64 + ) class Meta: model = Platform diff --git a/netbox/dcim/migrations/0074_increase_field_length_platform_name_slug.py b/netbox/dcim/migrations/0074_increase_field_length_platform_name_slug.py new file mode 100644 index 000000000..4fd8f203c --- /dev/null +++ b/netbox/dcim/migrations/0074_increase_field_length_platform_name_slug.py @@ -0,0 +1,23 @@ +# Generated by Django 2.2 on 2019-07-17 20:40 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dcim', '0073_interface_form_factor_to_type'), + ] + + operations = [ + migrations.AlterField( + model_name='platform', + name='name', + field=models.CharField(max_length=64, unique=True), + ), + migrations.AlterField( + model_name='platform', + name='slug', + field=models.SlugField(max_length=64, unique=True), + ), + ] diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index 0a73abd39..29384abcd 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -1385,11 +1385,12 @@ class Platform(ChangeLoggedModel): specifying a NAPALM driver. """ name = models.CharField( - max_length=50, + max_length=64, unique=True ) slug = models.SlugField( - unique=True + unique=True, + max_length=64 ) manufacturer = models.ForeignKey( to='dcim.Manufacturer', diff --git a/netbox/project-static/js/forms.js b/netbox/project-static/js/forms.js index 2469c0f8d..c82529f27 100644 --- a/netbox/project-static/js/forms.js +++ b/netbox/project-static/js/forms.js @@ -47,9 +47,10 @@ $(document).ready(function() { }); if (slug_field) { var slug_source = $('#id_' + slug_field.attr('slug-source')); + var slug_length = slug_field.attr('maxlength'); slug_source.on('keyup change', function() { if (slug_field && !slug_field.attr('_changed')) { - slug_field.val(slugify($(this).val(), 50)); + slug_field.val(slugify($(this).val(), (slug_length ? slug_length : 50))); } }) } From e67d4fb2e58454948dc427ca31604a6330d8144a Mon Sep 17 00:00:00 2001 From: Daniel Sheppard Date: Fri, 6 Sep 2019 13:32:21 -0500 Subject: [PATCH 04/22] Update Changelog with Future Changes --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dff8168c2..c4f700c78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +v2.6.4 (FUTURE) + +## Enhancements + +* [#2160](https://github.com/netbox-community/netbox/issues/2160) - Add Bulk Interface Vlan Editing +* [#3027](https://github.com/netbox-community/netbox/issues/3028) - Add filter for has local context data +* [#3318](https://github.com/netbox-community/netbox/issues/3318) - Increase length of platform name and slug to 64 characters +* [#3341](https://github.com/netbox-community/netbox/issues/3341) - Add Inline Vlan Editing + v2.6.3 (2019-09-04) ## New Features From 355910e182abe412b2a1b3ac46204dde69e1eb67 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 9 Sep 2019 15:50:10 -0400 Subject: [PATCH 05/22] Fixes #3489: Prevent exception triggered by webhook upon object deletion --- CHANGELOG.md | 4 ++++ netbox/extras/middleware.py | 34 ++++++++++++++++++++++------------ 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c4f700c78..0a035266d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ v2.6.4 (FUTURE) * [#3318](https://github.com/netbox-community/netbox/issues/3318) - Increase length of platform name and slug to 64 characters * [#3341](https://github.com/netbox-community/netbox/issues/3341) - Add Inline Vlan Editing +## Bug Fixes + +* [#3489](https://github.com/netbox-community/netbox/issues/3489) - Prevent exception triggered by webhook upon object deletion + v2.6.3 (2019-09-04) ## New Features diff --git a/netbox/extras/middleware.py b/netbox/extras/middleware.py index 35a6f96b0..5083af8ec 100644 --- a/netbox/extras/middleware.py +++ b/netbox/extras/middleware.py @@ -6,6 +6,7 @@ from datetime import timedelta from django.conf import settings from django.db.models.signals import post_delete, post_save from django.utils import timezone +from django.utils.functional import curry from django_prometheus.models import model_deletes, model_inserts, model_updates from .constants import ( @@ -18,10 +19,11 @@ from .webhooks import enqueue_webhooks _thread_locals = threading.local() -def cache_changed_object(sender, instance, **kwargs): +def handle_changed_object(sender, instance, **kwargs): """ - Cache an object being created or updated for the changelog. + Fires when an object is created or updated """ + # Queue the object and a new ObjectChange for processing once the request completes if hasattr(instance, 'to_objectchange'): action = OBJECTCHANGE_ACTION_CREATE if kwargs['created'] else OBJECTCHANGE_ACTION_UPDATE objectchange = instance.to_objectchange(action) @@ -30,15 +32,22 @@ def cache_changed_object(sender, instance, **kwargs): ) -def cache_deleted_object(sender, instance, **kwargs): +def _handle_deleted_object(request, sender, instance, **kwargs): """ - Cache an object being deleted for the changelog. + Fires when an object is deleted """ + # Record an Object Change if hasattr(instance, 'to_objectchange'): objectchange = instance.to_objectchange(OBJECTCHANGE_ACTION_DELETE) - _thread_locals.changed_objects.append( - (instance, objectchange) - ) + objectchange.user = request.user + objectchange.request_id = request.id + objectchange.save() + + # Enqueue webhooks + enqueue_webhooks(instance, request.user, request.id, OBJECTCHANGE_ACTION_DELETE) + + # Increment metric counters + model_deletes.labels(instance._meta.model_name).inc() def purge_objectchange_cache(sender, **kwargs): @@ -54,7 +63,7 @@ class ObjectChangeMiddleware(object): 1. Create an ObjectChange to reflect the modification to the object in the changelog. 2. Enqueue any relevant webhooks. - 3. Increment metric counter for the event type + 3. Increment the metric counter for the event type. The post_save and post_delete signals are employed to catch object modifications, however changes are recorded a bit differently for each. Objects being saved are cached into thread-local storage for action *after* the response has @@ -74,9 +83,12 @@ class ObjectChangeMiddleware(object): # the same request. request.id = uuid.uuid4() + # Signals don't include the request context, so we're currying it into the post_delete function ahead of time. + handle_deleted_object = curry(_handle_deleted_object, request) + # Connect our receivers to the post_save and post_delete signals. - post_save.connect(cache_changed_object, dispatch_uid='cache_changed_object') - post_delete.connect(cache_deleted_object, dispatch_uid='cache_deleted_object') + post_save.connect(handle_changed_object, dispatch_uid='cache_changed_object') + post_delete.connect(handle_deleted_object, dispatch_uid='cache_deleted_object') # Provide a hook for purging the change cache purge_changelog.connect(purge_objectchange_cache) @@ -104,8 +116,6 @@ class ObjectChangeMiddleware(object): model_inserts.labels(obj._meta.model_name).inc() elif objectchange.action == OBJECTCHANGE_ACTION_UPDATE: model_updates.labels(obj._meta.model_name).inc() - elif objectchange.action == OBJECTCHANGE_ACTION_DELETE: - model_deletes.labels(obj._meta.model_name).inc() # Housekeeping: 1% chance of clearing out expired ObjectChanges. This applies only to requests which result in # one or more changes being logged. From f5335306939d8c95adbd4067d5d3278a12d248cb Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 10 Sep 2019 15:30:19 -0400 Subject: [PATCH 06/22] Moved related projects list to the wiki --- README.md | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/README.md b/README.md index 04b2ff830..dc673221d 100644 --- a/README.md +++ b/README.md @@ -43,15 +43,4 @@ and run `upgrade.sh`. # Related projects -## Supported SDK - -- [pynetbox](https://github.com/digitalocean/pynetbox) - A Python API client library for Netbox - -## Community SDK - -- [netbox-client-ruby](https://github.com/ninech/netbox-client-ruby) - A Ruby client library for Netbox -- [powerbox](https://github.com/BatmanAMA/powerbox) - A PowerShell library for Netbox - -## Ansible Inventory - -- [netbox-as-ansible-inventory](https://github.com/AAbouZaid/netbox-as-ansible-inventory) - Ansible dynamic inventory script for Netbox +Please see [our wiki](https://github.com/netbox-community/netbox/wiki/Community-Contributions) for a list of relevant community projects. From 062c65fd67a55fef88c12da32dad469119d7b8fa Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 10 Sep 2019 15:45:54 -0400 Subject: [PATCH 07/22] Changelog cleanup --- CHANGELOG.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a035266d..c047c3c81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,10 +2,10 @@ v2.6.4 (FUTURE) ## Enhancements -* [#2160](https://github.com/netbox-community/netbox/issues/2160) - Add Bulk Interface Vlan Editing -* [#3027](https://github.com/netbox-community/netbox/issues/3028) - Add filter for has local context data +* [#2160](https://github.com/netbox-community/netbox/issues/2160) - Add bulk editing for interface VLAN assignment +* [#3027](https://github.com/netbox-community/netbox/issues/3028) - Add `local_context_data` boolean filter for devices * [#3318](https://github.com/netbox-community/netbox/issues/3318) - Increase length of platform name and slug to 64 characters -* [#3341](https://github.com/netbox-community/netbox/issues/3341) - Add Inline Vlan Editing +* [#3341](https://github.com/netbox-community/netbox/issues/3341) - Enable inline VLAN assignment while editing an interface ## Bug Fixes @@ -21,15 +21,6 @@ Custom scripts allow for the execution of arbitrary code via the NetBox UI. They Note: There are currently no API endpoints for this feature. These are planned for the upcoming v2.7 release. -## Bug Fixes - -* [#3392](https://github.com/netbox-community/netbox/issues/3392) - Add database index for ObjectChange time -* [#3420](https://github.com/netbox-community/netbox/issues/3420) - Serial number filter for racks, devices, and inventory items is now case-insensitive -* [#3428](https://github.com/netbox-community/netbox/issues/3428) - Fixed cache invalidation issues ([#3300](https://github.com/netbox-community/netbox/issues/3300), [#3363](https://github.com/netbox-community/netbox/issues/3363), [#3379](https://github.com/netbox-community/netbox/issues/3379), [#3382](https://github.com/netbox-community/netbox/issues/3382)) by switching to `prefetch_related()` instead of `select_related()` and removing use of `update()` -* [#3421](https://github.com/netbox-community/netbox/issues/3421) - Fix exception when ordering power connections list by PDU -* [#3424](https://github.com/netbox-community/netbox/issues/3424) - Fix tag coloring for non-linked tags -* [#3426](https://github.com/netbox-community/netbox/issues/3426) - Improve API error handling for ChoiceFields - ## Enhancements * [#3386](https://github.com/netbox-community/netbox/issues/3386) - Add `mac_address` filter for virtual machines @@ -40,6 +31,15 @@ Note: There are currently no API endpoints for this feature. These are planned f * [#3454](https://github.com/netbox-community/netbox/issues/3454) - Enable filtering circuits by region * [#3456](https://github.com/netbox-community/netbox/issues/3456) - Enable bulk editing of tag color +## Bug Fixes + +* [#3392](https://github.com/netbox-community/netbox/issues/3392) - Add database index for ObjectChange time +* [#3420](https://github.com/netbox-community/netbox/issues/3420) - Serial number filter for racks, devices, and inventory items is now case-insensitive +* [#3428](https://github.com/netbox-community/netbox/issues/3428) - Fixed cache invalidation issues ([#3300](https://github.com/netbox-community/netbox/issues/3300), [#3363](https://github.com/netbox-community/netbox/issues/3363), [#3379](https://github.com/netbox-community/netbox/issues/3379), [#3382](https://github.com/netbox-community/netbox/issues/3382)) by switching to `prefetch_related()` instead of `select_related()` and removing use of `update()` +* [#3421](https://github.com/netbox-community/netbox/issues/3421) - Fix exception when ordering power connections list by PDU +* [#3424](https://github.com/netbox-community/netbox/issues/3424) - Fix tag coloring for non-linked tags +* [#3426](https://github.com/netbox-community/netbox/issues/3426) - Improve API error handling for ChoiceFields + --- v2.6.2 (2019-08-02) From a8ca536d44cafe2b799033ee0b413a18ef91511e Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 10 Sep 2019 15:50:35 -0400 Subject: [PATCH 08/22] Bump platform name/slug max length to 100 chars (#3318) --- CHANGELOG.md | 2 +- .../0074_increase_field_length_platform_name_slug.py | 4 ++-- netbox/dcim/models.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c047c3c81..f05fac2be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ v2.6.4 (FUTURE) * [#2160](https://github.com/netbox-community/netbox/issues/2160) - Add bulk editing for interface VLAN assignment * [#3027](https://github.com/netbox-community/netbox/issues/3028) - Add `local_context_data` boolean filter for devices -* [#3318](https://github.com/netbox-community/netbox/issues/3318) - Increase length of platform name and slug to 64 characters +* [#3318](https://github.com/netbox-community/netbox/issues/3318) - Increase length of platform name and slug to 100 characters * [#3341](https://github.com/netbox-community/netbox/issues/3341) - Enable inline VLAN assignment while editing an interface ## Bug Fixes diff --git a/netbox/dcim/migrations/0074_increase_field_length_platform_name_slug.py b/netbox/dcim/migrations/0074_increase_field_length_platform_name_slug.py index 4fd8f203c..2c8a2255c 100644 --- a/netbox/dcim/migrations/0074_increase_field_length_platform_name_slug.py +++ b/netbox/dcim/migrations/0074_increase_field_length_platform_name_slug.py @@ -13,11 +13,11 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='platform', name='name', - field=models.CharField(max_length=64, unique=True), + field=models.CharField(max_length=100, unique=True), ), migrations.AlterField( model_name='platform', name='slug', - field=models.SlugField(max_length=64, unique=True), + field=models.SlugField(max_length=100, unique=True), ), ] diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index 29384abcd..88d2aee1d 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -1385,12 +1385,12 @@ class Platform(ChangeLoggedModel): specifying a NAPALM driver. """ name = models.CharField( - max_length=64, + max_length=100, unique=True ) slug = models.SlugField( unique=True, - max_length=64 + max_length=100 ) manufacturer = models.ForeignKey( to='dcim.Manufacturer', From 73065fa6e73ca7af2a24c0b5d0a06108929663ba Mon Sep 17 00:00:00 2001 From: Daniel Sheppard Date: Wed, 11 Sep 2019 10:10:43 -0500 Subject: [PATCH 09/22] Using static element to determine brief parameter, corrected to $(element) --- netbox/project-static/js/forms.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/netbox/project-static/js/forms.js b/netbox/project-static/js/forms.js index c82529f27..287c19465 100644 --- a/netbox/project-static/js/forms.js +++ b/netbox/project-static/js/forms.js @@ -75,7 +75,7 @@ $(document).ready(function() { var rendered_url = url; var filter_field; while (match = filter_regex.exec(url)) { - filter_field = $('#id_' + match[1]); + filter_field = $('#id_' + match[1]);untagged var custom_attr = $('option:selected', filter_field).attr('api-value'); if (custom_attr) { rendered_url = rendered_url.replace(match[0], custom_attr); @@ -149,7 +149,7 @@ $(document).ready(function() { }; // Allow for controlling the brief setting from within APISelect - parameters.brief = ( $('#id_untagged_vlan').is('[data-full]') ? undefined : true ); + parameters.brief = ( $(element).is('[data-full]') ? undefined : true ); // filter-for fields from a chain var attr_name = "data-filter-for-" + $(element).attr("name"); From 57d35181f086a676256b931984cc79a938d1508b Mon Sep 17 00:00:00 2001 From: Daniel Sheppard Date: Thu, 12 Sep 2019 11:13:40 -0500 Subject: [PATCH 10/22] Fix performance issues when creating/editing interfaces due to unfiltered vlan queryset --- netbox/dcim/forms.py | 91 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 774b69741..cdb4cfa4d 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -2189,6 +2189,36 @@ class InterfaceForm(InterfaceCommonForm, BootstrapMixin, forms.ModelForm): device__in=[self.instance.device, self.instance.device.get_vc_master()], type=IFACE_TYPE_LAG ) + # Limit VLan choices to those in: global vlans, global groups, the current site's group, the current site + vlan_choices = [] + global_vlans = VLAN.objects.filter(site=None, group=None) + vlan_choices.append( + ('Global', [(vlan.pk, vlan) for vlan in global_vlans]) + ) + for group in VLANGroup.objects.filter(site=None): + global_group_vlans = VLAN.objects.filter(group=group) + vlan_choices.append( + (group.name, [(vlan.pk, vlan) for vlan in global_group_vlans]) + ) + + site = getattr(self.instance.device, 'site', None) + if site is not None: + + # Add non-grouped site VLANs + site_vlans = VLAN.objects.filter(site=site, group=None) + vlan_choices.append((site.name, [(vlan.pk, vlan) for vlan in site_vlans])) + + # Add grouped site VLANs + for group in VLANGroup.objects.filter(site=site): + site_group_vlans = VLAN.objects.filter(group=group) + vlan_choices.append(( + '{} / {}'.format(group.site.name, group.name), + [(vlan.pk, vlan) for vlan in site_group_vlans] + )) + + self.fields['untagged_vlan'].choices = vlan_choices + self.fields['tagged_vlans'].choices = vlan_choices + class InterfaceCreateForm(InterfaceCommonForm, ComponentForm, forms.Form): name_pattern = ExpandableNameField( @@ -2269,6 +2299,37 @@ class InterfaceCreateForm(InterfaceCommonForm, ComponentForm, forms.Form): else: self.fields['lag'].queryset = Interface.objects.none() + # Limit VLan choices to those in: global vlans, global groups, the current site's group, the current site + vlan_choices = [] + global_vlans = VLAN.objects.filter(site=None, group=None) + vlan_choices.append( + ('Global', [(vlan.pk, vlan) for vlan in global_vlans]) + ) + for group in VLANGroup.objects.filter(site=None): + global_group_vlans = VLAN.objects.filter(group=group) + vlan_choices.append( + (group.name, [(vlan.pk, vlan) for vlan in global_group_vlans]) + ) + + site = getattr(self.parent, 'site', None) + if site is not None: + + # Add non-grouped site VLANs + site_vlans = VLAN.objects.filter(site=site, group=None) + vlan_choices.append((site.name, [(vlan.pk, vlan) for vlan in site_vlans])) + + # Add grouped site VLANs + for group in VLANGroup.objects.filter(site=site): + site_group_vlans = VLAN.objects.filter(group=group) + vlan_choices.append(( + '{} / {}'.format(group.site.name, group.name), + [(vlan.pk, vlan) for vlan in site_group_vlans] + )) + + self.fields['untagged_vlan'].choices = vlan_choices + self.fields['tagged_vlans'].choices = vlan_choices + + class InterfaceBulkEditForm(InterfaceCommonForm, BootstrapMixin, AddRemoveTagsForm, BulkEditForm): pk = forms.ModelMultipleChoiceField( @@ -2351,6 +2412,36 @@ class InterfaceBulkEditForm(InterfaceCommonForm, BootstrapMixin, AddRemoveTagsFo else: self.fields['lag'].choices = [] + # Limit VLan choices to those in: global vlans, global groups, the current site's group, the current site + vlan_choices = [] + global_vlans = VLAN.objects.filter(site=None, group=None) + vlan_choices.append( + ('Global', [(vlan.pk, vlan) for vlan in global_vlans]) + ) + for group in VLANGroup.objects.filter(site=None): + global_group_vlans = VLAN.objects.filter(group=group) + vlan_choices.append( + (group.name, [(vlan.pk, vlan) for vlan in global_group_vlans]) + ) + if self.parent_obj is not None: + site = getattr(self.parent_obj, 'site', None) + if site is not None: + + # Add non-grouped site VLANs + site_vlans = VLAN.objects.filter(site=site, group=None) + vlan_choices.append((site.name, [(vlan.pk, vlan) for vlan in site_vlans])) + + # Add grouped site VLANs + for group in VLANGroup.objects.filter(site=site): + site_group_vlans = VLAN.objects.filter(group=group) + vlan_choices.append(( + '{} / {}'.format(group.site.name, group.name), + [(vlan.pk, vlan) for vlan in site_group_vlans] + )) + + self.fields['untagged_vlan'].choices = vlan_choices + self.fields['tagged_vlans'].choices = vlan_choices + class InterfaceBulkRenameForm(BulkRenameForm): pk = forms.ModelMultipleChoiceField( From 5962e7c9424a095f8a01eb93131e7d0103cac450 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 13 Sep 2019 11:45:35 -0400 Subject: [PATCH 11/22] Fixes #3501: Fix rendering of checkboxes on custom script forms --- CHANGELOG.md | 1 + netbox/extras/scripts.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f05fac2be..a34a3deec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ v2.6.4 (FUTURE) ## Bug Fixes * [#3489](https://github.com/netbox-community/netbox/issues/3489) - Prevent exception triggered by webhook upon object deletion +* [#3501](https://github.com/netbox-community/netbox/issues/3501) - Fix rendering of checkboxes on custom script forms v2.6.3 (2019-09-04) diff --git a/netbox/extras/scripts.py b/netbox/extras/scripts.py index 9462ee5bd..842133671 100644 --- a/netbox/extras/scripts.py +++ b/netbox/extras/scripts.py @@ -61,7 +61,8 @@ class ScriptVariable: Render the variable as a Django form field. """ form_field = self.form_field(**self.field_attrs) - form_field.widget.attrs['class'] = 'form-control' + if not isinstance(form_field.widget, forms.CheckboxInput): + form_field.widget.attrs['class'] = 'form-control' return form_field From 1a1f6aff7b0c3fd801bfa548fb757b04902767ce Mon Sep 17 00:00:00 2001 From: Daniel Sheppard Date: Fri, 13 Sep 2019 12:08:48 -0500 Subject: [PATCH 12/22] Closes: #3495 --- docs/additional-features/custom-links.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/additional-features/custom-links.md b/docs/additional-features/custom-links.md index 91dd06e30..7c96eba8b 100644 --- a/docs/additional-features/custom-links.md +++ b/docs/additional-features/custom-links.md @@ -24,11 +24,19 @@ Only links which render with non-empty text are included on the page. You can em For example, if you only want to display a link for active devices, you could set the link text to ``` -{% if device.status == 1 %}View NMS{% endif %} +{% if obj.status == 1 %}View NMS{% endif %} ``` The link will not appear when viewing a device with any status other than "active." +Another example, if you want to only show an object of a certain manufacturer, you could set the link text to: + +``` +{% if obj.device_type.manufacturer.name == 'Cisco' %}View NMS {% endif %} +``` + +The link will only appear when viewing a device with a manufacturer name of "Cisco." + ## Link Groups You can specify a group name to organize links into related sets. Grouped links will render as a dropdown menu beneath a From a742d897d74c9b78a50127ec53ce99d3e7f504ed Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 17 Sep 2019 16:36:36 -0400 Subject: [PATCH 13/22] Closes #3510: Add minimum/maximum prefix length enforcement for IPNetworkVar --- CHANGELOG.md | 1 + netbox/extras/scripts.py | 16 ++++++++++++++++ netbox/utilities/validators.py | 18 +++++++++++++++++- 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a34a3deec..346e0effe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ v2.6.4 (FUTURE) * [#3027](https://github.com/netbox-community/netbox/issues/3028) - Add `local_context_data` boolean filter for devices * [#3318](https://github.com/netbox-community/netbox/issues/3318) - Increase length of platform name and slug to 100 characters * [#3341](https://github.com/netbox-community/netbox/issues/3341) - Enable inline VLAN assignment while editing an interface +* [#3510](https://github.com/netbox-community/netbox/issues/3510) - Add minimum/maximum prefix length enforcement for `IPNetworkVar` ## Bug Fixes diff --git a/netbox/extras/scripts.py b/netbox/extras/scripts.py index 842133671..4f1133603 100644 --- a/netbox/extras/scripts.py +++ b/netbox/extras/scripts.py @@ -16,6 +16,7 @@ from mptt.models import MPTTModel from ipam.formfields import IPFormField from utilities.exceptions import AbortTransaction +from utilities.validators import MaxPrefixLengthValidator, MinPrefixLengthValidator from .constants import LOG_DEFAULT, LOG_FAILURE, LOG_INFO, LOG_SUCCESS, LOG_WARNING from .forms import ScriptForm from .signals import purge_changelog @@ -162,6 +163,21 @@ class IPNetworkVar(ScriptVariable): """ form_field = IPFormField + def __init__(self, min_prefix_length=None, max_prefix_length=None, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.field_attrs['validators'] = list() + + # Optional minimum/maximum prefix lengths + if min_prefix_length is not None: + self.field_attrs['validators'].append( + MinPrefixLengthValidator(min_prefix_length) + ) + if max_prefix_length is not None: + self.field_attrs['validators'].append( + MaxPrefixLengthValidator(max_prefix_length) + ) + # # Scripts diff --git a/netbox/utilities/validators.py b/netbox/utilities/validators.py index cfa733208..fb7a5bba7 100644 --- a/netbox/utilities/validators.py +++ b/netbox/utilities/validators.py @@ -1,6 +1,6 @@ import re -from django.core.validators import _lazy_re_compile, URLValidator +from django.core.validators import _lazy_re_compile, BaseValidator, URLValidator class EnhancedURLValidator(URLValidator): @@ -26,3 +26,19 @@ class EnhancedURLValidator(URLValidator): r'(?:[/?#][^\s]*)?' # Path r'\Z', re.IGNORECASE) schemes = AnyURLScheme() + + +class MaxPrefixLengthValidator(BaseValidator): + message = 'The prefix length must be less than or equal to %(limit_value)s.' + code = 'max_prefix_length' + + def compare(self, a, b): + return a.prefixlen > b + + +class MinPrefixLengthValidator(BaseValidator): + message = 'The prefix length must be greater than or equal to %(limit_value)s.' + code = 'min_prefix_length' + + def compare(self, a, b): + return a.prefixlen < b From e8ee6f1bc5f5df89dc4a88a56f2d6384ca850d28 Mon Sep 17 00:00:00 2001 From: Daniel Sheppard Date: Tue, 17 Sep 2019 15:45:55 -0500 Subject: [PATCH 14/22] Clean up extra line that snuck in --- netbox/dcim/forms.py | 1 - 1 file changed, 1 deletion(-) diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index cdb4cfa4d..d59caae65 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -2330,7 +2330,6 @@ class InterfaceCreateForm(InterfaceCommonForm, ComponentForm, forms.Form): self.fields['tagged_vlans'].choices = vlan_choices - class InterfaceBulkEditForm(InterfaceCommonForm, BootstrapMixin, AddRemoveTagsForm, BulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=Interface.objects.all(), From 7264a4ffb66b07f60dbca180f78819772fa38bc8 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 18 Sep 2019 14:33:47 -0400 Subject: [PATCH 15/22] Fixes #3513: Fix assignment of tags when creating front/rear ports --- CHANGELOG.md | 1 + netbox/dcim/api/serializers.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 346e0effe..e05faa1ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ v2.6.4 (FUTURE) * [#3489](https://github.com/netbox-community/netbox/issues/3489) - Prevent exception triggered by webhook upon object deletion * [#3501](https://github.com/netbox-community/netbox/issues/3501) - Fix rendering of checkboxes on custom script forms +* [#3513](https://github.com/netbox-community/netbox/issues/3513) - Fix assignment of tags when creating front/rear ports v2.6.3 (2019-09-04) diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index e9526fa41..495709268 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -480,7 +480,7 @@ class InterfaceSerializer(TaggitSerializer, ConnectedEndpointSerializer): return super().validate(data) -class RearPortSerializer(ValidatedModelSerializer): +class RearPortSerializer(TaggitSerializer, ValidatedModelSerializer): device = NestedDeviceSerializer() type = ChoiceField(choices=PORT_TYPE_CHOICES) cable = NestedCableSerializer(read_only=True) @@ -502,7 +502,7 @@ class FrontPortRearPortSerializer(WritableNestedSerializer): fields = ['id', 'url', 'name'] -class FrontPortSerializer(ValidatedModelSerializer): +class FrontPortSerializer(TaggitSerializer, ValidatedModelSerializer): device = NestedDeviceSerializer() type = ChoiceField(choices=PORT_TYPE_CHOICES) rear_port = FrontPortRearPortSerializer() From 84208d54294e93e06d54476177670ef34a25ab4d Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 18 Sep 2019 14:40:47 -0400 Subject: [PATCH 16/22] Fixes #3511: Correct API URL for nested device bays --- CHANGELOG.md | 1 + netbox/dcim/api/nested_serializers.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e05faa1ed..a83ec5533 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ v2.6.4 (FUTURE) * [#3489](https://github.com/netbox-community/netbox/issues/3489) - Prevent exception triggered by webhook upon object deletion * [#3501](https://github.com/netbox-community/netbox/issues/3501) - Fix rendering of checkboxes on custom script forms +* [#3511](https://github.com/netbox-community/netbox/issues/3511) - Correct API URL for nested device bays * [#3513](https://github.com/netbox-community/netbox/issues/3513) - Fix assignment of tags when creating front/rear ports v2.6.3 (2019-09-04) diff --git a/netbox/dcim/api/nested_serializers.py b/netbox/dcim/api/nested_serializers.py index cf22916ad..30d84f703 100644 --- a/netbox/dcim/api/nested_serializers.py +++ b/netbox/dcim/api/nested_serializers.py @@ -228,7 +228,7 @@ class NestedFrontPortSerializer(WritableNestedSerializer): class NestedDeviceBaySerializer(WritableNestedSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearport-detail') + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebay-detail') device = NestedDeviceSerializer(read_only=True) class Meta: From a0545568cdcd5f3053ef878cb5ae9db525bbab6f Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 18 Sep 2019 15:39:26 -0400 Subject: [PATCH 17/22] Fixes #3514: Label TextVar fields when rendering custom script forms --- CHANGELOG.md | 1 + netbox/dcim/forms.py | 6 +++++- netbox/extras/forms.py | 4 +++- netbox/secrets/forms.py | 3 +++ netbox/templates/utilities/render_field.html | 2 +- netbox/utilities/forms.py | 4 ++-- netbox/virtualization/forms.py | 7 +++---- 7 files changed, 18 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a83ec5533..7986d5afe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ v2.6.4 (FUTURE) * [#3501](https://github.com/netbox-community/netbox/issues/3501) - Fix rendering of checkboxes on custom script forms * [#3511](https://github.com/netbox-community/netbox/issues/3511) - Correct API URL for nested device bays * [#3513](https://github.com/netbox-community/netbox/issues/3513) - Fix assignment of tags when creating front/rear ports +* [#3514](https://github.com/netbox-community/netbox/issues/3514) - Label TextVar fields when rendering custom script forms v2.6.3 (2019-09-04) diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index d59caae65..aec7a6723 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -809,6 +809,7 @@ class DeviceTypeForm(BootstrapMixin, CustomFieldForm): slug = SlugField( slug_source='model' ) + comments = CommentField() tags = TagField( required=False ) @@ -1358,7 +1359,10 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldForm): ) comments = CommentField() tags = TagField(required=False) - local_context_data = JSONField(required=False) + local_context_data = JSONField( + required=False, + label='' + ) class Meta: model = Device diff --git a/netbox/extras/forms.py b/netbox/extras/forms.py index 19f55c345..61bf2d7f3 100644 --- a/netbox/extras/forms.py +++ b/netbox/extras/forms.py @@ -241,7 +241,9 @@ class TagBulkEditForm(BootstrapMixin, BulkEditForm): # class ConfigContextForm(BootstrapMixin, forms.ModelForm): - data = JSONField() + data = JSONField( + label='' + ) class Meta: model = ConfigContext diff --git a/netbox/secrets/forms.py b/netbox/secrets/forms.py index 6c13ca243..ed0f455c1 100644 --- a/netbox/secrets/forms.py +++ b/netbox/secrets/forms.py @@ -199,6 +199,9 @@ class UserKeyForm(BootstrapMixin, forms.ModelForm): 'public_key': "Enter your public RSA key. Keep the private one with you; you'll need it for decryption. " "Please note that passphrase-protected keys are not supported.", } + labels = { + 'public_key': '' + } def clean_public_key(self): key = self.cleaned_data['public_key'] diff --git a/netbox/templates/utilities/render_field.html b/netbox/templates/utilities/render_field.html index 21b5b905a..e69296873 100644 --- a/netbox/templates/utilities/render_field.html +++ b/netbox/templates/utilities/render_field.html @@ -24,7 +24,7 @@ {% endif %} - {% elif field|widget_type == 'textarea' %} + {% elif field|widget_type == 'textarea' and not field.label %}
{{ field }} {% if bulk_nullable %} diff --git a/netbox/utilities/forms.py b/netbox/utilities/forms.py index aeba4d53d..876581dd4 100644 --- a/netbox/utilities/forms.py +++ b/netbox/utilities/forms.py @@ -384,7 +384,7 @@ class CSVDataField(forms.CharField): self.strip = False if not self.label: - self.label = 'CSV Data' + self.label = '' if not self.initial: self.initial = ','.join(required_fields) + '\n' if not self.help_text: @@ -484,7 +484,7 @@ class CommentField(forms.CharField): A textarea with support for GitHub-Flavored Markdown. Exists mostly just to add a standard help_text. """ widget = forms.Textarea - default_label = 'Comments' + default_label = '' # TODO: Port GFM syntax cheat sheet to internal documentation default_helptext = ' '\ ''\ diff --git a/netbox/virtualization/forms.py b/netbox/virtualization/forms.py index ef4999851..55672514e 100644 --- a/netbox/virtualization/forms.py +++ b/netbox/virtualization/forms.py @@ -79,9 +79,7 @@ class ClusterGroupCSVForm(forms.ModelForm): # class ClusterForm(BootstrapMixin, CustomFieldForm): - comments = CommentField( - widget=SmallTextarea() - ) + comments = CommentField() tags = TagField( required=False ) @@ -331,7 +329,8 @@ class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldForm): required=False ) local_context_data = JSONField( - required=False + required=False, + label='' ) class Meta: From 51fb0b59ec90bc324fb3ba9af08a8a0d5dea1e05 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 18 Sep 2019 15:59:52 -0400 Subject: [PATCH 18/22] Closes #3485: Enable embedded graphs for devices --- CHANGELOG.md | 1 + netbox/circuits/api/views.py | 2 +- netbox/dcim/api/views.py | 18 +++++++++++++++--- netbox/dcim/views.py | 9 ++++----- netbox/extras/constants.py | 2 ++ netbox/templates/dcim/device.html | 6 ++++++ netbox/templates/dcim/inc/interface.html | 2 +- 7 files changed, 30 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7986d5afe..3bbb83fe7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ v2.6.4 (FUTURE) * [#3027](https://github.com/netbox-community/netbox/issues/3028) - Add `local_context_data` boolean filter for devices * [#3318](https://github.com/netbox-community/netbox/issues/3318) - Increase length of platform name and slug to 100 characters * [#3341](https://github.com/netbox-community/netbox/issues/3341) - Enable inline VLAN assignment while editing an interface +* [#3485](https://github.com/netbox-community/netbox/issues/3485) - Enable embedded graphs for devices * [#3510](https://github.com/netbox-community/netbox/issues/3510) - Add minimum/maximum prefix length enforcement for `IPNetworkVar` ## Bug Fixes diff --git a/netbox/circuits/api/views.py b/netbox/circuits/api/views.py index dc1133c5e..65b0db14b 100644 --- a/netbox/circuits/api/views.py +++ b/netbox/circuits/api/views.py @@ -35,7 +35,7 @@ class ProviderViewSet(CustomFieldModelViewSet): filterset_class = filters.ProviderFilter @action(detail=True) - def graphs(self, request, pk=None): + def graphs(self, request, pk): """ A convenience method for rendering graphs for a particular provider. """ diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index 4ddee7337..12774e4be 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -23,7 +23,8 @@ from dcim.models import ( ) from extras.api.serializers import RenderedGraphSerializer from extras.api.views import CustomFieldModelViewSet -from extras.models import Graph, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE +from extras.constants import GRAPH_TYPE_DEVICE, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE +from extras.models import Graph from ipam.models import Prefix, VLAN from utilities.api import ( get_serializer_for_model, IsAuthenticatedOrLoginNotRequired, FieldChoicesViewSet, ModelViewSet, ServiceUnavailable, @@ -123,7 +124,7 @@ class SiteViewSet(CustomFieldModelViewSet): filterset_class = filters.SiteFilter @action(detail=True) - def graphs(self, request, pk=None): + def graphs(self, request, pk): """ A convenience method for rendering graphs for a particular site. """ @@ -346,6 +347,17 @@ class DeviceViewSet(CustomFieldModelViewSet): return serializers.DeviceWithConfigContextSerializer + @action(detail=True) + def graphs(self, request, pk): + """ + A convenience method for rendering graphs for a particular Device. + """ + device = get_object_or_404(Device, pk=pk) + queryset = Graph.objects.filter(type=GRAPH_TYPE_DEVICE) + serializer = RenderedGraphSerializer(queryset, many=True, context={'graphed_object': device}) + + return Response(serializer.data) + @action(detail=True, url_path='napalm') def napalm(self, request, pk): """ @@ -458,7 +470,7 @@ class InterfaceViewSet(CableTraceMixin, ModelViewSet): filterset_class = filters.InterfaceFilter @action(detail=True) - def graphs(self, request, pk=None): + def graphs(self, request, pk): """ A convenience method for rendering graphs for a particular interface. """ diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index f953f95c2..301d05e84 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -16,7 +16,8 @@ from django.utils.safestring import mark_safe from django.views.generic import View from circuits.models import Circuit -from extras.models import Graph, TopologyMap, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE +from extras.constants import GRAPH_TYPE_DEVICE, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE +from extras.models import Graph, TopologyMap from extras.views import ObjectConfigContextView from ipam.models import Prefix, VLAN from ipam.tables import InterfaceIPAddressTable, InterfaceVLANTable @@ -972,9 +973,6 @@ class DeviceView(PermissionRequiredMixin, View): 'rack', 'device_type__manufacturer' )[:10] - # Show graph button on interfaces only if at least one graph has been created. - show_graphs = Graph.objects.filter(type=GRAPH_TYPE_INTERFACE).exists() - return render(request, 'dcim/device.html', { 'device': device, 'console_ports': console_ports, @@ -989,7 +987,8 @@ class DeviceView(PermissionRequiredMixin, View): 'secrets': secrets, 'vc_members': vc_members, 'related_devices': related_devices, - 'show_graphs': show_graphs, + 'show_graphs': Graph.objects.filter(type=GRAPH_TYPE_DEVICE).exists(), + 'show_interface_graphs': Graph.objects.filter(type=GRAPH_TYPE_INTERFACE).exists(), }) diff --git a/netbox/extras/constants.py b/netbox/extras/constants.py index b72ae8c08..d136d3271 100644 --- a/netbox/extras/constants.py +++ b/netbox/extras/constants.py @@ -88,10 +88,12 @@ BUTTON_CLASS_CHOICES = ( # Graph types GRAPH_TYPE_INTERFACE = 100 +GRAPH_TYPE_DEVICE = 150 GRAPH_TYPE_PROVIDER = 200 GRAPH_TYPE_SITE = 300 GRAPH_TYPE_CHOICES = ( (GRAPH_TYPE_INTERFACE, 'Interface'), + (GRAPH_TYPE_DEVICE, 'Device'), (GRAPH_TYPE_PROVIDER, 'Provider'), (GRAPH_TYPE_SITE, 'Site'), ) diff --git a/netbox/templates/dcim/device.html b/netbox/templates/dcim/device.html index dca3e60a6..57e2b03b8 100644 --- a/netbox/templates/dcim/device.html +++ b/netbox/templates/dcim/device.html @@ -35,6 +35,12 @@
+ {% if show_graphs %} + + {% endif %} {% if perms.dcim.change_device %}