diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 5804480a0..0b51ec609 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -806,12 +806,10 @@ class WritableInterfaceConnectionSerializer(ValidatedModelSerializer): # class VirtualChassisSerializer(serializers.ModelSerializer): - site = NestedSiteSerializer() - master = NestedDeviceSerializer() class Meta: model = VirtualChassis - fields = ['id', 'site', 'domain', 'master'] + fields = ['id', 'domain'] class NestedVirtualChassisSerializer(serializers.ModelSerializer): @@ -826,7 +824,7 @@ class WritableVirtualChassisSerializer(ValidatedModelSerializer): class Meta: model = VirtualChassis - fields = ['id', 'site', 'domain', 'master'] + fields = ['id', 'domain'] # @@ -839,12 +837,23 @@ class VCMembershipSerializer(serializers.ModelSerializer): class Meta: model = VCMembership - fields = ['id', 'virtual_chassis', 'device', 'master_enabled', 'position', 'priority'] + fields = ['id', 'virtual_chassis', 'device', 'position', 'is_master', 'priority'] -class WritableVCMembershipSerializer(serializers.ModelSerializer): - virtual_chassis = serializers.PrimaryKeyRelatedField(queryset=VirtualChassis.objects.all(), required=False) +class WritableVCMembershipSerializer(ValidatedModelSerializer): class Meta: 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 diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index e782172a2..a3ef98a15 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals from collections import OrderedDict from django.conf import settings +from django.db import transaction from django.http import HttpResponseBadRequest, HttpResponseForbidden from django.shortcuts import get_object_or_404 from rest_framework.decorators import detail_route @@ -401,17 +402,30 @@ class InterfaceConnectionViewSet(ModelViewSet): # class VirtualChassisViewSet(ModelViewSet): - queryset = VirtualChassis.objects.select_related('master') + queryset = VirtualChassis.objects.all() 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 + 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) # diff --git a/netbox/dcim/apps.py b/netbox/dcim/apps.py index fb1f4ee39..ef3158508 100644 --- a/netbox/dcim/apps.py +++ b/netbox/dcim/apps.py @@ -6,3 +6,6 @@ from django.apps import AppConfig class DCIMConfig(AppConfig): name = "dcim" verbose_name = "DCIM" + + def ready(self): + import dcim.signals diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index c7f1992f3..d434871d6 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -17,7 +17,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, + RackReservation, RackRole, Region, Site, VCMembership, ) @@ -631,6 +631,13 @@ class InventoryItemFilter(DeviceComponentFilterSet): fields = ['name', 'part_id', 'serial', 'discovered'] +class VCMembershipFilter(django_filters.FilterSet): + + class Meta: + model = VCMembership + fields = ['virtual_chassis'] + + class ConsoleConnectionFilter(django_filters.FilterSet): site = django_filters.CharFilter( method='filter_site', diff --git a/netbox/dcim/migrations/0052_virtual_chassis.py b/netbox/dcim/migrations/0052_virtual_chassis.py index 96f38d5c9..db10b2510 100644 --- a/netbox/dcim/migrations/0052_virtual_chassis.py +++ b/netbox/dcim/migrations/0052_virtual_chassis.py @@ -1,5 +1,5 @@ # -*- 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 import django.core.validators @@ -18,8 +18,8 @@ class Migration(migrations.Migration): 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)])), + ('is_master', models.BooleanField(default=False)), ('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')), ], @@ -33,7 +33,6 @@ class Migration(migrations.Migration): 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( diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index 2d3857c62..24fe183e6 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -1494,21 +1494,10 @@ class VirtualChassis(models.Model): 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): @@ -1525,12 +1514,12 @@ class VCMembership(models.Model): on_delete=models.CASCADE, related_name='vc_membership' ) - master_enabled = models.BooleanField( - default=True - ) position = models.PositiveSmallIntegerField( validators=[MaxValueValidator(255)] ) + is_master = models.BooleanField( + default=False + ) priority = models.PositiveSmallIntegerField( blank=True, null=True, @@ -1541,3 +1530,14 @@ class VCMembership(models.Model): ordering = ['virtual_chassis', 'position'] unique_together = ['virtual_chassis', 'position'] 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) + }) diff --git a/netbox/dcim/signals.py b/netbox/dcim/signals.py new file mode 100644 index 000000000..ca33dd251 --- /dev/null +++ b/netbox/dcim/signals.py @@ -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()