Browse Source

Added initial UI views for virtual chassis assignment

Jeremy Stretch 7 years ago
parent
commit
5f91413023

+ 24 - 1
netbox/dcim/forms.py

@@ -30,7 +30,7 @@ from .models import (
     DeviceBay, DeviceBayTemplate, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate,
     Device, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer, InventoryItem,
     Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation,
-    RackRole, Region, Site, VirtualChassis
+    RackRole, Region, Site, VCMembership, VirtualChassis
 )
 from .constants import *
 
@@ -2181,3 +2181,26 @@ class VirtualChassisForm(BootstrapMixin, forms.ModelForm):
     class Meta:
         model = VirtualChassis
         fields = ['domain']
+
+
+class DeviceSelectionForm(forms.Form):
+    pk = forms.ModelMultipleChoiceField(queryset=Device.objects.all(), widget=forms.MultipleHiddenInput)
+
+
+class VirtualChassisCreateForm(BootstrapMixin, forms.ModelForm):
+    master = forms.ModelChoiceField(queryset=Device.objects.all())
+
+    class Meta:
+        model = VirtualChassis
+        fields = ['master', 'domain']
+
+    def __init__(self, candidate_pks, *args, **kwargs):
+        super(VirtualChassisCreateForm, self).__init__(*args, **kwargs)
+        self.fields['master'].queryset = Device.objects.filter(pk__in=candidate_pks)
+
+
+class VCMembershipForm(BootstrapMixin, forms.ModelForm):
+
+    class Meta:
+        model = VCMembership
+        fields = ['device', 'position', 'priority']

+ 8 - 1
netbox/dcim/models.py

@@ -1030,6 +1030,13 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
         else:
             return None
 
