mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-22 20:12:00 -06:00
Added initial UI views for virtual chassis assignment
This commit is contained in:
parent
3b801d43bc
commit
5f91413023
@ -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']
|
||||
|
@ -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({
|
||||
|
@ -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()
|
||||
|
@ -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'),
|
||||
|
||||
]
|
||||
|
@ -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'
|
||||
|
@ -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>
|
||||
|
@ -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
netbox/templates/dcim/virtualchassis_add.html
Normal file
56
netbox/templates/dcim/virtualchassis_add.html
Normal 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 %}
|
Loading…
Reference in New Issue
Block a user