mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-16 04:02:52 -06:00
Initial work on virtual chassis support
This commit is contained in:
parent
7e475511b6
commit
55e07c1c9a
@ -14,7 +14,7 @@ from dcim.models import (
|
|||||||
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||||
DeviceBayTemplate, DeviceType, DeviceRole, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer,
|
DeviceBayTemplate, DeviceType, DeviceRole, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer,
|
||||||
InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
|
InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
|
||||||
RackReservation, RackRole, Region, Site,
|
RackReservation, RackRole, Region, Site, VirtualChassis, VCMembership
|
||||||
)
|
)
|
||||||
from extras.api.customfields import CustomFieldModelSerializer
|
from extras.api.customfields import CustomFieldModelSerializer
|
||||||
from ipam.models import IPAddress, VLAN
|
from ipam.models import IPAddress, VLAN
|
||||||
@ -799,3 +799,52 @@ class WritableInterfaceConnectionSerializer(ValidatedModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = InterfaceConnection
|
model = InterfaceConnection
|
||||||
fields = ['id', 'interface_a', 'interface_b', 'connection_status']
|
fields = ['id', 'interface_a', 'interface_b', 'connection_status']
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Virtual chassis
|
||||||
|
#
|
||||||
|
|
||||||
|
class VirtualChassisSerializer(serializers.ModelSerializer):
|
||||||
|
site = NestedSiteSerializer()
|
||||||
|
master = NestedDeviceSerializer()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = VirtualChassis
|
||||||
|
fields = ['id', 'site', 'domain', 'master']
|
||||||
|
|
||||||
|
|
||||||
|
class NestedVirtualChassisSerializer(serializers.ModelSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualchassis-detail')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = VirtualChassis
|
||||||
|
fields = ['id', 'url']
|
||||||
|
|
||||||
|
|
||||||
|
class WritableVirtualChassisSerializer(ValidatedModelSerializer):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = VirtualChassis
|
||||||
|
fields = ['id', 'site', 'domain', 'master']
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Virtual chassis memberships
|
||||||
|
#
|
||||||
|
|
||||||
|
class VCMembershipSerializer(serializers.ModelSerializer):
|
||||||
|
virtual_chassis = NestedVirtualChassisSerializer()
|
||||||
|
device = NestedDeviceSerializer()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = VCMembership
|
||||||
|
fields = ['id', 'virtual_chassis', 'device', 'master_enabled', 'position', 'priority']
|
||||||
|
|
||||||
|
|
||||||
|
class WritableVCMembershipSerializer(serializers.ModelSerializer):
|
||||||
|
virtual_chassis = serializers.PrimaryKeyRelatedField(queryset=VirtualChassis.objects.all(), required=False)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = VCMembership
|
||||||
|
fields = ['id', 'virtual_chassis', 'device', 'master_enabled', 'position', 'priority']
|
||||||
|
@ -60,6 +60,10 @@ router.register(r'console-connections', views.ConsoleConnectionViewSet, base_nam
|
|||||||
router.register(r'power-connections', views.PowerConnectionViewSet, base_name='powerconnections')
|
router.register(r'power-connections', views.PowerConnectionViewSet, base_name='powerconnections')
|
||||||
router.register(r'interface-connections', views.InterfaceConnectionViewSet)
|
router.register(r'interface-connections', views.InterfaceConnectionViewSet)
|
||||||
|
|
||||||
|
# Virtual chassis
|
||||||
|
router.register(r'virtual-chassis', views.VirtualChassisViewSet)
|
||||||
|
router.register(r'vc-memberships', views.VCMembershipViewSet)
|
||||||
|
|
||||||
# Miscellaneous
|
# Miscellaneous
|
||||||
router.register(r'connected-device', views.ConnectedDeviceViewSet, base_name='connected-device')
|
router.register(r'connected-device', views.ConnectedDeviceViewSet, base_name='connected-device')
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ from dcim.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,
|
RackReservation, RackRole, Region, Site, VCMembership, VirtualChassis
|
||||||
)
|
)
|
||||||
from extras.api.serializers import RenderedGraphSerializer
|
from extras.api.serializers import RenderedGraphSerializer
|
||||||
from extras.api.views import CustomFieldModelViewSet
|
from extras.api.views import CustomFieldModelViewSet
|
||||||
@ -396,6 +396,24 @@ class InterfaceConnectionViewSet(ModelViewSet):
|
|||||||
filter_class = filters.InterfaceConnectionFilter
|
filter_class = filters.InterfaceConnectionFilter
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Virtual chassis
|
||||||
|
#
|
||||||
|
|
||||||
|
class VirtualChassisViewSet(ModelViewSet):
|
||||||
|
queryset = VirtualChassis.objects.select_related('master')
|
||||||
|
serializer_class = serializers.VirtualChassisSerializer
|
||||||
|
write_serializer_class = serializers.WritableVirtualChassisSerializer
|
||||||
|
# filter_class = filters.VirtualChassisFilter
|
||||||
|
|
||||||
|
|
||||||
|
class VCMembershipViewSet(ModelViewSet):
|
||||||
|
queryset = VCMembership.objects.select_related('virtual_chassis', 'device')
|
||||||
|
serializer_class = serializers.VCMembershipSerializer
|
||||||
|
write_serializer_class = serializers.WritableVCMembershipSerializer
|
||||||
|
# filter_class = filters.VCMembershipFilter
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Miscellaneous
|
# Miscellaneous
|
||||||
#
|
#
|
||||||
|
@ -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,
|
RackRole, Region, Site, VirtualChassis
|
||||||
)
|
)
|
||||||
from .constants import *
|
from .constants import *
|
||||||
|
|
||||||
@ -2170,3 +2170,14 @@ class InventoryItemForm(BootstrapMixin, forms.ModelForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = InventoryItem
|
model = InventoryItem
|
||||||
fields = ['name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description']
|
fields = ['name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description']
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Virtual chassis
|
||||||
|
#
|
||||||
|
|
||||||
|
class VirtualChassisForm(BootstrapMixin, forms.ModelForm):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = VirtualChassis
|
||||||
|
fields = ['domain']
|
||||||
|
48
netbox/dcim/migrations/0052_virtual_chassis.py
Normal file
48
netbox/dcim/migrations/0052_virtual_chassis.py
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.6 on 2017-11-17 20:39
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import django.core.validators
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('dcim', '0051_rackreservation_tenant'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='VCMembership',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('master_enabled', models.BooleanField(default=True)),
|
||||||
|
('position', models.PositiveSmallIntegerField(validators=[django.core.validators.MaxValueValidator(255)])),
|
||||||
|
('priority', models.PositiveSmallIntegerField(blank=True, null=True, validators=[django.core.validators.MaxValueValidator(255)])),
|
||||||
|
('device', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='vc_membership', to='dcim.Device')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'VC membership',
|
||||||
|
'ordering': ['virtual_chassis', 'position'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='VirtualChassis',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('domain', models.CharField(blank=True, max_length=30)),
|
||||||
|
('master', models.OneToOneField(on_delete=django.db.models.deletion.PROTECT, related_name='vc_master_for', to='dcim.Device')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='vcmembership',
|
||||||
|
name='virtual_chassis',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='memberships', to='dcim.VirtualChassis'),
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='vcmembership',
|
||||||
|
unique_together=set([('virtual_chassis', 'position')]),
|
||||||
|
),
|
||||||
|
]
|
@ -1479,3 +1479,65 @@ class InventoryItem(models.Model):
|
|||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Virtual chassis
|
||||||
|
#
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
|
class VirtualChassis(models.Model):
|
||||||
|
"""
|
||||||
|
A collection of Devices which operate with a shared control plane (e.g. a switch stack).
|
||||||
|
"""
|
||||||
|
domain = models.CharField(
|
||||||
|
max_length=30,
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
|
master = models.OneToOneField(
|
||||||
|
to='Device',
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
related_name='vc_master_for'
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return "{}?virtual_chassis={}".format(reverse('dcim:device_list'), self.pk)
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
|
||||||
|
# Check that the master Device is not already assigned to a VirtualChassis.
|
||||||
|
if VCMembership.objects.filter(device=self.master).exclude(virtual_chassis=self):
|
||||||
|
raise ValidationError("The master device is already assigned to a different virtual chassis.")
|
||||||
|
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
|
class VCMembership(models.Model):
|
||||||
|
"""
|
||||||
|
An attachment of a physical Device to a VirtualChassis.
|
||||||
|
"""
|
||||||
|
virtual_chassis = models.ForeignKey(
|
||||||
|
to='VirtualChassis',
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='memberships'
|
||||||
|
)
|
||||||
|
device = models.OneToOneField(
|
||||||
|
to='Device',
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='vc_membership'
|
||||||
|
)
|
||||||
|
master_enabled = models.BooleanField(
|
||||||
|
default=True
|
||||||
|
)
|
||||||
|
position = models.PositiveSmallIntegerField(
|
||||||
|
validators=[MaxValueValidator(255)]
|
||||||
|
)
|
||||||
|
priority = models.PositiveSmallIntegerField(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
validators=[MaxValueValidator(255)]
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['virtual_chassis', 'position']
|
||||||
|
unique_together = ['virtual_chassis', 'position']
|
||||||
|
verbose_name = 'VC membership'
|
||||||
|
@ -7,7 +7,7 @@ from utilities.tables import BaseTable, ToggleColumn
|
|||||||
from .models import (
|
from .models import (
|
||||||
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||||
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceTemplate, Manufacturer, Platform, PowerOutlet,
|
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceTemplate, Manufacturer, Platform, PowerOutlet,
|
||||||
PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, Region, Site,
|
PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, Region, Site, VirtualChassis
|
||||||
)
|
)
|
||||||
|
|
||||||
REGION_LINK = """
|
REGION_LINK = """
|
||||||
@ -111,6 +111,12 @@ UTILIZATION_GRAPH = """
|
|||||||
{% utilization_graph value %}
|
{% utilization_graph value %}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
VIRTUALCHASSIS_ACTIONS = """
|
||||||
|
{% if perms.dcim.change_virtualchassis %}
|
||||||
|
<a href="{% url 'dcim:virtualchassis_edit' pk=record.pk %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||||
|
{% endif %}
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Regions
|
# Regions
|
||||||
@ -523,3 +529,22 @@ class InterfaceConnectionTable(BaseTable):
|
|||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = Interface
|
model = Interface
|
||||||
fields = ('device_a', 'interface_a', 'device_b', 'interface_b')
|
fields = ('device_a', 'interface_a', 'device_b', 'interface_b')
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Virtual chassis
|
||||||
|
#
|
||||||
|
|
||||||
|
class VirtualChassisTable(BaseTable):
|
||||||
|
pk = ToggleColumn()
|
||||||
|
master = tables.LinkColumn()
|
||||||
|
member_count = tables.Column(verbose_name='Members')
|
||||||
|
actions = tables.TemplateColumn(
|
||||||
|
template_code=VIRTUALCHASSIS_ACTIONS,
|
||||||
|
attrs={'td': {'class': 'text-right'}},
|
||||||
|
verbose_name=''
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta(BaseTable.Meta):
|
||||||
|
model = VirtualChassis
|
||||||
|
fields = ('pk', 'master', 'domain', 'member_count', 'actions')
|
||||||
|
@ -207,4 +207,8 @@ urlpatterns = [
|
|||||||
url(r'^interface-connections/$', views.InterfaceConnectionsListView.as_view(), name='interface_connections_list'),
|
url(r'^interface-connections/$', views.InterfaceConnectionsListView.as_view(), name='interface_connections_list'),
|
||||||
url(r'^interface-connections/import/$', views.InterfaceConnectionsBulkImportView.as_view(), name='interface_connections_import'),
|
url(r'^interface-connections/import/$', views.InterfaceConnectionsBulkImportView.as_view(), name='interface_connections_import'),
|
||||||
|
|
||||||
|
# Virtual chassis
|
||||||
|
url(r'^virtual-chassis/$', views.VirtualChassisListView.as_view(), name='virtualchassis_list'),
|
||||||
|
url(r'^virtual-chassis/(?P<pk>\d+)/edit/$', views.VirtualChassisEditView.as_view(), name='virtualchassis_edit'),
|
||||||
|
|
||||||
]
|
]
|
||||||
|
@ -31,7 +31,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,
|
RackReservation, RackRole, Region, Site, VirtualChassis
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -1829,3 +1829,22 @@ class InventoryItemDeleteView(PermissionRequiredMixin, ComponentDeleteView):
|
|||||||
permission_required = 'dcim.delete_inventoryitem'
|
permission_required = 'dcim.delete_inventoryitem'
|
||||||
model = InventoryItem
|
model = InventoryItem
|
||||||
parent_field = 'device'
|
parent_field = 'device'
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Virtual chassis
|
||||||
|
#
|
||||||
|
|
||||||
|
class VirtualChassisListView(ObjectListView):
|
||||||
|
queryset = VirtualChassis.objects.annotate(member_count=Count('memberships'))
|
||||||
|
table = tables.VirtualChassisTable
|
||||||
|
template_name = 'dcim/virtualchassis_list.html'
|
||||||
|
|
||||||
|
|
||||||
|
class VirtualChassisEditView(PermissionRequiredMixin, ObjectEditView):
|
||||||
|
permission_required = 'dcim.change_virtualchassis'
|
||||||
|
model = VirtualChassis
|
||||||
|
model_form = forms.VirtualChassisForm
|
||||||
|
|
||||||
|
def get_return_url(self, request, obj):
|
||||||
|
return reverse('dcim:virtualchassis_list')
|
||||||
|
11
netbox/templates/dcim/virtualchassis_list.html
Normal file
11
netbox/templates/dcim/virtualchassis_list.html
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{% extends '_base.html' %}
|
||||||
|
{% load helpers %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>{% block title %}Virtual Chassis{% endblock %}</h1>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
{% include 'utilities/obj_table.html' %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
Loading…
Reference in New Issue
Block a user