From 700194b80da3ea51f7e013da90cab53db9252a3f Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 12 Sep 2017 12:49:01 -0400 Subject: [PATCH] Implemented bulk interface creation for virtual machines --- netbox/dcim/forms.py | 11 +- netbox/dcim/views.py | 106 ++++++------------ .../obj_bulk_add_component.html} | 16 +-- .../inc/virtualmachine_table.html | 14 +++ .../virtualization/virtualmachine_list.html | 2 +- netbox/utilities/views.py | 82 ++++++++++++++ netbox/virtualization/forms.py | 16 +++ netbox/virtualization/urls.py | 2 +- netbox/virtualization/views.py | 18 ++- 9 files changed, 169 insertions(+), 98 deletions(-) rename netbox/templates/{dcim/device_bulk_add_component.html => utilities/obj_bulk_add_component.html} (69%) create mode 100644 netbox/templates/virtualization/inc/virtualmachine_table.html diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 747092b3d..18896bac6 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -903,11 +903,12 @@ class DeviceBulkAddComponentForm(BootstrapMixin, forms.Form): name_pattern = ExpandableNameField(label='Name') -class DeviceBulkAddInterfaceForm(forms.ModelForm, DeviceBulkAddComponentForm): - - class Meta: - model = Interface - fields = ['pk', 'name_pattern', 'form_factor', 'mgmt_only', 'description'] +class DeviceBulkAddInterfaceForm(DeviceBulkAddComponentForm): + form_factor = forms.ChoiceField(choices=IFACE_FF_CHOICES) + enabled = forms.BooleanField(required=False, initial=True) + mtu = forms.IntegerField(required=False, min_value=1, max_value=32767, label='MTU') + mgmt_only = forms.BooleanField(required=False, label='OOB Management') + description = forms.CharField(max_length=100, required=False) # diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 671e514dc..3e456cfd4 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -23,8 +23,8 @@ from extras.models import Graph, TopologyMap, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_S from utilities.forms import ConfirmationForm from utilities.paginator import EnhancedPaginator from utilities.views import ( - BulkDeleteView, BulkEditView, BulkImportView, ComponentCreateView, ComponentDeleteView, ComponentEditView, - ObjectDeleteView, ObjectEditView, ObjectListView, + BulkComponentCreateView, BulkDeleteView, BulkEditView, BulkImportView, ComponentCreateView, ComponentDeleteView, + ComponentEditView, ObjectDeleteView, ObjectEditView, ObjectListView, ) from . import filters, forms, tables from .models import ( @@ -1569,109 +1569,67 @@ class DeviceBayBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): # -# Bulk device component creation +# Bulk Device component creation # -class DeviceBulkAddComponentView(View): - """ - Add one or more components (e.g. interfaces) to a selected set of Devices. - """ - form = forms.DeviceBulkAddComponentForm - model = None - model_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 = [obj.pk for obj in filters.DeviceFilter(request.GET, Device.objects.all())] - 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.model_form(component_data) - if component_form.is_valid(): - new_components.append(component_form.save(commit=False)) - else: - for field, errors in component_form.errors.as_data().items(): - for e in errors: - form.add_error(field, '{} {}: {}'.format(device, name, ', '.join(e))) - - if not form.errors: - self.model.objects.bulk_create(new_components) - messages.success(request, "Added {} {} to {} devices.".format( - len(new_components), self.model._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, "No devices were selected.") - return redirect('dcim:device_list') - - return render(request, 'dcim/device_bulk_add_component.html', { - 'form': form, - 'component_name': self.model._meta.verbose_name_plural, - 'selected_devices': selected_devices, - 'return_url': reverse('dcim:device_list'), - }) - - -class DeviceBulkAddConsolePortView(PermissionRequiredMixin, DeviceBulkAddComponentView): +class DeviceBulkAddConsolePortView(PermissionRequiredMixin, BulkComponentCreateView): permission_required = 'dcim.add_consoleport' + parent_model = Device + parent_field = 'device' + form = forms.DeviceBulkAddComponentForm model = ConsolePort model_form = forms.ConsolePortForm + table = tables.DeviceTable -class DeviceBulkAddConsoleServerPortView(PermissionRequiredMixin, DeviceBulkAddComponentView): +class DeviceBulkAddConsoleServerPortView(PermissionRequiredMixin, BulkComponentCreateView): permission_required = 'dcim.add_consoleserverport' + parent_model = Device + parent_field = 'device' + form = forms.DeviceBulkAddComponentForm model = ConsoleServerPort model_form = forms.ConsoleServerPortForm + table = tables.DeviceTable -class DeviceBulkAddPowerPortView(PermissionRequiredMixin, DeviceBulkAddComponentView): +class DeviceBulkAddPowerPortView(PermissionRequiredMixin, BulkComponentCreateView): permission_required = 'dcim.add_powerport' + parent_model = Device + parent_field = 'device' + form = forms.DeviceBulkAddComponentForm model = PowerPort model_form = forms.PowerPortForm + table = tables.DeviceTable -class DeviceBulkAddPowerOutletView(PermissionRequiredMixin, DeviceBulkAddComponentView): +class DeviceBulkAddPowerOutletView(PermissionRequiredMixin, BulkComponentCreateView): permission_required = 'dcim.add_poweroutlet' + parent_model = Device + parent_field = 'device' + form = forms.DeviceBulkAddComponentForm model = PowerOutlet model_form = forms.PowerOutletForm + table = tables.DeviceTable -class DeviceBulkAddInterfaceView(PermissionRequiredMixin, DeviceBulkAddComponentView): +class DeviceBulkAddInterfaceView(PermissionRequiredMixin, BulkComponentCreateView): permission_required = 'dcim.add_interface' + parent_model = Device + parent_field = 'device' form = forms.DeviceBulkAddInterfaceForm model = Interface model_form = forms.InterfaceForm + table = tables.DeviceTable -class DeviceBulkAddDeviceBayView(PermissionRequiredMixin, DeviceBulkAddComponentView): +class DeviceBulkAddDeviceBayView(PermissionRequiredMixin, BulkComponentCreateView): permission_required = 'dcim.add_devicebay' + parent_model = Device + parent_field = 'device' + form = forms.DeviceBulkAddComponentForm model = DeviceBay model_form = forms.DeviceBayForm + table = tables.DeviceTable # diff --git a/netbox/templates/dcim/device_bulk_add_component.html b/netbox/templates/utilities/obj_bulk_add_component.html similarity index 69% rename from netbox/templates/dcim/device_bulk_add_component.html rename to netbox/templates/utilities/obj_bulk_add_component.html index 697942998..93c7060a4 100644 --- a/netbox/templates/dcim/device_bulk_add_component.html +++ b/netbox/templates/utilities/obj_bulk_add_component.html @@ -14,21 +14,7 @@
-
Selected Devices
- - - - - - - {% for device in selected_devices %} - - - - - - {% endfor %} -
DeviceTypeRole
{{ device }}{{ device.device_type.full_name }}{{ device.device_role }}
+ {% include 'inc/table.html' %}
diff --git a/netbox/templates/virtualization/inc/virtualmachine_table.html b/netbox/templates/virtualization/inc/virtualmachine_table.html new file mode 100644 index 000000000..ce249593e --- /dev/null +++ b/netbox/templates/virtualization/inc/virtualmachine_table.html @@ -0,0 +1,14 @@ +{% extends 'utilities/obj_table.html' %} + +{% block extra_actions %} + {% if perms.virtualization.change_virtualmachine %} +
+ + +
+ {% endif %} +{% endblock %} \ No newline at end of file diff --git a/netbox/templates/virtualization/virtualmachine_list.html b/netbox/templates/virtualization/virtualmachine_list.html index 38e842e74..a771dfbd9 100644 --- a/netbox/templates/virtualization/virtualmachine_list.html +++ b/netbox/templates/virtualization/virtualmachine_list.html @@ -17,7 +17,7 @@

