mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-23 04:22:01 -06:00
Moved VC master designation to membership model
This commit is contained in:
parent
55e07c1c9a
commit
3b801d43bc
@ -806,12 +806,10 @@ class WritableInterfaceConnectionSerializer(ValidatedModelSerializer):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class VirtualChassisSerializer(serializers.ModelSerializer):
|
class VirtualChassisSerializer(serializers.ModelSerializer):
|
||||||
site = NestedSiteSerializer()
|
|
||||||
master = NestedDeviceSerializer()
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = VirtualChassis
|
model = VirtualChassis
|
||||||
fields = ['id', 'site', 'domain', 'master']
|
fields = ['id', 'domain']
|
||||||
|
|
||||||
|
|
||||||
class NestedVirtualChassisSerializer(serializers.ModelSerializer):
|
class NestedVirtualChassisSerializer(serializers.ModelSerializer):
|
||||||
@ -826,7 +824,7 @@ class WritableVirtualChassisSerializer(ValidatedModelSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = VirtualChassis
|
model = VirtualChassis
|
||||||
fields = ['id', 'site', 'domain', 'master']
|
fields = ['id', 'domain']
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -839,12 +837,23 @@ class VCMembershipSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = VCMembership
|
model = VCMembership
|
||||||
fields = ['id', 'virtual_chassis', 'device', 'master_enabled', 'position', 'priority']
|
fields = ['id', 'virtual_chassis', 'device', 'position', 'is_master', 'priority']
|
||||||
|
|
||||||
|
|
||||||
class WritableVCMembershipSerializer(serializers.ModelSerializer):
|
class WritableVCMembershipSerializer(ValidatedModelSerializer):
|
||||||
virtual_chassis = serializers.PrimaryKeyRelatedField(queryset=VirtualChassis.objects.all(), required=False)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = VCMembership
|
model = VCMembership
|
||||||
fields = ['id', 'virtual_chassis', 'device', 'master_enabled', 'position', 'priority']
|
fields = ['id', 'virtual_chassis', 'device', 'position', 'is_master', 'priority']
|
||||||
|
|
||||||
|
def validate(self, data):
|
||||||
|
|
||||||
|
# Validate uniqueness of (virtual_chassis, position)
|
||||||
|
validator = UniqueTogetherValidator(queryset=VCMembership.objects.all(), fields=('virtual_chassis', 'position'))
|
||||||
|
validator.set_context(self)
|
||||||
|
validator(data)
|
||||||
|
|
||||||
|
# Enforce model validation
|
||||||
|
super(WritableVCMembershipSerializer, self).validate(data)
|
||||||
|
|
||||||
|
return data
|
||||||
|
@ -3,6 +3,7 @@ from __future__ import unicode_literals
|
|||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.db import transaction
|
||||||
from django.http import HttpResponseBadRequest, HttpResponseForbidden
|
from django.http import HttpResponseBadRequest, HttpResponseForbidden
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from rest_framework.decorators import detail_route
|
from rest_framework.decorators import detail_route
|
||||||
@ -401,17 +402,30 @@ class InterfaceConnectionViewSet(ModelViewSet):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class VirtualChassisViewSet(ModelViewSet):
|
class VirtualChassisViewSet(ModelViewSet):
|
||||||
queryset = VirtualChassis.objects.select_related('master')
|
queryset = VirtualChassis.objects.all()
|
||||||
serializer_class = serializers.VirtualChassisSerializer
|
serializer_class = serializers.VirtualChassisSerializer
|
||||||
write_serializer_class = serializers.WritableVirtualChassisSerializer
|
write_serializer_class = serializers.WritableVirtualChassisSerializer
|
||||||
# filter_class = filters.VirtualChassisFilter
|
|
||||||
|
|
||||||
|
|
||||||
class VCMembershipViewSet(ModelViewSet):
|
class VCMembershipViewSet(ModelViewSet):
|
||||||
queryset = VCMembership.objects.select_related('virtual_chassis', 'device')
|
queryset = VCMembership.objects.select_related('virtual_chassis', 'device')
|
||||||
serializer_class = serializers.VCMembershipSerializer
|
serializer_class = serializers.VCMembershipSerializer
|
||||||
write_serializer_class = serializers.WritableVCMembershipSerializer
|
write_serializer_class = serializers.WritableVCMembershipSerializer
|
||||||
# filter_class = filters.VCMembershipFilter
|
filter_class = filters.VCMembershipFilter
|
||||||
|
|
||||||
|
def create(self, request, *args, **kwargs):
|
||||||
|
|
||||||
|
with transaction.atomic():
|
||||||
|
|
||||||
|
# Automatically create a new VirtualChassis for new VCMemberships with no VC specified
|
||||||
|
virtual_chassis = request.data.get('virtual_chassis', None)
|
||||||
|
is_master = request.data.get('is_master', False)
|
||||||
|
if not virtual_chassis and is_master:
|
||||||
|
vc = VirtualChassis()
|
||||||
|
vc.save()
|
||||||
|
request.data['virtual_chassis'] = vc.pk
|
||||||
|
|
||||||
|
return super(VCMembershipViewSet, self).create(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
@ -6,3 +6,6 @@ from django.apps import AppConfig
|
|||||||
class DCIMConfig(AppConfig):
|
class DCIMConfig(AppConfig):
|
||||||
name = "dcim"
|
name = "dcim"
|
||||||
verbose_name = "DCIM"
|
verbose_name = "DCIM"
|
||||||
|
|
||||||
|
def ready(self):
|
||||||
|
import dcim.signals
|
||||||
|
@ -17,7 +17,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, VCMembership,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -631,6 +631,13 @@ class InventoryItemFilter(DeviceComponentFilterSet):
|
|||||||
fields = ['name', 'part_id', 'serial', 'discovered']
|
fields = ['name', 'part_id', 'serial', 'discovered']
|
||||||
|
|
||||||
|
|
||||||
|
class VCMembershipFilter(django_filters.FilterSet):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = VCMembership
|
||||||
|
fields = ['virtual_chassis']
|
||||||
|
|
||||||
|
|
||||||
class ConsoleConnectionFilter(django_filters.FilterSet):
|
class ConsoleConnectionFilter(django_filters.FilterSet):
|
||||||
site = django_filters.CharFilter(
|
site = django_filters.CharFilter(
|
||||||
method='filter_site',
|
method='filter_site',
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Generated by Django 1.11.6 on 2017-11-17 20:39
|
# Generated by Django 1.11.6 on 2017-11-27 17:27
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import django.core.validators
|
import django.core.validators
|
||||||
@ -18,8 +18,8 @@ class Migration(migrations.Migration):
|
|||||||
name='VCMembership',
|
name='VCMembership',
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('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)])),
|
('position', models.PositiveSmallIntegerField(validators=[django.core.validators.MaxValueValidator(255)])),
|
||||||
|
('is_master', models.BooleanField(default=False)),
|
||||||
('priority', models.PositiveSmallIntegerField(blank=True, null=True, 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')),
|
('device', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='vc_membership', to='dcim.Device')),
|
||||||
],
|
],
|
||||||
@ -33,7 +33,6 @@ class Migration(migrations.Migration):
|
|||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
('domain', models.CharField(blank=True, max_length=30)),
|
('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(
|
migrations.AddField(
|
||||||
|
@ -1494,21 +1494,10 @@ class VirtualChassis(models.Model):
|
|||||||
max_length=30,
|
max_length=30,
|
||||||
blank=True
|
blank=True
|
||||||
)
|
)
|
||||||
master = models.OneToOneField(
|
|
||||||
to='Device',
|
|
||||||
on_delete=models.PROTECT,
|
|
||||||
related_name='vc_master_for'
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return "{}?virtual_chassis={}".format(reverse('dcim:device_list'), self.pk)
|
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
|
@python_2_unicode_compatible
|
||||||
class VCMembership(models.Model):
|
class VCMembership(models.Model):
|
||||||
@ -1525,12 +1514,12 @@ class VCMembership(models.Model):
|
|||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
related_name='vc_membership'
|
related_name='vc_membership'
|
||||||
)
|
)
|
||||||
master_enabled = models.BooleanField(
|
|
||||||
default=True
|
|
||||||
)
|
|
||||||
position = models.PositiveSmallIntegerField(
|
position = models.PositiveSmallIntegerField(
|
||||||
validators=[MaxValueValidator(255)]
|
validators=[MaxValueValidator(255)]
|
||||||
)
|
)
|
||||||
|
is_master = models.BooleanField(
|
||||||
|
default=False
|
||||||
|
)
|
||||||
priority = models.PositiveSmallIntegerField(
|
priority = models.PositiveSmallIntegerField(
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
@ -1541,3 +1530,14 @@ class VCMembership(models.Model):
|
|||||||
ordering = ['virtual_chassis', 'position']
|
ordering = ['virtual_chassis', 'position']
|
||||||
unique_together = ['virtual_chassis', 'position']
|
unique_together = ['virtual_chassis', 'position']
|
||||||
verbose_name = 'VC membership'
|
verbose_name = 'VC membership'
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
|
||||||
|
# Check for master conflicts
|
||||||
|
if self.virtual_chassis and self.is_master:
|
||||||
|
master_conflict = VCMembership.objects.filter(virtual_chassis=self.virtual_chassis).first()
|
||||||
|
if master_conflict:
|
||||||
|
raise ValidationError({
|
||||||
|
'virtual_chassis': "{} has already been designated as the master for this virtual chassis. It must "
|
||||||
|
"be demoted before a new master can be assigned.".format(master_conflict.device)
|
||||||
|
})
|
||||||
|
16
netbox/dcim/signals.py
Normal file
16
netbox/dcim/signals.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db.models.signals import post_delete
|
||||||
|
from django.dispatch import receiver
|
||||||
|
|
||||||
|
from .models import VCMembership
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(post_delete, sender=VCMembership)
|
||||||
|
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()
|
Loading…
Reference in New Issue
Block a user