mirror of
https://github.com/netbox-community/netbox.git
synced 2026-03-21 20:18:38 -06:00
feat(ipam): Expose total_vlan_ids on VLAN groups
Rename the internal `_total_vlan_ids` field to `total_vlan_ids` on VLANGroup and expose it as a read-only integer field. This change includes a migration to rename the database column, adds `total_vlan_ids` to VLANGroup API representations as a read-only attribute, updates the UI table to include a "Total VLAN IDs" column, and adjusts related tests accordingly. Fixes #20698
This commit is contained in:
@@ -36,6 +36,7 @@ class VLANGroupSerializer(OrganizationalModelSerializer):
|
|||||||
scope_id = serializers.IntegerField(allow_null=True, required=False, default=None)
|
scope_id = serializers.IntegerField(allow_null=True, required=False, default=None)
|
||||||
scope = GFKSerializerField(read_only=True)
|
scope = GFKSerializerField(read_only=True)
|
||||||
vid_ranges = IntegerRangeSerializer(many=True, required=False)
|
vid_ranges = IntegerRangeSerializer(many=True, required=False)
|
||||||
|
total_vlan_ids = serializers.IntegerField(read_only=True)
|
||||||
utilization = serializers.CharField(read_only=True)
|
utilization = serializers.CharField(read_only=True)
|
||||||
tenant = TenantSerializer(nested=True, required=False, allow_null=True)
|
tenant = TenantSerializer(nested=True, required=False, allow_null=True)
|
||||||
|
|
||||||
@@ -46,7 +47,8 @@ class VLANGroupSerializer(OrganizationalModelSerializer):
|
|||||||
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',
|
||||||
'tenant', 'description', 'owner', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
|
'total_vlan_ids', 'tenant', 'description', 'owner', 'comments', 'tags', 'custom_fields',
|
||||||
|
'created', 'last_updated',
|
||||||
'vlan_count', 'utilization',
|
'vlan_count', 'utilization',
|
||||||
]
|
]
|
||||||
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'vlan_count')
|
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'vlan_count')
|
||||||
|
|||||||
@@ -962,7 +962,7 @@ class VLANGroupFilterSet(OrganizationalModelFilterSet, TenancyFilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = VLANGroup
|
model = VLANGroup
|
||||||
fields = ('id', 'name', 'slug', 'description', 'scope_id')
|
fields = ('id', 'name', 'slug', 'description', 'scope_id', 'total_vlan_ids')
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
if not value.strip():
|
if not value.strip():
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import strawberry_django
|
|||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from netaddr.core import AddrFormatError
|
from netaddr.core import AddrFormatError
|
||||||
from strawberry.scalars import ID
|
from strawberry.scalars import ID
|
||||||
from strawberry_django import BaseFilterLookup, DateFilterLookup, FilterLookup, StrFilterLookup
|
from strawberry_django import BaseFilterLookup, ComparisonFilterLookup, DateFilterLookup, FilterLookup, StrFilterLookup
|
||||||
|
|
||||||
from dcim.graphql.filter_mixins import ScopedFilterMixin
|
from dcim.graphql.filter_mixins import ScopedFilterMixin
|
||||||
from dcim.models import Device
|
from dcim.models import Device
|
||||||
@@ -397,6 +397,7 @@ class VLANGroupFilter(ScopedFilterMixin, OrganizationalModelFilter):
|
|||||||
vid_ranges: Annotated['IntegerRangeArrayLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
vid_ranges: Annotated['IntegerRangeArrayLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||||
strawberry_django.filter_field()
|
strawberry_django.filter_field()
|
||||||
)
|
)
|
||||||
|
total_vlan_ids: ComparisonFilterLookup[int] | None = strawberry_django.filter_field()
|
||||||
|
|
||||||
|
|
||||||
@strawberry_django.filter_type(models.VLANTranslationPolicy, lookups=True)
|
@strawberry_django.filter_type(models.VLANTranslationPolicy, lookups=True)
|
||||||
|
|||||||
@@ -305,6 +305,7 @@ class VLANGroupType(OrganizationalObjectType):
|
|||||||
|
|
||||||
vlans: list[VLANType]
|
vlans: list[VLANType]
|
||||||
vid_ranges: list[str]
|
vid_ranges: list[str]
|
||||||
|
total_vlan_ids: BigInt
|
||||||
tenant: Annotated['TenantType', strawberry.lazy('tenancy.graphql.types')] | None
|
tenant: Annotated['TenantType', strawberry.lazy('tenancy.graphql.types')] | None
|
||||||
|
|
||||||
@strawberry_django.field
|
@strawberry_django.field
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
('ipam', '0086_gfk_indexes'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='vlangroup',
|
||||||
|
old_name='_total_vlan_ids',
|
||||||
|
new_name='total_vlan_ids',
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -62,6 +62,9 @@ class VLANGroup(OrganizationalModel):
|
|||||||
verbose_name=_('VLAN ID ranges'),
|
verbose_name=_('VLAN ID ranges'),
|
||||||
default=default_vid_ranges
|
default=default_vid_ranges
|
||||||
)
|
)
|
||||||
|
total_vlan_ids = models.PositiveBigIntegerField(
|
||||||
|
default=VLAN_VID_MAX - VLAN_VID_MIN + 1,
|
||||||
|
)
|
||||||
tenant = models.ForeignKey(
|
tenant = models.ForeignKey(
|
||||||
to='tenancy.Tenant',
|
to='tenancy.Tenant',
|
||||||
on_delete=models.PROTECT,
|
on_delete=models.PROTECT,
|
||||||
@@ -69,9 +72,6 @@ class VLANGroup(OrganizationalModel):
|
|||||||
blank=True,
|
blank=True,
|
||||||
null=True
|
null=True
|
||||||
)
|
)
|
||||||
_total_vlan_ids = models.PositiveBigIntegerField(
|
|
||||||
default=VLAN_VID_MAX - VLAN_VID_MIN + 1
|
|
||||||
)
|
|
||||||
|
|
||||||
objects = VLANGroupQuerySet.as_manager()
|
objects = VLANGroupQuerySet.as_manager()
|
||||||
|
|
||||||
@@ -130,10 +130,10 @@ class VLANGroup(OrganizationalModel):
|
|||||||
raise ValidationError({'vid_ranges': _("Ranges cannot overlap.")})
|
raise ValidationError({'vid_ranges': _("Ranges cannot overlap.")})
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
self._total_vlan_ids = 0
|
self.total_vlan_ids = 0
|
||||||
for vid_range in self.vid_ranges:
|
for vid_range in self.vid_ranges:
|
||||||
# VID range is inclusive on lower-bound, exclusive on upper-bound
|
# VID range is inclusive on lower-bound, exclusive on upper-bound
|
||||||
self._total_vlan_ids += vid_range.upper - vid_range.lower
|
self.total_vlan_ids += vid_range.upper - vid_range.lower
|
||||||
|
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ class VLANGroupQuerySet(RestrictedQuerySet):
|
|||||||
|
|
||||||
return self.annotate(
|
return self.annotate(
|
||||||
vlan_count=count_related(VLAN, 'group'),
|
vlan_count=count_related(VLAN, 'group'),
|
||||||
utilization=Round(F('vlan_count') * 100.0 / F('_total_vlan_ids'), 2)
|
utilization=Round(F('vlan_count') * 100.0 / F('total_vlan_ids'), 2),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -53,6 +53,9 @@ class VLANGroupTable(TenancyColumnsMixin, OrganizationalModelTable):
|
|||||||
url_params={'group_id': 'pk'},
|
url_params={'group_id': 'pk'},
|
||||||
verbose_name=_('VLANs')
|
verbose_name=_('VLANs')
|
||||||
)
|
)
|
||||||
|
total_vlan_ids = tables.Column(
|
||||||
|
verbose_name=_('Total VLAN IDs'),
|
||||||
|
)
|
||||||
utilization = columns.UtilizationColumn(
|
utilization = columns.UtilizationColumn(
|
||||||
orderable=False,
|
orderable=False,
|
||||||
verbose_name=_('Utilization')
|
verbose_name=_('Utilization')
|
||||||
@@ -67,8 +70,9 @@ class VLANGroupTable(TenancyColumnsMixin, OrganizationalModelTable):
|
|||||||
class Meta(OrganizationalModelTable.Meta):
|
class Meta(OrganizationalModelTable.Meta):
|
||||||
model = VLANGroup
|
model = VLANGroup
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'name', 'scope_type', 'scope', 'vid_ranges_list', 'vlan_count', 'slug', 'description',
|
'pk', 'id', 'name', 'slug', 'description', 'scope_type', 'scope', 'vid_ranges_list', 'vlan_count',
|
||||||
'tenant', 'tenant_group', 'comments', 'tags', 'created', 'last_updated', 'actions', 'utilization',
|
'total_vlan_ids', 'tenant', 'tenant_group', 'comments', 'tags', 'created', 'last_updated', 'actions',
|
||||||
|
'utilization',
|
||||||
)
|
)
|
||||||
default_columns = (
|
default_columns = (
|
||||||
'pk', 'name', 'scope_type', 'scope', 'vlan_count', 'utilization', 'tenant', 'description'
|
'pk', 'name', 'scope_type', 'scope', 'vlan_count', 'utilization', 'tenant', 'description'
|
||||||
|
|||||||
@@ -663,7 +663,7 @@ class TestVLANGroup(TestCase):
|
|||||||
|
|
||||||
def test_total_vlan_ids(self):
|
def test_total_vlan_ids(self):
|
||||||
vlangroup = VLANGroup.objects.first()
|
vlangroup = VLANGroup.objects.first()
|
||||||
self.assertEqual(vlangroup._total_vlan_ids, 100)
|
self.assertEqual(vlangroup.total_vlan_ids, 100)
|
||||||
|
|
||||||
|
|
||||||
class TestVLAN(TestCase):
|
class TestVLAN(TestCase):
|
||||||
|
|||||||
Reference in New Issue
Block a user