Moved VC master designation to membership model

This commit is contained in:
Jeremy Stretch 2017-11-27 15:59:13 -05:00
parent 55e07c1c9a
commit 3b801d43bc
7 changed files with 77 additions and 29 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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