Răsfoiți Sursa

18296 Add Tenancy to VLAN Groups (#18690)

* 18296 add tenant to vlan groups

* 18296 add tenant to vlan groups

* 18296 add tenant to vlan groups

* 18296 add tenant to vlan groups

* 18296 review changes
Arthur Hanson 11 luni în urmă
părinte
comite
08b2fc424a

+ 2 - 1
netbox/ipam/api/serializers_/vlans.py

@@ -37,6 +37,7 @@ class VLANGroupSerializer(NetBoxModelSerializer):
     scope = serializers.SerializerMethodField(read_only=True)
     scope = serializers.SerializerMethodField(read_only=True)
     vid_ranges = IntegerRangeSerializer(many=True, required=False)
     vid_ranges = IntegerRangeSerializer(many=True, required=False)
     utilization = serializers.CharField(read_only=True)
     utilization = serializers.CharField(read_only=True)
+    tenant = TenantSerializer(nested=True, required=False, allow_null=True)
 
 
     # Related object counts
     # Related object counts
     vlan_count = RelatedObjectCountField('vlans')
     vlan_count = RelatedObjectCountField('vlans')
@@ -45,7 +46,7 @@ class VLANGroupSerializer(NetBoxModelSerializer):
         model = VLANGroup
         model = VLANGroup
         fields = [
         fields = [
             'id', 'url', 'display_url', 'display', 'name', 'slug', 'scope_type', 'scope_id', 'scope', 'vid_ranges',
             'id', 'url', 'display_url', 'display', 'name', 'slug', 'scope_type', 'scope_id', 'scope', 'vid_ranges',
-            'description', 'tags', 'custom_fields', 'created', 'last_updated', 'vlan_count', 'utilization'
+            'tenant', 'description', 'tags', 'custom_fields', 'created', 'last_updated', 'vlan_count', 'utilization'
         ]
         ]
         brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'vlan_count')
         brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'vlan_count')
         validators = []
         validators = []

+ 1 - 1
netbox/ipam/filtersets.py

@@ -857,7 +857,7 @@ class FHRPGroupAssignmentFilterSet(ChangeLoggedModelFilterSet):
         )
         )
 
 
 
 
-class VLANGroupFilterSet(OrganizationalModelFilterSet):
+class VLANGroupFilterSet(OrganizationalModelFilterSet, TenancyFilterSet):
     scope_type = ContentTypeFilter()
     scope_type = ContentTypeFilter()
     region = django_filters.NumberFilter(
     region = django_filters.NumberFilter(
         method='filter_scope'
         method='filter_scope'

+ 6 - 0
netbox/ipam/forms/bulk_edit.py

@@ -430,11 +430,17 @@ class VLANGroupBulkEditForm(NetBoxModelBulkEditForm):
         label=_('VLAN ID ranges'),
         label=_('VLAN ID ranges'),
         required=False
         required=False
     )
     )
+    tenant = DynamicModelChoiceField(
+        label=_('Tenant'),
+        queryset=Tenant.objects.all(),
+        required=False
+    )
 
 
     model = VLANGroup
     model = VLANGroup
     fieldsets = (
     fieldsets = (
         FieldSet('site', 'vid_ranges', 'description'),
         FieldSet('site', 'vid_ranges', 'description'),
         FieldSet('scope_type', 'scope', name=_('Scope')),
         FieldSet('scope_type', 'scope', name=_('Scope')),
+        FieldSet('tenant', name=_('Tenancy')),
     )
     )
     nullable_fields = ('description', 'scope')
     nullable_fields = ('description', 'scope')
 
 

+ 8 - 1
netbox/ipam/forms/bulk_import.py

@@ -438,10 +438,17 @@ class VLANGroupImportForm(NetBoxModelImportForm):
     vid_ranges = NumericRangeArrayField(
     vid_ranges = NumericRangeArrayField(
         required=False
         required=False
     )
     )
+    tenant = CSVModelChoiceField(
+        label=_('Tenant'),
+        queryset=Tenant.objects.all(),
+        required=False,
+        to_field_name='name',
+        help_text=_('Assigned tenant')
+    )
 
 
     class Meta:
     class Meta:
         model = VLANGroup
         model = VLANGroup
-        fields = ('name', 'slug', 'scope_type', 'scope_id', 'vid_ranges', 'description', 'tags')
+        fields = ('name', 'slug', 'scope_type', 'scope_id', 'vid_ranges', 'tenant', 'description', 'tags')
         labels = {
         labels = {
             'scope_id': 'Scope ID',
             'scope_id': 'Scope ID',
         }
         }

+ 2 - 1
netbox/ipam/forms/filtersets.py

@@ -411,12 +411,13 @@ class FHRPGroupFilterForm(NetBoxModelFilterSetForm):
     tag = TagFilterField(model)
     tag = TagFilterField(model)
 
 
 
 
