Kaynağa Gözat

Change VLANGroup site to scope (GFK)

Jeremy Stretch 5 yıl önce
ebeveyn
işleme
4dae781be0

+ 17 - 3
netbox/ipam/api/serializers.py

@@ -114,14 +114,20 @@ class RoleSerializer(OrganizationalModelSerializer):
 
 class VLANGroupSerializer(OrganizationalModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlangroup-detail')
-    site = NestedSiteSerializer(required=False, allow_null=True)
+    scope_type = ContentTypeField(
+        queryset=ContentType.objects.filter(
+            app_label='dcim',
+            model__in=['region', 'sitegroup', 'site', 'location', 'rack']
+        )
+    )
+    scope = serializers.SerializerMethodField(read_only=True)
     vlan_count = serializers.IntegerField(read_only=True)
 
     class Meta:
         model = VLANGroup
         fields = [
-            'id', 'url', 'name', 'slug', 'site', 'description', 'custom_fields', 'created', 'last_updated',
-            'vlan_count',
+            'id', 'url', 'name', 'slug', 'scope_type', 'scope_id', 'scope', 'description', 'custom_fields', 'created',
+            'last_updated', 'vlan_count',
         ]
         validators = []
 
@@ -138,6 +144,14 @@ class VLANGroupSerializer(OrganizationalModelSerializer):
 
         return data
 
+    def get_scope(self, obj):
+        if obj.scope_id is None:
+            return None
+        serializer = get_serializer_for_model(obj.scope, prefix='Nested')
+        context = {'request': self.context['request']}
+
+        return serializer(obj.scope, context=context).data
+
 
 class VLANSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlan-detail')

+ 1 - 1
netbox/ipam/api/views.py

@@ -283,7 +283,7 @@ class IPAddressViewSet(CustomFieldModelViewSet):
 #
 
 class VLANGroupViewSet(CustomFieldModelViewSet):
-    queryset = VLANGroup.objects.prefetch_related('site').annotate(
+    queryset = VLANGroup.objects.annotate(
         vlan_count=count_related(VLAN, 'group')
     )
     serializer_class = serializers.VLANGroupSerializer

+ 21 - 34
netbox/ipam/filters.py

@@ -1,5 +1,6 @@
 import django_filters
 import netaddr
+from django.contrib.contenttypes.models import ContentType
 from django.core.exceptions import ValidationError
 from django.db.models import Q
 from netaddr.core import AddrFormatError
@@ -8,8 +9,8 @@ from dcim.models import Device, Interface, Region, Site, SiteGroup
 from extras.filters import CustomFieldModelFilterSet, CreatedUpdatedFilterSet
 from tenancy.filters import TenancyFilterSet
 from utilities.filters import (
-    BaseFilterSet, MultiValueCharFilter, MultiValueNumberFilter, NameSlugSearchFilterSet, NumericArrayFilter, TagFilter,
-    TreeNodeMultipleChoiceFilter,
+    BaseFilterSet, ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter, NameSlugSearchFilterSet,
+    NumericArrayFilter, TagFilter, TreeNodeMultipleChoiceFilter,
 )
 from virtualization.models import VirtualMachine, VMInterface
 from .choices import *
@@ -535,46 +536,32 @@ class IPAddressFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilter
 
 
 class VLANGroupFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
-    region_id = TreeNodeMultipleChoiceFilter(
-        queryset=Region.objects.all(),
-        field_name='site__region',
-        lookup_expr='in',
-        label='Region (ID)',
+    scope_type = ContentTypeFilter()
+    region = django_filters.NumberFilter(
+        method='filter_scope'
     )
-    region = TreeNodeMultipleChoiceFilter(
-        queryset=Region.objects.all(),
-        field_name='site__region',
-        lookup_expr='in',
-        to_field_name='slug',
-        label='Region (slug)',
-    )
-    site_group_id = TreeNodeMultipleChoiceFilter(
-        queryset=SiteGroup.objects.all(),
-        field_name='site__group',
-        lookup_expr='in',
-        label='Site group (ID)',
+    sitegroup = django_filters.NumberFilter(
+        method='filter_scope'
     )
-    site_group = TreeNodeMultipleChoiceFilter(
-        queryset=SiteGroup.objects.all(),
-        field_name='site__group',
-        lookup_expr='in',
-        to_field_name='slug',
-        label='Site group (slug)',
+    site = django_filters.NumberFilter(
+        method='filter_scope'
     )
-    site_id = django_filters.ModelMultipleChoiceFilter(
-        queryset=Site.objects.all(),
-        label='Site (ID)',
+    location = django_filters.NumberFilter(
+        method='filter_scope'
     )
-    site = django_filters.ModelMultipleChoiceFilter(
-        field_name='site__slug',
-        queryset=Site.objects.all(),
-        to_field_name='slug',
-        label='Site (slug)',
+    rack = django_filters.NumberFilter(
+        method='filter_scope'
     )
 
     class Meta:
         model = VLANGroup
-        fields = ['id', 'name', 'slug', 'description']
+        fields = ['id', 'name', 'slug', 'description', 'scope_id']
+
+    def filter_scope(self, queryset, name, value):
+        return queryset.filter(
+            scope_type=ContentType.objects.get(app_label='dcim', model=name),
+            scope_id=value
+        )
 
 
 class VLANFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):

+ 68 - 9
netbox/ipam/forms.py

@@ -1,7 +1,8 @@
 from django import forms
+from django.contrib.contenttypes.models import ContentType
 from django.utils.translation import gettext as _
 
-from dcim.models import Device, Interface, Rack, Region, Site, SiteGroup
+from dcim.models import Device, Interface, Location, Rack, Region, Site, SiteGroup
 from extras.forms import (
     AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldModelCSVForm, CustomFieldModelForm, CustomFieldFilterForm,
 )
@@ -1126,18 +1127,70 @@ class VLANGroupForm(BootstrapMixin, CustomFieldModelForm):
     site = DynamicModelChoiceField(
         queryset=Site.objects.all(),
         required=False,
+        initial_params={
+            'locations': '$location'
+        },
         query_params={
             'region_id': '$region',
             'group_id': '$site_group',
         }
     )
+    location = DynamicModelChoiceField(
+        queryset=Location.objects.all(),
+        required=False,
+        initial_params={
+            'racks': '$rack'
+        },
+        query_params={
+            'site_id': '$site',
+        }
+    )
+    rack = DynamicModelChoiceField(
+        queryset=Rack.objects.all(),
+        required=False,
+        query_params={
+            'site_id': '$site',
+            'location_id': '$location',
+        }
+    )
     slug = SlugField()
 
     class Meta:
         model = VLANGroup
         fields = [
-            'region', 'site', 'name', 'slug', 'description',
+            'name', 'slug', 'description', 'region', 'site_group', 'site', 'location', 'rack',
         ]
+        fieldsets = (
+            ('VLAN Group', ('name', 'slug', 'description')),
+            ('Scope', ('region', 'site_group', 'site', 'location', 'rack')),
+        )
+
+    def __init__(self, *args, **kwargs):
+        instance = kwargs.get('instance')
+        initial = kwargs.get('initial', {})
+
+        if instance is not None and instance.scope:
+            if type(instance.scope) is Rack:
+                initial['rack'] = instance.scope
+            elif type(instance.scope) is Location:
+                initial['location'] = instance.scope
+            elif type(instance.scope) is Site:
+                initial['site'] = instance.scope
+            elif type(instance.scope) is SiteGroup:
+                initial['site_group'] = instance.scope
+            elif type(instance.scope) is Region:
+                initial['region'] = instance.scope
+
+            kwargs['initial'] = initial
+
+        super().__init__(*args, **kwargs)
+
+    def clean(self):
+        super().clean()
+
+        # Assign scope object
+        self.instance.scope = self.cleaned_data['rack'] or self.cleaned_data['location'] or self.cleaned_data['site'] \
+            or self.cleaned_data['site_group'] or self.cleaned_data['region'] or None
 
 
 class VLANGroupCSVForm(CustomFieldModelCSVForm):
@@ -1155,25 +1208,31 @@ class VLANGroupCSVForm(CustomFieldModelCSVForm):
 
 
 class VLANGroupFilterForm(BootstrapMixin, forms.Form):
-    region_id = DynamicModelMultipleChoiceField(
+    region = DynamicModelMultipleChoiceField(
         queryset=Region.objects.all(),
         required=False,
         label=_('Region')
     )
-    site_group_id = DynamicModelMultipleChoiceField(
+    sitegroup = DynamicModelMultipleChoiceField(
         queryset=SiteGroup.objects.all(),
         required=False,
         label=_('Site group')
     )
-    site_id = DynamicModelMultipleChoiceField(
+    site = DynamicModelMultipleChoiceField(
         queryset=Site.objects.all(),
         required=False,
-        null_option='None',
-        query_params={
-            'region_id': '$region_id'
-        },
         label=_('Site')
     )
+    location = DynamicModelMultipleChoiceField(
+        queryset=Location.objects.all(),
+        required=False,
+        label=_('Location')
+    )
+    rack = DynamicModelMultipleChoiceField(
+        queryset=Rack.objects.all(),
+        required=False,
+        label=_('Rack')
+    )
 
 
 #

+ 36 - 0
netbox/ipam/migrations/0045_vlangroup_scope.py

@@ -0,0 +1,36 @@
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('contenttypes', '0002_remove_content_type_name'),
+        ('ipam', '0044_standardize_models'),
+    ]
+
+    operations = [
+        migrations.RenameField(
+            model_name='vlangroup',
+            old_name='site',
+            new_name='scope_id',
+        ),
+        migrations.AlterField(
+            model_name='vlangroup',
+            name='scope_id',
+            field=models.PositiveBigIntegerField(blank=True, null=True),
+        ),
+        migrations.AddField(
+            model_name='vlangroup',
+            name='scope_type',
+            field=models.ForeignKey(blank=True, limit_choices_to=models.Q(('app_label', 'dcim'), ('model__in', ['region', 'sitegroup', 'site', 'location', 'rack'])), null=True, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype'),
+        ),
+        migrations.AlterModelOptions(
+            name='vlangroup',
+            options={'ordering': ('name', 'pk'), 'verbose_name': 'VLAN group', 'verbose_name_plural': 'VLAN groups'},
+        ),
+        migrations.AlterUniqueTogether(
+            name='vlangroup',
+            unique_together={('scope_type', 'scope_id', 'name'), ('scope_type', 'scope_id', 'slug')},
+        ),
+    ]

+ 23 - 9
netbox/ipam/models/vlans.py

@@ -1,3 +1,5 @@
+from django.contrib.contenttypes.fields import GenericForeignKey
+from django.contrib.contenttypes.models import ContentType
 from django.core.exceptions import ValidationError
 from django.core.validators import MaxValueValidator, MinValueValidator
 from django.db import models
@@ -31,13 +33,24 @@ class VLANGroup(OrganizationalModel):
     slug = models.SlugField(
         max_length=100
     )
-    site = models.ForeignKey(
-        to='dcim.Site',
-        on_delete=models.PROTECT,
-        related_name='vlan_groups',
+    scope_type = models.ForeignKey(
+        to=ContentType,
+        on_delete=models.CASCADE,
+        limit_choices_to=Q(
+            app_label='dcim',
+            model__in=['region', 'sitegroup', 'site', 'location', 'rack']
+        ),
+        blank=True,
+        null=True
+    )
+    scope_id = models.PositiveBigIntegerField(
         blank=True,
         null=True
     )
+    scope = GenericForeignKey(
+        ct_field='scope_type',
+        fk_field='scope_id'
+    )
     description = models.CharField(
         max_length=200,
         blank=True
@@ -45,13 +58,13 @@ class VLANGroup(OrganizationalModel):
 
     objects = RestrictedQuerySet.as_manager()
 
-    csv_headers = ['name', 'slug', 'site', 'description']
+    csv_headers = ['name', 'slug', 'scope_type', 'scope_id', 'description']
 
     class Meta:
-        ordering = ('site', 'name', 'pk')  # (site, name) may be non-unique
+        ordering = ('name', 'pk')  # Name may be non-unique
         unique_together = [
-            ['site', 'name'],
-            ['site', 'slug'],
+            ['scope_type', 'scope_id', 'name'],
+            ['scope_type', 'scope_id', 'slug'],
         ]
         verbose_name = 'VLAN group'
         verbose_name_plural = 'VLAN groups'
@@ -66,7 +79,8 @@ class VLANGroup(OrganizationalModel):
         return (
             self.name,
             self.slug,
-            self.site.name if self.site else None,
+            f'{self.scope_type.app_label}.{self.scope_type.model}',
+            self.scope_id,
             self.description,
         )
 

+ 3 - 3
netbox/ipam/tables.py

@@ -414,7 +414,7 @@ class InterfaceIPAddressTable(BaseTable):
 class VLANGroupTable(BaseTable):
     pk = ToggleColumn()
     name = tables.Column(linkify=True)
-    site = tables.Column(
+    scope = tables.Column(
         linkify=True
     )
     vlan_count = LinkedCountColumn(
@@ -429,8 +429,8 @@ class VLANGroupTable(BaseTable):
 
     class Meta(BaseTable.Meta):
         model = VLANGroup
-        fields = ('pk', 'name', 'site', 'vlan_count', 'slug', 'description', 'actions')
-        default_columns = ('pk', 'name', 'site', 'vlan_count', 'description', 'actions')
+        fields = ('pk', 'name', 'scope_type', 'scope', 'vlan_count', 'slug', 'description', 'actions')
+        default_columns = ('pk', 'name', 'scope_type', 'scope', 'vlan_count', 'description', 'actions')
 
 
 #