Parcourir la source

Moved VC master designation to membership model

Jeremy Stretch il y a 7 ans
Parent
commit
3b801d43bc

+ 17 - 8
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

+ 17 - 3
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)
 
 
 #

+ 3 - 0
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

+ 8 - 1
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',

+ 2 - 3
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(

+ 14 - 14
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)
+                })

+ 16 - 0
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()