-class VLANGroupFilterForm(NetBoxModelFilterSetForm):
+class VLANGroupFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
     fieldsets = (
     fieldsets = (
         FieldSet('q', 'filter_id', 'tag'),
         FieldSet('q', 'filter_id', 'tag'),
         FieldSet('region', 'sitegroup', 'site', 'location', 'rack', name=_('Location')),
         FieldSet('region', 'sitegroup', 'site', 'location', 'rack', name=_('Location')),
         FieldSet('cluster_group', 'cluster', name=_('Cluster')),
         FieldSet('cluster_group', 'cluster', name=_('Cluster')),
         FieldSet('contains_vid', name=_('VLANs')),
         FieldSet('contains_vid', name=_('VLANs')),
+        FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
     )
     )
     model = VLANGroup
     model = VLANGroup
     region = DynamicModelMultipleChoiceField(
     region = DynamicModelMultipleChoiceField(

+ 3 - 2
netbox/ipam/forms/model_forms.py

@@ -598,7 +598,7 @@ class FHRPGroupAssignmentForm(forms.ModelForm):
         return group
         return group
 
 
 
 
-class VLANGroupForm(NetBoxModelForm):
+class VLANGroupForm(TenancyForm, NetBoxModelForm):
     slug = SlugField()
     slug = SlugField()
     vid_ranges = NumericRangeArrayField(
     vid_ranges = NumericRangeArrayField(
         label=_('VLAN IDs')
         label=_('VLAN IDs')
@@ -621,12 +621,13 @@ class VLANGroupForm(NetBoxModelForm):
         FieldSet('name', 'slug', 'description', 'tags', name=_('VLAN Group')),
         FieldSet('name', 'slug', 'description', 'tags', name=_('VLAN Group')),
         FieldSet('vid_ranges', name=_('Child VLANs')),
         FieldSet('vid_ranges', name=_('Child VLANs')),
         FieldSet('scope_type', 'scope', name=_('Scope')),
         FieldSet('scope_type', 'scope', name=_('Scope')),
+        FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
     )
     )
 
 
     class Meta:
     class Meta:
         model = VLANGroup
         model = VLANGroup
         fields = [
         fields = [
-            'name', 'slug', 'description', 'vid_ranges', 'scope_type', 'tags',
+            'name', 'slug', 'description', 'vid_ranges', 'scope_type', 'tenant_group', 'tenant', 'tags',
         ]
         ]
 
 
     def __init__(self, *args, **kwargs):
     def __init__(self, *args, **kwargs):

+ 1 - 0
netbox/ipam/graphql/types.py

@@ -266,6 +266,7 @@ class VLANGroupType(OrganizationalObjectType):
 
 
     vlans: List[VLANType]
     vlans: List[VLANType]
     vid_ranges: List[str]
     vid_ranges: List[str]
+    tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None
 
 
     @strawberry_django.field
     @strawberry_django.field
     def scope(self) -> Annotated[Union[
     def scope(self) -> Annotated[Union[

+ 26 - 0
netbox/ipam/migrations/0077_vlangroup_tenant.py

@@ -0,0 +1,26 @@
+# Generated by Django 5.1.3 on 2025-02-20 17:49
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('ipam', '0076_natural_ordering'),
+        ('tenancy', '0017_natural_ordering'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='vlangroup',
+            name='tenant',
+            field=models.ForeignKey(
+                blank=True,
+                null=True,
+                on_delete=django.db.models.deletion.PROTECT,
+                related_name='vlan_groups',
+                to='tenancy.tenant',
+            ),
+        ),
+    ]

+ 7 - 0
netbox/ipam/models/vlans.py

@@ -62,6 +62,13 @@ class VLANGroup(OrganizationalModel):
         verbose_name=_('VLAN ID ranges'),
         verbose_name=_('VLAN ID ranges'),
         default=default_vid_ranges
         default=default_vid_ranges
     )
     )
+    tenant = models.ForeignKey(
+        to='tenancy.Tenant',
+        on_delete=models.PROTECT,
+        related_name='vlan_groups',
+        blank=True,
+        null=True
+    )
     _total_vlan_ids = models.PositiveBigIntegerField(
     _total_vlan_ids = models.PositiveBigIntegerField(
         default=VLAN_VID_MAX - VLAN_VID_MIN + 1
         default=VLAN_VID_MAX - VLAN_VID_MIN + 1
     )
     )

+ 5 - 3
netbox/ipam/tables/vlans.py

@@ -28,7 +28,7 @@ AVAILABLE_LABEL = mark_safe('<span class="badge text-bg-success">Available</span
 # VLAN groups
 # VLAN groups
 #
 #
 
 
-class VLANGroupTable(NetBoxTable):
+class VLANGroupTable(TenancyColumnsMixin, NetBoxTable):
     name = tables.Column(
     name = tables.Column(
         verbose_name=_('Name'),
         verbose_name=_('Name'),
         linkify=True
         linkify=True
@@ -65,9 +65,11 @@ class VLANGroupTable(NetBoxTable):
         model = VLANGroup
         model = VLANGroup
         fields = (
         fields = (
             'pk', 'id', 'name', 'scope_type', 'scope', 'vid_ranges_list', 'vlan_count', 'slug', 'description',
             'pk', 'id', 'name', 'scope_type', 'scope', 'vid_ranges_list', 'vlan_count', 'slug', 'description',
-            'tags', 'created', 'last_updated', 'actions', 'utilization',
+            'tenant', 'tenant_group', 'tags', 'created', 'last_updated', 'actions', 'utilization',
+        )
+        default_columns = (
+            'pk', 'name', 'scope_type', 'scope', 'vlan_count', 'utilization', 'tenant', 'description'
         )
         )
-        default_columns = ('pk', 'name', 'scope_type', 'scope', 'vlan_count', 'utilization', 'description')
 
 
 
 
 #
 #

+ 35 - 3
netbox/ipam/tests/test_filtersets.py

@@ -1568,27 +1568,45 @@ class VLANGroupTestCase(TestCase, ChangeLoggedFilterSetTests):
         cluster = Cluster(name='Cluster 1', type=clustertype)
         cluster = Cluster(name='Cluster 1', type=clustertype)
         cluster.save()
         cluster.save()
 
 
+        tenant_groups = (
+            TenantGroup(name='Tenant group 1', slug='tenant-group-1'),
+            TenantGroup(name='Tenant group 2', slug='tenant-group-2'),
+            TenantGroup(name='Tenant group 3', slug='tenant-group-3'),
+        )
+        for tenantgroup in tenant_groups:
+            tenantgroup.save()
+
+        tenants = (
+            Tenant(name='Tenant 1', slug='tenant-1', group=tenant_groups[0]),
+            Tenant(name='Tenant 2', slug='tenant-2', group=tenant_groups[1]),
+            Tenant(name='Tenant 3', slug='tenant-3', group=tenant_groups[2]),
+        )
+        Tenant.objects.bulk_create(tenants)
+
         vlan_groups = (
         vlan_groups = (
             VLANGroup(
             VLANGroup(
                 name='VLAN Group 1',
                 name='VLAN Group 1',
                 slug='vlan-group-1',
                 slug='vlan-group-1',
                 vid_ranges=[NumericRange(1, 11), NumericRange(100, 200)],
                 vid_ranges=[NumericRange(1, 11), NumericRange(100, 200)],
                 scope=region,
                 scope=region,
-                description='foobar1'
+                description='foobar1',
+                tenant=tenants[0]
             ),
             ),
             VLANGroup(
             VLANGroup(
                 name='VLAN Group 2',
                 name='VLAN Group 2',
                 slug='vlan-group-2',
                 slug='vlan-group-2',
                 vid_ranges=[NumericRange(1, 11), NumericRange(200, 300)],
                 vid_ranges=[NumericRange(1, 11), NumericRange(200, 300)],
                 scope=sitegroup,
                 scope=sitegroup,
-                description='foobar2'
+                description='foobar2',
+                tenant=tenants[1]
             ),
             ),
             VLANGroup(
             VLANGroup(
                 name='VLAN Group 3',
                 name='VLAN Group 3',
                 slug='vlan-group-3',
                 slug='vlan-group-3',
                 vid_ranges=[NumericRange(1, 11), NumericRange(300, 400)],
                 vid_ranges=[NumericRange(1, 11), NumericRange(300, 400)],
                 scope=site,
                 scope=site,
-                description='foobar3'
+                description='foobar3',
+                tenant=tenants[1]
             ),
             ),
             VLANGroup(
             VLANGroup(
                 name='VLAN Group 4',
                 name='VLAN Group 4',
@@ -1671,6 +1689,20 @@ class VLANGroupTestCase(TestCase, ChangeLoggedFilterSetTests):
         params = {'cluster': Cluster.objects.first().pk}
         params = {'cluster': Cluster.objects.first().pk}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
 
 
+    def test_tenant(self):
+        tenants = Tenant.objects.all()[:2]
+        params = {'tenant_id': [tenants[0].pk, tenants[1].pk]}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
+        params = {'tenant': [tenants[0].slug, tenants[1].slug]}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
+
+    def test_tenant_group(self):
+        tenant_groups = TenantGroup.objects.all()[:2]
+        params = {'tenant_group_id': [tenant_groups[0].pk, tenant_groups[1].pk]}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
+        params = {'tenant_group': [tenant_groups[0].slug, tenant_groups[1].slug]}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
+
 
 
 class VLANTestCase(TestCase, ChangeLoggedFilterSetTests):
 class VLANTestCase(TestCase, ChangeLoggedFilterSetTests):
     queryset = VLAN.objects.all()
     queryset = VLAN.objects.all()

+ 9 - 0
netbox/templates/ipam/vlangroup.html

@@ -46,6 +46,15 @@
           <th scope="row">Utilization</th>
           <th scope="row">Utilization</th>
           <td>{% utilization_graph object.utilization %}</td>
           <td>{% utilization_graph object.utilization %}</td>
         </tr>
         </tr>
+        <tr>
+          <th scope="row">{% trans "Tenant" %}</th>
+          <td>
+            {% if object.tenant.group %}
+              {{ object.tenant.group|linkify }} /
+            {% endif %}
+            {{ object.tenant|linkify|placeholder }}
+          </td>
+        </tr>
       </table>
       </table>
     </div>
     </div>
     {% include 'inc/panels/tags.html' %}
     {% include 'inc/panels/tags.html' %}