mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-24 17:38:37 -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,
|
DeviceBay, DeviceBayTemplate, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate,
|
||||||
Device, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer, InventoryItem,
|
Device, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer, InventoryItem,
|
||||||
Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation,
|
Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation,
|
||||||
RackRole, Region, Site, VirtualChassis
|
RackRole, Region, Site, VCMembership, VirtualChassis
|
||||||
)
|
)
|
||||||
from .constants import *
|
from .constants import *
|
||||||
|
|
||||||
@ -2181,3 +2181,26 @@ class VirtualChassisForm(BootstrapMixin, forms.ModelForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = VirtualChassis
|
model = VirtualChassis
|
||||||
fields = ['domain']
|
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:
|
else:
|
||||||
return None
|
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):
|
def get_children(self):
|
||||||
"""
|
"""
|
||||||
Return the set of child Devices installed in DeviceBays within this Device.
|
Return the set of child Devices installed in DeviceBays within this Device.
|
||||||
@ -1534,7 +1541,7 @@ class VCMembership(models.Model):
|
|||||||
def clean(self):
|
def clean(self):
|
||||||
|
|
||||||
# Check for master conflicts
|
# 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()
|
master_conflict = VCMembership.objects.filter(virtual_chassis=self.virtual_chassis).first()
|
||||||
if master_conflict:
|
if master_conflict:
|
||||||
raise ValidationError({
|
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.
|
When the last VCMembership of a VirtualChassis has been deleted, delete the VirtualChassis as well.
|
||||||
"""
|
"""
|
||||||
virtual_chassis = instance.virtual_chassis
|
pass
|
||||||
if not VCMembership.objects.filter(virtual_chassis=virtual_chassis).exists():
|
# virtual_chassis = instance.virtual_chassis
|
||||||
virtual_chassis.delete()
|
# if not VCMembership.objects.filter(virtual_chassis=virtual_chassis).exists():
|
||||||
|
# virtual_chassis.delete()
|
||||||
|
@ -209,6 +209,8 @@ urlpatterns = [
|
|||||||
|
|
||||||
# Virtual chassis
|
# Virtual chassis
|
||||||
url(r'^virtual-chassis/$', views.VirtualChassisListView.as_view(), name='virtualchassis_list'),
|
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+)/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.decorators import permission_required
|
||||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||||
from django.core.paginator import EmptyPage, PageNotAnInteger
|
from django.core.paginator import EmptyPage, PageNotAnInteger
|
||||||
|
from django.db import transaction
|
||||||
from django.db.models import Count, Q
|
from django.db.models import Count, Q
|
||||||
|
from django.forms import ModelChoiceField, modelformset_factory
|
||||||
from django.http import HttpResponseRedirect
|
from django.http import HttpResponseRedirect
|
||||||
from django.shortcuts import get_object_or_404, redirect, render
|
from django.shortcuts import get_object_or_404, redirect, render
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
@ -31,7 +33,7 @@ from .models import (
|
|||||||
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||||
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer,
|
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer,
|
||||||
InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
|
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)
|
services = Service.objects.filter(device=device)
|
||||||
secrets = device.secrets.all()
|
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.
|
# Find up to ten devices in the same site with the same functional role for quick reference.
|
||||||
related_devices = Device.objects.filter(
|
related_devices = Device.objects.filter(
|
||||||
site=device.site, device_role=device.device_role
|
site=device.site, device_role=device.device_role
|
||||||
@ -854,6 +859,7 @@ class DeviceView(View):
|
|||||||
'device_bays': device_bays,
|
'device_bays': device_bays,
|
||||||
'services': services,
|
'services': services,
|
||||||
'secrets': secrets,
|
'secrets': secrets,
|
||||||
|
'vc_memberships': vc_memberships,
|
||||||
'related_devices': related_devices,
|
'related_devices': related_devices,
|
||||||
'show_graphs': show_graphs,
|
'show_graphs': show_graphs,
|
||||||
})
|
})
|
||||||
@ -1841,6 +1847,52 @@ class VirtualChassisListView(ObjectListView):
|
|||||||
template_name = 'dcim/virtualchassis_list.html'
|
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):
|
class VirtualChassisEditView(PermissionRequiredMixin, ObjectEditView):
|
||||||
permission_required = 'dcim.change_virtualchassis'
|
permission_required = 'dcim.change_virtualchassis'
|
||||||
model = VirtualChassis
|
model = VirtualChassis
|
||||||
@ -1848,3 +1900,9 @@ class VirtualChassisEditView(PermissionRequiredMixin, ObjectEditView):
|
|||||||
|
|
||||||
def get_return_url(self, request, obj):
|
def get_return_url(self, request, obj):
|
||||||
return reverse('dcim:virtualchassis_list')
|
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>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</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 panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<strong>Management</strong>
|
<strong>Management</strong>
|
||||||
|
@ -16,4 +16,9 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% 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 %}
|
{% 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