mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-25 01:48:38 -06:00
Fixes #664: Re-implemented view for bulk creation of interfaces across multiple devices
This commit is contained in:
parent
76c6fbbfba
commit
bbac6e2ba6
@ -584,6 +584,18 @@ class DeviceBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
|||||||
nullable_fields = ['tenant', 'platform']
|
nullable_fields = ['tenant', 'platform']
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceBulkAddComponentForm(forms.Form, BootstrapMixin):
|
||||||
|
pk = forms.ModelMultipleChoiceField(queryset=Device.objects.all(), widget=forms.MultipleHiddenInput)
|
||||||
|
name_pattern = ExpandableNameField(label='Name')
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceBulkAddInterfaceForm(forms.ModelForm, DeviceBulkAddComponentForm):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Interface
|
||||||
|
fields = ['name_pattern', 'form_factor', 'mgmt_only', 'description']
|
||||||
|
|
||||||
|
|
||||||
class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||||
model = Device
|
model = Device
|
||||||
site = FilterChoiceField(queryset=Site.objects.annotate(filter_count=Count('racks__devices')), to_field_name='slug')
|
site = FilterChoiceField(queryset=Site.objects.annotate(filter_count=Count('racks__devices')), to_field_name='slug')
|
||||||
@ -1014,10 +1026,6 @@ class InterfaceCreateForm(forms.ModelForm, BootstrapMixin):
|
|||||||
fields = ['name_pattern', 'form_factor', 'mac_address', 'mgmt_only', 'description']
|
fields = ['name_pattern', 'form_factor', 'mac_address', 'mgmt_only', 'description']
|
||||||
|
|
||||||
|
|
||||||
class InterfaceBulkCreateForm(InterfaceCreateForm, BootstrapMixin):
|
|
||||||
pk = forms.ModelMultipleChoiceField(queryset=Device.objects.all(), widget=forms.MultipleHiddenInput)
|
|
||||||
|
|
||||||
|
|
||||||
class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
|
class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||||
pk = forms.ModelMultipleChoiceField(queryset=Interface.objects.all(), widget=forms.MultipleHiddenInput)
|
pk = forms.ModelMultipleChoiceField(queryset=Interface.objects.all(), widget=forms.MultipleHiddenInput)
|
||||||
form_factor = forms.ChoiceField(choices=add_blank_choice(IFACE_FF_CHOICES), required=False)
|
form_factor = forms.ChoiceField(choices=add_blank_choice(IFACE_FF_CHOICES), required=False)
|
||||||
@ -1250,7 +1258,7 @@ class IPAddressForm(BootstrapMixin, CustomFieldForm):
|
|||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Interfaces
|
# Modules
|
||||||
#
|
#
|
||||||
|
|
||||||
class ModuleForm(forms.ModelForm, BootstrapMixin):
|
class ModuleForm(forms.ModelForm, BootstrapMixin):
|
||||||
|
@ -154,7 +154,7 @@ urlpatterns = [
|
|||||||
url(r'^interface-connections/import/$', views.InterfaceConnectionsBulkImportView.as_view(), name='interface_connections_import'),
|
url(r'^interface-connections/import/$', views.InterfaceConnectionsBulkImportView.as_view(), name='interface_connections_import'),
|
||||||
|
|
||||||
# Interfaces
|
# Interfaces
|
||||||
url(r'^devices/interfaces/add/$', views.InterfaceBulkAddView.as_view(), name='interface_add_multi'),
|
url(r'^devices/interfaces/add/$', views.DeviceBulkAddInterfaceView.as_view(), name='device_bulk_add_interface'),
|
||||||
url(r'^devices/(?P<pk>\d+)/interfaces/add/$', views.interface_add, name='interface_add'),
|
url(r'^devices/(?P<pk>\d+)/interfaces/add/$', views.interface_add, name='interface_add'),
|
||||||
url(r'^devices/(?P<pk>\d+)/interfaces/edit/$', views.InterfaceBulkEditView.as_view(), name='interface_bulk_edit'),
|
url(r'^devices/(?P<pk>\d+)/interfaces/edit/$', views.InterfaceBulkEditView.as_view(), name='interface_bulk_edit'),
|
||||||
url(r'^devices/(?P<pk>\d+)/interfaces/delete/$', views.InterfaceBulkDeleteView.as_view(), name='interface_bulk_delete'),
|
url(r'^devices/(?P<pk>\d+)/interfaces/delete/$', views.InterfaceBulkDeleteView.as_view(), name='interface_bulk_delete'),
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
from copy import deepcopy
|
||||||
import re
|
import re
|
||||||
from natsort import natsorted
|
from natsort import natsorted
|
||||||
from operator import attrgetter
|
from operator import attrgetter
|
||||||
@ -7,6 +8,7 @@ from django.contrib.auth.decorators import permission_required
|
|||||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
|
from django.db import transaction
|
||||||
from django.db.models import Count
|
from django.db.models import Count
|
||||||
from django.http import HttpResponseRedirect
|
from django.http import HttpResponseRedirect
|
||||||
from django.shortcuts import get_object_or_404, redirect, render
|
from django.shortcuts import get_object_or_404, redirect, render
|
||||||
@ -686,6 +688,80 @@ def device_lldp_neighbors(request, pk):
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceBulkAddComponentView(View):
|
||||||
|
"""
|
||||||
|
Add one or more components (e.g. interfaces) to a selected set of Devices.
|
||||||
|
"""
|
||||||
|
form = None
|
||||||
|
component_cls = None
|
||||||
|
component_form = None
|
||||||
|
|
||||||
|
def get(self):
|
||||||
|
return redirect('dcim:device_list')
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
|
||||||
|
# Are we editing *all* objects in the queryset or just a selected subset?
|
||||||
|
if request.POST.get('_all'):
|
||||||
|
pk_list = [int(pk) for pk in request.POST.get('pk_all').split(',') if pk]
|
||||||
|
else:
|
||||||
|
pk_list = [int(pk) for pk in request.POST.getlist('pk')]
|
||||||
|
|
||||||
|
if '_create' in request.POST:
|
||||||
|
form = self.form(request.POST)
|
||||||
|
if form.is_valid():
|
||||||
|
|
||||||
|
new_components = []
|
||||||
|
data = deepcopy(form.cleaned_data)
|
||||||
|
for device in data['pk']:
|
||||||
|
|
||||||
|
names = data['name_pattern']
|
||||||
|
for name in names:
|
||||||
|
component_data = {
|
||||||
|
'device': device.pk,
|
||||||
|
'name': name,
|
||||||
|
}
|
||||||
|
component_data.update(data)
|
||||||
|
component_form = self.component_form(component_data)
|
||||||
|
if component_form.is_valid():
|
||||||
|
new_components.append(component_form.save(commit=False))
|
||||||
|
else:
|
||||||
|
form.add_error('name_pattern', "Duplicate {} name for {}: {}".format(
|
||||||
|
self.component_cls._meta.verbose_name, device, name
|
||||||
|
))
|
||||||
|
|
||||||
|
if not form.errors:
|
||||||
|
self.component_cls.objects.bulk_create(new_components)
|
||||||
|
messages.success(request, u"Added {} {} to {} devices.".format(
|
||||||
|
len(new_components), self.component_cls._meta.verbose_name_plural, len(form.cleaned_data['pk'])
|
||||||
|
))
|
||||||
|
return redirect('dcim:device_list')
|
||||||
|
|
||||||
|
else:
|
||||||
|
form = self.form(initial={'pk': pk_list})
|
||||||
|
|
||||||
|
selected_devices = Device.objects.filter(pk__in=pk_list)
|
||||||
|
if not selected_devices:
|
||||||
|
messages.warning(request, u"No devices were selected.")
|
||||||
|
return redirect('dcim:device_list')
|
||||||
|
|
||||||
|
return render(request, 'dcim/device_bulk_add_component.html', {
|
||||||
|
'form': form,
|
||||||
|
'component_name': self.component_cls._meta.verbose_name_plural,
|
||||||
|
'selected_devices': selected_devices,
|
||||||
|
'cancel_url': reverse('dcim:device_list'),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceBulkAddInterfaceView(DeviceBulkAddComponentView):
|
||||||
|
"""
|
||||||
|
Add one or more components (e.g. interfaces) to a selected set of Devices.
|
||||||
|
"""
|
||||||
|
form = forms.DeviceBulkAddInterfaceForm
|
||||||
|
component_cls = Interface
|
||||||
|
component_form = forms.InterfaceForm
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Console ports
|
# Console ports
|
||||||
#
|
#
|
||||||
@ -1236,39 +1312,6 @@ class InterfaceDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
|||||||
model = Interface
|
model = Interface
|
||||||
|
|
||||||
|
|
||||||
class InterfaceBulkAddView(PermissionRequiredMixin, BulkEditView):
|
|
||||||
permission_required = 'dcim.add_interface'
|
|
||||||
cls = Device
|
|
||||||
form = forms.InterfaceBulkCreateForm
|
|
||||||
template_name = 'dcim/interface_add_multi.html'
|
|
||||||
default_redirect_url = 'dcim:device_list'
|
|
||||||
|
|
||||||
def update_objects(self, pk_list, form, fields):
|
|
||||||
|
|
||||||
selected_devices = Device.objects.filter(pk__in=pk_list)
|
|
||||||
interfaces = []
|
|
||||||
|
|
||||||
for device in selected_devices:
|
|
||||||
for name in form.cleaned_data['name_pattern']:
|
|
||||||
iface_form = forms.InterfaceForm({
|
|
||||||
'device': device.pk,
|
|
||||||
'name': name,
|
|
||||||
'mac_address': form.cleaned_data['mac_address'],
|
|
||||||
'form_factor': form.cleaned_data['form_factor'],
|
|
||||||
'mgmt_only': form.cleaned_data['mgmt_only'],
|
|
||||||
'description': form.cleaned_data['description'],
|
|
||||||
})
|
|
||||||
if iface_form.is_valid():
|
|
||||||
interfaces.append(iface_form.save(commit=False))
|
|
||||||
else:
|
|
||||||
form.add_error(None, "Duplicate interface {} found for device {}".format(name, device))
|
|
||||||
|
|
||||||
if not form.errors:
|
|
||||||
Interface.objects.bulk_create(interfaces)
|
|
||||||
messages.success(self.request, u"Added {} interfaces to {} devices.".format(len(interfaces),
|
|
||||||
len(selected_devices)))
|
|
||||||
|
|
||||||
|
|
||||||
class InterfaceBulkEditView(PermissionRequiredMixin, BulkEditView):
|
class InterfaceBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||||
permission_required = 'dcim.change_interface'
|
permission_required = 'dcim.change_interface'
|
||||||
cls = Interface
|
cls = Interface
|
||||||
|
60
netbox/templates/dcim/device_bulk_add_component.html
Normal file
60
netbox/templates/dcim/device_bulk_add_component.html
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
{% extends '_base.html' %}
|
||||||
|
{% load form_helpers %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>Add {{ component_name|title }}</h1>
|
||||||
|
<form action="." method="post" class="form form-horizontal">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% if request.POST.redirect_url %}
|
||||||
|
<input type="hidden" name="redirect_url" value="{{ request.POST.redirect_url }}" />
|
||||||
|
{% endif %}
|
||||||
|
{% for field in form.hidden_fields %}
|
||||||
|
{{ field }}
|
||||||
|
{% endfor %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-7">
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading"><strong>Selected Devices</strong></div>
|
||||||
|
<table class="panel-body table table-hover">
|
||||||
|
<tr>
|
||||||
|
<th>Device</th>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>Role</th>
|
||||||
|
</tr>
|
||||||
|
{% for device in selected_devices %}
|
||||||
|
<tr>
|
||||||
|
<td><a href="{% url 'dcim:device' pk=device.pk %}">{{ device }}</a></td>
|
||||||
|
<td>{{ device.device_type }}</td>
|
||||||
|
<td>{{ device.device_role }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-5">
|
||||||
|
{% if form.non_field_errors %}
|
||||||
|
<div class="panel panel-danger">
|
||||||
|
<div class="panel-heading"><strong>Errors</strong></div>
|
||||||
|
<div class="panel-body">
|
||||||
|
{{ form.non_field_errors }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading"><strong>{{ component_name|title }} to Add</strong></div>
|
||||||
|
<div class="panel-body">
|
||||||
|
{% for field in form.visible_fields %}
|
||||||
|
{% render_field field %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group text-right">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<button type="submit" name="_create" class="btn btn-primary">Create</button>
|
||||||
|
<a href="{{ cancel_url }}" class="btn btn-default">Cancel</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
{% block extra_actions %}
|
{% block extra_actions %}
|
||||||
{% if perms.dcim.add_interface %}
|
{% if perms.dcim.add_interface %}
|
||||||
<button type="submit" name="_edit" formaction="{% url 'dcim:interface_add_multi' %}" class="btn btn-primary btn-sm">
|
<button type="submit" name="_edit" formaction="{% url 'dcim:device_bulk_add_interface' %}" class="btn btn-primary btn-sm">
|
||||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add Interfaces
|
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add Interfaces
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -1,23 +0,0 @@
|
|||||||
{% extends 'utilities/bulk_edit_form.html' %}
|
|
||||||
{% load form_helpers %}
|
|
||||||
|
|
||||||
{% block title %}Add Interfaces{% endblock %}
|
|
||||||
|
|
||||||
{% block selected_objects_title %}Selected Devices{% endblock %}
|
|
||||||
|
|
||||||
{% block form_title %}Interface(s) to Add{% endblock %}
|
|
||||||
|
|
||||||
{% block selected_objects_table %}
|
|
||||||
<tr>
|
|
||||||
<th>Device</th>
|
|
||||||
<th>Type</th>
|
|
||||||
<th>Role</th>
|
|
||||||
</tr>
|
|
||||||
{% for device in selected_objects %}
|
|
||||||
<tr>
|
|
||||||
<td><a href="{% url 'dcim:device' pk=device.pk %}">{{ device }}</a></td>
|
|
||||||
<td>{{ device.device_type }}</td>
|
|
||||||
<td>{{ device.device_role }}</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
{% endblock %}
|
|
Loading…
Reference in New Issue
Block a user