Added initial UI views for virtual chassis assignment

This commit is contained in:
Jeremy Stretch 2017-11-29 12:58:36 -05:00
parent 3b801d43bc
commit 5f91413023
8 changed files with 191 additions and 6 deletions

View File

@ -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']

View File

@ -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({

View File

@ -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()

View File

@ -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'),
]

View File

@ -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'

View File

@ -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>

View File

@ -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 %}

View File

@ -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 %}