diff --git a/netbox/templates/virtualization/cluster_add_virtualmachines.html b/netbox/templates/virtualization/cluster_add_virtualmachines.html
new file mode 100644
index 000000000..48339ebdd
--- /dev/null
+++ b/netbox/templates/virtualization/cluster_add_virtualmachines.html
@@ -0,0 +1,37 @@
+{% extends 'base.html' %}
+{% load static %}
+{% load form_helpers %}
+
+{% block content %}
+
+{% endblock %}
diff --git a/netbox/virtualization/forms.py b/netbox/virtualization/forms.py
index ce4ca3e3c..64ec8a919 100644
--- a/netbox/virtualization/forms.py
+++ b/netbox/virtualization/forms.py
@@ -276,6 +276,28 @@ class ClusterRemoveDevicesForm(ConfirmationForm):
)
+class ClusterAddVirtualMachinesForm(BootstrapMixin, forms.Form):
+ virtualmachines = DynamicModelMultipleChoiceField(
+ queryset=VirtualMachine.objects.all(),
+ )
+
+ class Meta:
+ fields = [
+ 'virtualmachines',
+ ]
+
+ def __init__(self, cluster, *args, **kwargs):
+
+ self.cluster = cluster
+
+ super().__init__(*args, **kwargs)
+
+ self.fields['virtualmachines'].choices = []
+
+ def clean(self):
+ super().clean()
+
+
#
# Virtual Machines
#
diff --git a/netbox/virtualization/urls.py b/netbox/virtualization/urls.py
index 3d6f07566..451bca41e 100644
--- a/netbox/virtualization/urls.py
+++ b/netbox/virtualization/urls.py
@@ -38,6 +38,7 @@ urlpatterns = [
path('clusters/
/changelog/', ObjectChangeLogView.as_view(), name='cluster_changelog', kwargs={'model': Cluster}),
path('clusters//devices/add/', views.ClusterAddDevicesView.as_view(), name='cluster_add_devices'),
path('clusters//devices/remove/', views.ClusterRemoveDevicesView.as_view(), name='cluster_remove_devices'),
+ path('clusters//virtual-machines/add/', views.ClusterAddVirtualMachinesView.as_view(), name='cluster_add_virtualmachines'),
# Virtual machines
path('virtual-machines/', views.VirtualMachineListView.as_view(), name='virtualmachine_list'),
diff --git a/netbox/virtualization/views.py b/netbox/virtualization/views.py
index 2b7ae3a13..267eb076d 100644
--- a/netbox/virtualization/views.py
+++ b/netbox/virtualization/views.py
@@ -14,6 +14,7 @@ from utilities.views import (
BulkComponentCreateView, BulkDeleteView, BulkEditView, BulkImportView, BulkRenameView, ComponentCreateView,
ObjectView, ObjectDeleteView, ObjectEditView, ObjectListView,
)
+from virtualization.tables import VirtualMachineTable
from . import filters, forms, tables
from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
@@ -103,11 +104,18 @@ class ClusterView(ObjectView):
queryset = Cluster.objects.all()
def get(self, request, pk):
+ virtual_machines = VirtualMachine.objects.restrict(request.user)
+
self.queryset = self.queryset.prefetch_related(
- Prefetch('virtual_machines', queryset=VirtualMachine.objects.restrict(request.user))
+ Prefetch('virtual_machines', queryset=virtual_machines)
)
cluster = get_object_or_404(self.queryset, pk=pk)
+
+ virtual_machine_table = VirtualMachineTable(list(virtual_machines.filter(cluster=cluster)), orderable=False)
+ if request.user.has_perm('virtualization.change_cluster'):
+ virtual_machine_table.columns.show('pk')
+
devices = Device.objects.restrict(request.user, 'view').filter(cluster=cluster).prefetch_related(
'site', 'rack', 'tenant', 'device_type__manufacturer'
)
@@ -118,6 +126,7 @@ class ClusterView(ObjectView):
return render(request, 'virtualization/cluster.html', {
'cluster': cluster,
'device_table': device_table,
+ 'virtual_machine_table': virtual_machine_table,
})
@@ -232,6 +241,47 @@ class ClusterRemoveDevicesView(ObjectEditView):
})
+class ClusterAddVirtualMachinesView(ObjectEditView):
+ queryset = Cluster.objects.all()
+ form = forms.ClusterAddVirtualMachinesForm
+ template_name = 'virtualization/cluster_add_virtualmachines.html'
+
+ def get(self, request, pk):
+ cluster = get_object_or_404(self.queryset, pk=pk)
+ form = self.form(cluster, initial=request.GET)
+
+ return render(request, self.template_name, {
+ 'cluster': cluster,
+ 'form': form,
+ 'return_url': reverse('virtualization:cluster', kwargs={'pk': pk}),
+ })
+
+ def post(self, request, pk):
+ cluster = get_object_or_404(self.queryset, pk=pk)
+ form = self.form(cluster, request.POST)
+
+ if form.is_valid():
+
+ virtualmachine_pks = form.cleaned_data['virtualmachines']
+ with transaction.atomic():
+
+ # Assign the selected VirtualMachines to the Cluster
+ for virtualmachine in VirtualMachine.objects.filter(pk__in=virtualmachine_pks):
+ virtualmachine.cluster = cluster
+ virtualmachine.save()
+
+ messages.success(request, "Added {} virtual machines to cluster {}".format(
+ len(virtualmachine_pks), cluster
+ ))
+ return redirect(cluster.get_absolute_url())
+
+ return render(request, self.template_name, {
+ 'cluster': cluster,
+ 'form': form,
+ 'return_url': cluster.get_absolute_url(),
+ })
+
+
#
# Virtual machines
#