{% block title %}Virtual Machines{% endblock %}

- {% include 'utilities/obj_table.html' with bulk_edit_url='virtualization:virtualmachine_bulk_edit' bulk_delete_url='virtualization:virtualmachine_bulk_delete' %} + {% include 'virtualization/inc/virtualmachine_table.html' with bulk_edit_url='virtualization:virtualmachine_bulk_edit' bulk_delete_url='virtualization:virtualmachine_bulk_delete' %}
{% include 'inc/search_panel.html' %} diff --git a/netbox/utilities/views.py b/netbox/utilities/views.py index 4d9c0c675..1da5b85e2 100644 --- a/netbox/utilities/views.py +++ b/netbox/utilities/views.py @@ -705,6 +705,9 @@ class BulkDeleteView(View): # class ComponentCreateView(View): + """ + Add one or more components (e.g. interfaces, console ports, etc.) to a Device or VirtualMachine. + """ parent_model = None parent_field = None model = None @@ -786,3 +789,82 @@ class ComponentDeleteView(ObjectDeleteView): def get_return_url(self, request, obj): return getattr(obj, self.parent_field).get_absolute_url() + + +class BulkComponentCreateView(View): + """ + Add one or more components (e.g. interfaces, console ports, etc.) to a set of Devices or VirtualMachines. + """ + parent_model = None + parent_field = None + form = None + model = None + model_form = None + filter = None + table = None + template_name = 'utilities/obj_bulk_add_component.html' + default_return_url = 'home' + + def post(self, request): + + # Are we editing *all* objects in the queryset or just a selected subset? + if request.POST.get('_all') and self.filter is not None: + pk_list = [obj.pk for obj in self.filter(request.GET, self.model.objects.only('pk')).qs] + else: + pk_list = [int(pk) for pk in request.POST.getlist('pk')] + + # Determine URL to redirect users upon modification of objects + posted_return_url = request.POST.get('return_url') + if posted_return_url and is_safe_url(url=posted_return_url, host=request.get_host()): + return_url = posted_return_url + else: + return_url = reverse(self.default_return_url) + + selected_objects = self.parent_model.objects.filter(pk__in=pk_list) + if not selected_objects: + messages.warning(request, "No {} were selected.".format(self.parent_model._meta.verbose_name_plural)) + return redirect(return_url) + table = self.table(selected_objects) + + if '_create' in request.POST: + form = self.form(request.POST) + if form.is_valid(): + + new_components = [] + data = deepcopy(form.cleaned_data) + for obj in data['pk']: + + names = data['name_pattern'] + for name in names: + component_data = { + self.parent_field: obj.pk, + 'name': name, + } + component_data.update(data) + component_form = self.model_form(component_data) + if component_form.is_valid(): + new_components.append(component_form.save(commit=False)) + else: + for field, errors in component_form.errors.as_data().items(): + for e in errors: + form.add_error(field, '{} {}: {}'.format(obj, name, ', '.join(e))) + + if not form.errors: + self.model.objects.bulk_create(new_components) + messages.success(request, "Added {} {} to {} {}.".format( + len(new_components), + self.model._meta.verbose_name_plural, + len(form.cleaned_data['pk']), + self.parent_model._meta.verbose_name_plural + )) + return redirect(return_url) + + else: + form = self.form(initial={'pk': pk_list}) + + return render(request, self.template_name, { + 'form': form, + 'component_name': self.model._meta.verbose_name_plural, + 'table': table, + 'return_url': reverse('dcim:device_list'), + }) diff --git a/netbox/virtualization/forms.py b/netbox/virtualization/forms.py index b1ce920e4..661d6094d 100644 --- a/netbox/virtualization/forms.py +++ b/netbox/virtualization/forms.py @@ -301,3 +301,19 @@ class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm): class Meta: nullable_fields = ['mtu', 'description'] + + +# +# Bulk VirtualMachine component creation +# + +class VirtualMachineBulkAddComponentForm(BootstrapMixin, forms.Form): + pk = forms.ModelMultipleChoiceField(queryset=VirtualMachine.objects.all(), widget=forms.MultipleHiddenInput) + name_pattern = ExpandableNameField(label='Name') + + +class VirtualMachineBulkAddInterfaceForm(VirtualMachineBulkAddComponentForm): + form_factor = forms.ChoiceField(choices=VIFACE_FF_CHOICES) + enabled = forms.BooleanField(required=False, initial=True) + mtu = forms.IntegerField(required=False, min_value=1, max_value=32767, label='MTU') + description = forms.CharField(max_length=100, required=False) diff --git a/netbox/virtualization/urls.py b/netbox/virtualization/urls.py index bc8c44767..9d976f300 100644 --- a/netbox/virtualization/urls.py +++ b/netbox/virtualization/urls.py @@ -45,7 +45,7 @@ urlpatterns = [ url(r'^virtual-machines/(?P\d+)/services/assign/$', ServiceCreateView.as_view(), name='virtualmachine_service_assign'), # VM interfaces - # url(r'^virtual-machines/interfaces/add/$', views.VMBulkAddInterfaceView.as_view(), name='vm_bulk_add_interface'), + url(r'^virtual-machines/interfaces/add/$', views.VirtualMachineBulkAddInterfaceView.as_view(), name='virtualmachine_bulk_add_interface'), url(r'^virtual-machines/(?P\d+)/interfaces/add/$', views.InterfaceCreateView.as_view(), name='interface_add'), url(r'^virtual-machines/(?P\d+)/interfaces/edit/$', views.InterfaceBulkEditView.as_view(), name='interface_bulk_edit'), url(r'^virtual-machines/(?P\d+)/interfaces/delete/$', views.InterfaceBulkDeleteView.as_view(), name='interface_bulk_delete'), diff --git a/netbox/virtualization/views.py b/netbox/virtualization/views.py index c8b3ea8f2..79629bf98 100644 --- a/netbox/virtualization/views.py +++ b/netbox/virtualization/views.py @@ -11,8 +11,8 @@ from dcim.models import Device, Interface from dcim.tables import DeviceTable from ipam.models import Service from utilities.views import ( - BulkDeleteView, BulkEditView, BulkImportView, ComponentCreateView, ComponentDeleteView, ComponentEditView, - ObjectDeleteView, ObjectEditView, ObjectListView, + BulkComponentCreateView, BulkDeleteView, BulkEditView, BulkImportView, ComponentCreateView, ComponentDeleteView, + ComponentEditView, ObjectDeleteView, ObjectEditView, ObjectListView, ) from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine from . import filters @@ -344,3 +344,17 @@ class InterfaceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): cls = Interface parent_cls = VirtualMachine table = tables.InterfaceTable + + +# +# Bulk Device component creation +# + +class VirtualMachineBulkAddInterfaceView(PermissionRequiredMixin, BulkComponentCreateView): + permission_required = 'dcim.add_interface' + parent_model = VirtualMachine + parent_field = 'virtual_machine' + form = forms.VirtualMachineBulkAddInterfaceForm + model = Interface + model_form = forms.InterfaceForm + table = tables.VirtualMachineTable