Browse Source

Implemented bulk interface creation for virtual machines

Jeremy Stretch 7 years ago
parent
commit
700194b80d

+ 6 - 5
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)
 
 
 #

+ 32 - 74
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
 
 
 #

+ 1 - 15
netbox/templates/dcim/device_bulk_add_component.html

@@ -14,21 +14,7 @@
     <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.full_name }}</td>
-                            <td>{{ device.device_role }}</td>
-                        </tr>
-                    {% endfor %}
-                </table>
+                {% include 'inc/table.html' %}
             </div>
         </div>
         <div class="col-md-5">

+ 14 - 0
netbox/templates/virtualization/inc/virtualmachine_table.html

@@ -0,0 +1,14 @@
+{% extends 'utilities/obj_table.html' %}
+
+{% block extra_actions %}
+    {% if perms.virtualization.change_virtualmachine %}
+        <div class="btn-group">
+            <button type="button" class="btn btn-sm btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+                <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add Components <span class="caret"></span>
+            </button>
+            <ul class="dropdown-menu">
+                {% if perms.dcim.add_interface %}<li><a href="{% url 'virtualization:virtualmachine_bulk_add_interface' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="formaction">Interfaces</a></li>{% endif %}
+            </ul>
+        </div>
+    {% endif %}
+{% endblock %}

+ 1 - 1
netbox/templates/virtualization/virtualmachine_list.html

@@ -17,7 +17,7 @@
 <h1>{% block title %}Virtual Machines{% endblock %}</h1>
 <div class="row">
 	<div class="col-md-9">
-        {% 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' %}
     </div>
     <div class="col-md-3">
 		{% include 'inc/search_panel.html' %}

+ 82 - 0
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'),
+        })

+ 16 - 0
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)

+ 1 - 1
netbox/virtualization/urls.py

@@ -45,7 +45,7 @@ urlpatterns = [
     url(r'^virtual-machines/(?P<virtualmachine>\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<pk>\d+)/interfaces/add/$', views.InterfaceCreateView.as_view(), name='interface_add'),
     url(r'^virtual-machines/(?P<pk>\d+)/interfaces/edit/$', views.InterfaceBulkEditView.as_view(), name='interface_bulk_edit'),
     url(r'^virtual-machines/(?P<pk>\d+)/interfaces/delete/$', views.InterfaceBulkDeleteView.as_view(), name='interface_bulk_delete'),

+ 16 - 2
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