+ {% 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