From 0e5138d6ecc414eab3f668bf2b4de3b0cb0dbdee Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 17 Feb 2017 16:10:07 -0500 Subject: [PATCH 1/6] Fixes #872: TypeError on bulk IP address creation (Python 3) --- netbox/utilities/views.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/netbox/utilities/views.py b/netbox/utilities/views.py index 9d1561a48..3fa09b829 100644 --- a/netbox/utilities/views.py +++ b/netbox/utilities/views.py @@ -307,11 +307,12 @@ class BulkAddView(View): if form.is_valid(): # The first field will be used as the pattern - pattern_field = form.fields.keys()[0] + field_names = list(form.fields.keys()) + pattern_field = field_names[0] pattern = form.cleaned_data[pattern_field] # All other fields will be copied as object attributes - kwargs = {k: form.cleaned_data[k] for k in form.fields.keys()[1:]} + kwargs = {k: form.cleaned_data[k] for k in field_names[1:]} new_objs = [] try: From b7f4a11eee1dfb0b3947fcc4642898c3029b89ab Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 17 Feb 2017 16:34:09 -0500 Subject: [PATCH 2/6] Fixes #892: Restored missing edit/delete buttons when viewing child prefixes and IP addresses from a parent object --- netbox/ipam/views.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index 191d33a90..6eef522ec 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -297,9 +297,17 @@ def aggregate(request, pk): prefix_table.base_columns['pk'].visible = True RequestConfig(request, paginate={'klass': EnhancedPaginator}).configure(prefix_table) + # Compile permissions list for rendering the object table + permissions = { + 'add': request.user.has_perm('ipam.add_prefix'), + 'change': request.user.has_perm('ipam.change_prefix'), + 'delete': request.user.has_perm('ipam.delete_prefix'), + } + return render(request, 'ipam/aggregate.html', { 'aggregate': aggregate, 'prefix_table': prefix_table, + 'permissions': permissions, }) @@ -425,6 +433,13 @@ def prefix(request, pk): child_prefix_table.base_columns['pk'].visible = True RequestConfig(request, paginate={'klass': EnhancedPaginator}).configure(child_prefix_table) + # Compile permissions list for rendering the object table + permissions = { + 'add': request.user.has_perm('ipam.add_prefix'), + 'change': request.user.has_perm('ipam.change_prefix'), + 'delete': request.user.has_perm('ipam.delete_prefix'), + } + return render(request, 'ipam/prefix.html', { 'prefix': prefix, 'aggregate': aggregate, @@ -432,6 +447,7 @@ def prefix(request, pk): 'parent_prefix_table': parent_prefix_table, 'child_prefix_table': child_prefix_table, 'duplicate_prefix_table': duplicate_prefix_table, + 'permissions': permissions, 'return_url': prefix.get_absolute_url(), }) @@ -490,9 +506,17 @@ def prefix_ipaddresses(request, pk): ip_table.base_columns['pk'].visible = True RequestConfig(request, paginate={'klass': EnhancedPaginator}).configure(ip_table) + # Compile permissions list for rendering the object table + permissions = { + 'add': request.user.has_perm('ipam.add_ipaddress'), + 'change': request.user.has_perm('ipam.change_ipaddress'), + 'delete': request.user.has_perm('ipam.delete_ipaddress'), + } + return render(request, 'ipam/prefix_ipaddresses.html', { 'prefix': prefix, 'ip_table': ip_table, + 'permissions': permissions, }) From 7d1aeede1a94b1599cee9280b315d9532f5d535c Mon Sep 17 00:00:00 2001 From: Jasperswaagman Date: Tue, 21 Feb 2017 15:20:42 +0100 Subject: [PATCH 3/6] Typo --- docs/data-model/dcim.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/data-model/dcim.md b/docs/data-model/dcim.md index aa8673fb1..a345312d5 100644 --- a/docs/data-model/dcim.md +++ b/docs/data-model/dcim.md @@ -24,7 +24,7 @@ Each group is assigned to a parent site for easy navigation. Hierarchical recurs ### Rack Roles -Each rak can optionally be assigned to a functional role. For example, you might designate a rack for compute or storage resources, or to house colocated customer devices. +Each rack can optionally be assigned to a functional role. For example, you might designate a rack for compute or storage resources, or to house colocated customer devices. --- From aba9748ffd1a6bcb98efda777781ae5f14bfe821 Mon Sep 17 00:00:00 2001 From: Shawn Peng Date: Tue, 21 Feb 2017 10:27:24 -0800 Subject: [PATCH 4/6] Fix #235: Enable global vlan (#904) * Fix #235: Enable global vlan Decouple site/vlan, make site optional for vlan/vlangroup Change html generation code to check site existence before dereference Create site search function, if site is None for a VLAN, view it as global VLAN * commit1 * commit2 * commit3 * Add migration file for VLAN&VLAN group * Revert unintentional commits --- netbox/ipam/filters.py | 24 ++++++++++++++--- netbox/ipam/forms.py | 17 +++++++----- .../migrations/0015_auto_20170219_0726.py | 26 +++++++++++++++++++ netbox/ipam/models.py | 9 ++++--- netbox/templates/ipam/vlan.html | 14 ++++++++-- 5 files changed, 73 insertions(+), 17 deletions(-) create mode 100644 netbox/ipam/migrations/0015_auto_20170219_0726.py diff --git a/netbox/ipam/filters.py b/netbox/ipam/filters.py index a0dc9f633..558f5eade 100644 --- a/netbox/ipam/filters.py +++ b/netbox/ipam/filters.py @@ -262,37 +262,47 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet): class VLANGroupFilter(django_filters.FilterSet): - site_id = django_filters.ModelMultipleChoiceFilter( + site_id = NullableModelMultipleChoiceFilter( name='site', queryset=Site.objects.all(), label='Site (ID)', + method='site_search', ) - site = django_filters.ModelMultipleChoiceFilter( + site = NullableModelMultipleChoiceFilter( name='site__slug', queryset=Site.objects.all(), to_field_name='slug', label='Site (slug)', + method='site_search', ) class Meta: model = VLANGroup + def site_search(self, queryset, name, value): + q = Q(**{name: None}) + for v in value: + q |= Q(**{name: v}) + return queryset.filter(q) + class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet): q = django_filters.MethodFilter( action='search', label='Search', ) - site_id = django_filters.ModelMultipleChoiceFilter( + site_id = NullableModelMultipleChoiceFilter( name='site', queryset=Site.objects.all(), label='Site (ID)', + method='site_search', ) - site = django_filters.ModelMultipleChoiceFilter( + site = NullableModelMultipleChoiceFilter( name='site__slug', queryset=Site.objects.all(), to_field_name='slug', label='Site (slug)', + method='site_search', ) group_id = NullableModelMultipleChoiceFilter( name='group', @@ -349,6 +359,12 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet): pass return queryset.filter(qs_filter) + def site_search(self, queryset, name, value): + q = Q(**{name: None}) + for v in value: + q |= Q(**{name: v}) + return queryset.filter(q) + class ServiceFilter(django_filters.FilterSet): device_id = django_filters.ModelMultipleChoiceFilter( diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index 83be76169..d6db8c9bc 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -153,7 +153,8 @@ class RoleForm(BootstrapMixin, forms.ModelForm): class PrefixForm(BootstrapMixin, CustomFieldForm): site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False, label='Site', - widget=forms.Select(attrs={'filter-for': 'vlan'})) + widget=forms.Select(attrs={'filter-for': 'vlan', + 'default_value': '0'})) vlan = forms.ModelChoiceField(queryset=VLAN.objects.all(), required=False, label='VLAN', widget=APISelect(api_url='/api/ipam/vlans/?site_id={{site}}', display_field='display_name')) @@ -173,7 +174,7 @@ class PrefixForm(BootstrapMixin, CustomFieldForm): elif self.initial.get('site'): self.fields['vlan'].queryset = VLAN.objects.filter(site=self.initial['site']) else: - self.fields['vlan'].choices = [] + self.fields['vlan'].queryset = VLAN.objects.filter(site=None) class PrefixFromCSVForm(forms.ModelForm): @@ -508,7 +509,8 @@ class VLANGroupForm(BootstrapMixin, forms.ModelForm): class VLANGroupFilterForm(BootstrapMixin, forms.Form): - site = FilterChoiceField(queryset=Site.objects.annotate(filter_count=Count('vlan_groups')), to_field_name='slug') + site = FilterChoiceField(queryset=Site.objects.annotate(filter_count=Count('vlan_groups')), to_field_name='slug', + null_option=(0, 'Global')) # @@ -532,7 +534,7 @@ class VLANForm(BootstrapMixin, CustomFieldForm): 'role': "The primary function of this VLAN", } widgets = { - 'site': forms.Select(attrs={'filter-for': 'group'}), + 'site': forms.Select(attrs={'filter-for': 'group', 'default_value': '0'}), } def __init__(self, *args, **kwargs): @@ -545,11 +547,11 @@ class VLANForm(BootstrapMixin, CustomFieldForm): elif self.initial.get('site'): self.fields['group'].queryset = VLANGroup.objects.filter(site=self.initial['site']) else: - self.fields['group'].choices = [] + self.fields['group'].queryset = VLANGroup.objects.filter(site=None) class VLANFromCSVForm(forms.ModelForm): - site = forms.ModelChoiceField(queryset=Site.objects.all(), to_field_name='name', + site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False, to_field_name='name', error_messages={'invalid_choice': 'Site not found.'}) group = forms.ModelChoiceField(queryset=VLANGroup.objects.all(), required=False, to_field_name='name', error_messages={'invalid_choice': 'VLAN group not found.'}) @@ -599,7 +601,8 @@ def vlan_status_choices(): class VLANFilterForm(BootstrapMixin, CustomFieldFilterForm): model = VLAN q = forms.CharField(required=False, label='Search') - site = FilterChoiceField(queryset=Site.objects.annotate(filter_count=Count('vlans')), to_field_name='slug') + site = FilterChoiceField(queryset=Site.objects.annotate(filter_count=Count('vlans')), to_field_name='slug', + null_option=(0, 'Global')) group_id = FilterChoiceField(queryset=VLANGroup.objects.annotate(filter_count=Count('vlans')), label='VLAN group', null_option=(0, 'None')) tenant = FilterChoiceField(queryset=Tenant.objects.annotate(filter_count=Count('vlans')), to_field_name='slug', diff --git a/netbox/ipam/migrations/0015_auto_20170219_0726.py b/netbox/ipam/migrations/0015_auto_20170219_0726.py new file mode 100644 index 000000000..38003b2b8 --- /dev/null +++ b/netbox/ipam/migrations/0015_auto_20170219_0726.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-02-19 07:26 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('ipam', '0014_ipaddress_status_add_deprecated'), + ] + + operations = [ + migrations.AlterField( + model_name='vlan', + name='site', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='vlans', to='dcim.Site'), + ), + migrations.AlterField( + model_name='vlangroup', + name='site', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='vlan_groups', to='dcim.Site'), + ), + ] diff --git a/netbox/ipam/models.py b/netbox/ipam/models.py index d37fdec25..fa9e7834b 100644 --- a/netbox/ipam/models.py +++ b/netbox/ipam/models.py @@ -485,7 +485,7 @@ class VLANGroup(models.Model): """ name = models.CharField(max_length=50) slug = models.SlugField() - site = models.ForeignKey('dcim.Site', related_name='vlan_groups') + site = models.ForeignKey('dcim.Site', related_name='vlan_groups', on_delete=models.SET_NULL, blank=True, null=True) class Meta: ordering = ['site', 'name'] @@ -497,7 +497,8 @@ class VLANGroup(models.Model): verbose_name_plural = 'VLAN groups' def __str__(self): - return u'{} - {}'.format(self.site.name, self.name) + site_name = self.site.name if self.site else '__global' + return u'{} - {}'.format(site_name, self.name) def get_absolute_url(self): return "{}?group_id={}".format(reverse('ipam:vlan_list'), self.pk) @@ -513,7 +514,7 @@ class VLAN(CreatedUpdatedModel, CustomFieldModel): Like Prefixes, each VLAN is assigned an operational status and optionally a user-defined Role. A VLAN can have zero or more Prefixes assigned to it. """ - site = models.ForeignKey('dcim.Site', related_name='vlans', on_delete=models.PROTECT) + site = models.ForeignKey('dcim.Site', related_name='vlans', on_delete=models.PROTECT, blank=True, null=True) group = models.ForeignKey('VLANGroup', related_name='vlans', blank=True, null=True, on_delete=models.PROTECT) vid = models.PositiveSmallIntegerField(verbose_name='ID', validators=[ MinValueValidator(1), @@ -551,7 +552,7 @@ class VLAN(CreatedUpdatedModel, CustomFieldModel): def to_csv(self): return csv_format([ - self.site.name, + self.site.name if self.site else None, self.group.name if self.group else None, self.vid, self.name, diff --git a/netbox/templates/ipam/vlan.html b/netbox/templates/ipam/vlan.html index 3d81392e0..5d9f219fc 100644 --- a/netbox/templates/ipam/vlan.html +++ b/netbox/templates/ipam/vlan.html @@ -8,7 +8,11 @@