+    @property
+    def virtual_chassis(self):
+        try:
+            return VCMembership.objects.get(device=self).virtual_chassis
+        except VCMembership.DoesNotExist:
+            return None
+
     def get_children(self):
         """
         Return the set of child Devices installed in DeviceBays within this Device.
@@ -1534,7 +1541,7 @@ class VCMembership(models.Model):
     def clean(self):
 
         # Check for master conflicts
-        if self.virtual_chassis and self.is_master:
+        if getattr(self, 'virtual_chassis', None) and self.is_master:
             master_conflict = VCMembership.objects.filter(virtual_chassis=self.virtual_chassis).first()
             if master_conflict:
                 raise ValidationError({

+ 4 - 3
netbox/dcim/signals.py

@@ -11,6 +11,7 @@ def delete_empty_vc(instance, **kwargs):
     """
     When the last VCMembership of a VirtualChassis has been deleted, delete the VirtualChassis as well.
     """
-    virtual_chassis = instance.virtual_chassis
-    if not VCMembership.objects.filter(virtual_chassis=virtual_chassis).exists():
-        virtual_chassis.delete()
+    pass
+    # virtual_chassis = instance.virtual_chassis
+    # if not VCMembership.objects.filter(virtual_chassis=virtual_chassis).exists():
+    #     virtual_chassis.delete()

+ 2 - 0
netbox/dcim/urls.py

@@ -209,6 +209,8 @@ urlpatterns = [
 
     # Virtual chassis
     url(r'^virtual-chassis/$', views.VirtualChassisListView.as_view(), name='virtualchassis_list'),
+    url(r'^virtual-chassis/add/$', views.VirtualChassisCreateView.as_view(), name='virtualchassis_add'),
     url(r'^virtual-chassis/(?P<pk>\d+)/edit/$', views.VirtualChassisEditView.as_view(), name='virtualchassis_edit'),
+    url(r'^virtual-chassis/(?P<pk>\d+)/delete/$', views.VirtualChassisDeleteView.as_view(), name='virtualchassis_delete'),
 
 ]

+ 59 - 1
netbox/dcim/views.py

@@ -6,7 +6,9 @@ from django.contrib import messages
 from django.contrib.auth.decorators import permission_required
 from django.contrib.auth.mixins import PermissionRequiredMixin
 from django.core.paginator import EmptyPage, PageNotAnInteger
+from django.db import transaction
 from django.db.models import Count, Q
+from django.forms import ModelChoiceField, modelformset_factory
 from django.http import HttpResponseRedirect
 from django.shortcuts import get_object_or_404, redirect, render
 from django.urls import reverse
@@ -31,7 +33,7 @@ from .models import (
     ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
     DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer,
     InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
-    RackReservation, RackRole, Region, Site, VirtualChassis
+    RackReservation, RackRole, Region, Site, VCMembership, VirtualChassis,
 )
 
 
@@ -832,6 +834,9 @@ class DeviceView(View):
         services = Service.objects.filter(device=device)
         secrets = device.secrets.all()
 
+        # Find virtual chassis memberships
+        vc_memberships = VCMembership.objects.filter(virtual_chassis=device.virtual_chassis).select_related('device')
+
         # Find up to ten devices in the same site with the same functional role for quick reference.
         related_devices = Device.objects.filter(
             site=device.site, device_role=device.device_role
@@ -854,6 +859,7 @@ class DeviceView(View):
             'device_bays': device_bays,
             'services': services,
             'secrets': secrets,
+            'vc_memberships': vc_memberships,
             'related_devices': related_devices,
             'show_graphs': show_graphs,
         })
@@ -1841,6 +1847,52 @@ class VirtualChassisListView(ObjectListView):
     template_name = 'dcim/virtualchassis_list.html'
 
 
+class VirtualChassisCreateView(PermissionRequiredMixin, View):
+    permission_required = 'dcim.add_virtualchassis'
+
+    def post(self, request):
+
+        # Get the list of devices being added to a VirtualChassis
+        pk_form = forms.DeviceSelectionForm(request.POST)
+        pk_form.full_clean()
+        device_list = pk_form.cleaned_data['pk']
+
+        # Generate a custom VCMembershipForm where the device field is limited to only the selected devices
+        class _VCMembershipForm(forms.VCMembershipForm):
+            device = ModelChoiceField(queryset=Device.objects.filter(pk__in=device_list))
+
+        VCMembershipFormSet = modelformset_factory(model=VCMembership, form=_VCMembershipForm, extra=len(device_list))
+
+        if '_create' in request.POST:
+
+            vc_form = forms.VirtualChassisCreateForm(device_list, request.POST)
+            formset = VCMembershipFormSet(request.POST)
+
+            if vc_form.is_valid() and formset.is_valid():
+                with transaction.atomic():
+                    virtual_chassis = vc_form.save()
+                    vc_memberships = formset.save(commit=False)
+                    for vcm in vc_memberships:
+                        vcm.virtual_chassis = virtual_chassis
+                        if vcm.device == vc_form.cleaned_data['master']:
+                            vcm.is_master = True
+                        vcm.save()
+                    return redirect(vc_form.cleaned_data['master'].get_absolute_url())
+
+        else:
+
+            vc_form = forms.VirtualChassisCreateForm(device_list)
+            initial_data = [{'device': pk, 'position': i} for i, pk in enumerate(device_list, start=1)]
+            formset = VCMembershipFormSet(queryset=VCMembership.objects.none(), initial=initial_data)
+
+        return render(request, 'dcim/virtualchassis_add.html', {
+            'pk_form': pk_form,
+            'vc_form': vc_form,
+            'formset': formset,
+            'return_url': reverse('dcim:device_list'),
+        })
+
+
 class VirtualChassisEditView(PermissionRequiredMixin, ObjectEditView):
     permission_required = 'dcim.change_virtualchassis'
     model = VirtualChassis
@@ -1848,3 +1900,9 @@ class VirtualChassisEditView(PermissionRequiredMixin, ObjectEditView):
 
     def get_return_url(self, request, obj):
         return reverse('dcim:virtualchassis_list')
+
+
+class VirtualChassisDeleteView(PermissionRequiredMixin, ObjectDeleteView):
+    permission_required = 'dcim.delete_virtualchassis'
+    model = VirtualChassis
+    default_return_url = 'dcim:device_list'

+ 33 - 0
netbox/templates/dcim/device.html

@@ -98,6 +98,39 @@
                 </tr>
             </table>
         </div>
+        {% if vc_memberships %}
+            <div class="panel panel-default">
+                <div class="panel-heading">
+                    <strong>Virtual Chassis</strong>
+                </div>
+                <table class="table table-hover panel-body attr-table">
+                    <tr>
+                        <th>Device</th>
+                        <th>Position</th>
+                        <th>Master</th>
+                        <th>Priority</th>
+                    </tr>
+                    {% for vcm in vc_memberships %}
+                        <tr{% if vcm.device == device %} class="success"{% endif %}>
+                            <td>
+                                <a href="{{ vcm.device.get_absolute_url }}">{{ vcm.device }}</a>
+                            </td>
+                            <td>{{ vcm.position }}</td>
+                            <td>{{ vcm.is_master }}</td>
+                            <td>{{ vcm.priority|default:"" }}</td>
+                            <td>
+                        </tr>
+                    {% endfor %}
+                </table>
+                {% if perms.dcim.delete_virtualchassis %}
+                    <div class="panel-footer text-right">
+                        <a href="{% url 'dcim:virtualchassis_delete' pk=device.virtual_chassis.pk %}" class="btn btn-danger btn-xs">
+                            <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete Virtual Chassis
+                        </a>
+                    </div>
+                {% endif %}
+            </div>
+        {% endif %}
         <div class="panel panel-default">
             <div class="panel-heading">
                 <strong>Management</strong>

+ 5 - 0
netbox/templates/dcim/inc/device_table.html

@@ -16,4 +16,9 @@
             </ul>
         </div>
     {% endif %}
+    {% if perms.dcim.add_virtualchassis %}
+        <button type="submit" name="_edit" formaction="{% url 'dcim:virtualchassis_add' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-primary btn-sm">
+            <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Create Virtual Chassis
+        </button>
+    {% endif %}
 {% endblock %}

+ 56 - 0
netbox/templates/dcim/virtualchassis_add.html

@@ -0,0 +1,56 @@
+{% extends '_base.html' %}
+{% load form_helpers %}
+
+{% block content %}
+    <form action="" method="post" enctype="multipart/form-data" class="form form-horizontal">
+        {% csrf_token %}
+        {{ pk_form.pk }}
+        {{ formset.management_form }}
+        <div class="row">
+            <div class="col-md-6 col-md-offset-3">
+                <h3>{% block title %}New Virtual Chassis{% endblock %}</h3>
+                {% if vc_form.non_field_errors %}
+                    <div class="panel panel-danger">
+                        <div class="panel-heading"><strong>Errors</strong></div>
+                        <div class="panel-body">
+                            {{ vc_form.non_field_errors }}
+                        </div>
+                    </div>
+                {% endif %}
+                <div class="panel panel-default">
+                    <div class="panel-heading"><strong>Virtual Chassis</strong></div>
+                    <div class="table panel-body">
+                        {% render_form vc_form %}
+                    </div>
+                </div>
+                <div class="panel panel-default">
+                    <div class="panel-heading"><strong>Members</strong></div>
+                    <table class="table panel-body">
+                        <thead>
+                            <tr>
+                                <th>Device</th>
+                                <th>Position</th>
+                                <th>Priority</th>
+                            </tr>
+                        </thead>
+                        <tbody>
+                            {% for form in formset %}
+                                <tr>
+                                    <td>{{ form.device }}</td>
+                                    <td>{{ form.position }}</td>
+                                    <td>{{ form.priority }}</td>
+                                </tr>
+                            {% endfor %}
+                        </tbody>
+                    </table>
+                </div>
+            </div>
+        </div>
+        <div class="row">
+            <div class="col-md-6 col-md-offset-3 text-right">
+                <button type="submit" name="_create" class="btn btn-primary">Create</button>
+                <a href="{{ return_url }}" class="btn btn-default">Cancel</a>
+            </div>
+        </div>
+    </form>
+{% endblock %}