diff --git a/netbox/ipam/api/serializers_/vlans.py b/netbox/ipam/api/serializers_/vlans.py index d3b8cc414..608fcf0b4 100644 --- a/netbox/ipam/api/serializers_/vlans.py +++ b/netbox/ipam/api/serializers_/vlans.py @@ -32,7 +32,7 @@ class VLANGroupSerializer(NetBoxModelSerializer): ) scope_id = serializers.IntegerField(allow_null=True, required=False, default=None) scope = serializers.SerializerMethodField(read_only=True) - vlan_id_ranges = IntegerRangeSerializer(many=True, required=False) + vid_ranges = IntegerRangeSerializer(many=True, required=False) utilization = serializers.CharField(read_only=True) # Related object counts @@ -41,7 +41,7 @@ class VLANGroupSerializer(NetBoxModelSerializer): class Meta: model = VLANGroup fields = [ - 'id', 'url', 'display_url', 'display', 'name', 'slug', 'scope_type', 'scope_id', 'scope', 'vlan_id_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' ] brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'vlan_count') diff --git a/netbox/ipam/filtersets.py b/netbox/ipam/filtersets.py index 42e5ab045..30634850a 100644 --- a/netbox/ipam/filtersets.py +++ b/netbox/ipam/filtersets.py @@ -943,7 +943,7 @@ class VLANGroupFilterSet(OrganizationalModelFilterSet): # TODO: See if this can be optimized without compromising queryset integrity # Expand VLAN ID ranges to query by integer groups = VLANGroup.objects.raw( - f'SELECT id FROM {table_name}, unnest(vlan_id_ranges) vid_range WHERE %s <@ vid_range', + f'SELECT id FROM {table_name}, unnest(vid_ranges) vid_range WHERE %s <@ vid_range', params=(value,) ) return queryset.filter( diff --git a/netbox/ipam/forms/bulk_edit.py b/netbox/ipam/forms/bulk_edit.py index 0b9de6d8c..2f59c564f 100644 --- a/netbox/ipam/forms/bulk_edit.py +++ b/netbox/ipam/forms/bulk_edit.py @@ -472,14 +472,14 @@ class VLANGroupBulkEditForm(NetBoxModelBulkEditForm): 'group_id': '$clustergroup', } ) - vlan_id_ranges = NumericRangeArrayField( + vid_ranges = NumericRangeArrayField( label=_('VLAN ID ranges'), required=False ) model = VLANGroup fieldsets = ( - FieldSet('site', 'vlan_id_ranges', 'description'), + FieldSet('site', 'vid_ranges', 'description'), FieldSet( 'scope_type', 'region', 'sitegroup', 'site', 'location', 'rack', 'clustergroup', 'cluster', name=_('Scope') ), diff --git a/netbox/ipam/forms/bulk_import.py b/netbox/ipam/forms/bulk_import.py index 24dbf5895..dea250c79 100644 --- a/netbox/ipam/forms/bulk_import.py +++ b/netbox/ipam/forms/bulk_import.py @@ -412,13 +412,13 @@ class VLANGroupImportForm(NetBoxModelImportForm): required=False, label=_('Scope type (app & model)') ) - vlan_id_ranges = NumericRangeArrayField( + vid_ranges = NumericRangeArrayField( required=False ) class Meta: model = VLANGroup - fields = ('name', 'slug', 'scope_type', 'scope_id', 'vlan_id_ranges', 'description', 'tags') + fields = ('name', 'slug', 'scope_type', 'scope_id', 'vid_ranges', 'description', 'tags') labels = { 'scope_id': 'Scope ID', } diff --git a/netbox/ipam/forms/model_forms.py b/netbox/ipam/forms/model_forms.py index bdbc98d9b..e6060d1af 100644 --- a/netbox/ipam/forms/model_forms.py +++ b/netbox/ipam/forms/model_forms.py @@ -633,13 +633,13 @@ class VLANGroupForm(NetBoxModelForm): } ) slug = SlugField() - vlan_id_ranges = NumericRangeArrayField( + vid_ranges = NumericRangeArrayField( label=_('VLAN IDs') ) fieldsets = ( FieldSet('name', 'slug', 'description', 'tags', name=_('VLAN Group')), - FieldSet('vlan_id_ranges', name=_('Child VLANs')), + FieldSet('vid_ranges', name=_('Child VLANs')), FieldSet( 'scope_type', 'region', 'sitegroup', 'site', 'location', 'rack', 'clustergroup', 'cluster', name=_('Scope') @@ -650,7 +650,7 @@ class VLANGroupForm(NetBoxModelForm): model = VLANGroup fields = [ 'name', 'slug', 'description', 'scope_type', 'region', 'sitegroup', 'site', 'location', 'rack', - 'clustergroup', 'cluster', 'vlan_id_ranges', 'tags', + 'clustergroup', 'cluster', 'vid_ranges', 'tags', ] def __init__(self, *args, **kwargs): diff --git a/netbox/ipam/graphql/types.py b/netbox/ipam/graphql/types.py index 2adaa31f2..46d45816e 100644 --- a/netbox/ipam/graphql/types.py +++ b/netbox/ipam/graphql/types.py @@ -251,7 +251,7 @@ class VLANType(NetBoxObjectType): class VLANGroupType(OrganizationalObjectType): vlans: List[VLANType] - vlan_id_ranges: List[str] + vid_ranges: List[str] @strawberry_django.field def scope(self) -> Annotated[Union[ diff --git a/netbox/ipam/migrations/0070_vlangroup_vlan_id_ranges.py b/netbox/ipam/migrations/0070_vlangroup_vlan_id_ranges.py index 4102ab991..926f4401d 100644 --- a/netbox/ipam/migrations/0070_vlangroup_vlan_id_ranges.py +++ b/netbox/ipam/migrations/0070_vlangroup_vlan_id_ranges.py @@ -10,12 +10,12 @@ def move_min_max(apps, schema_editor): VLANGroup = apps.get_model('ipam', 'VLANGroup') for group in VLANGroup.objects.all(): if group.min_vid or group.max_vid: - group.vlan_id_ranges = [ + group.vid_ranges = [ NumericRange(group.min_vid, group.max_vid, bounds='[]') ] group._total_vlan_ids = 0 - for vlan_range in group.vlan_id_ranges: + for vlan_range in group.vid_ranges: group._total_vlan_ids += int(vlan_range.upper) - int(vlan_range.lower) + 1 group.save() @@ -30,10 +30,10 @@ class Migration(migrations.Migration): operations = [ migrations.AddField( model_name='vlangroup', - name='vlan_id_ranges', + name='vid_ranges', field=django.contrib.postgres.fields.ArrayField( base_field=django.contrib.postgres.fields.ranges.IntegerRangeField(), - default=ipam.models.vlans.default_vlan_id_ranges, + default=ipam.models.vlans.default_vid_ranges, size=None ), ), diff --git a/netbox/ipam/models/vlans.py b/netbox/ipam/models/vlans.py index aa954932a..8562217a2 100644 --- a/netbox/ipam/models/vlans.py +++ b/netbox/ipam/models/vlans.py @@ -21,7 +21,7 @@ __all__ = ( ) -def default_vlan_id_ranges(): +def default_vid_ranges(): return [ NumericRange(VLAN_VID_MIN, VLAN_VID_MAX, bounds='[]') ] @@ -54,10 +54,10 @@ class VLANGroup(OrganizationalModel): ct_field='scope_type', fk_field='scope_id' ) - vlan_id_ranges = ArrayField( + vid_ranges = ArrayField( IntegerRangeField(), verbose_name=_('VLAN ID ranges'), - default=default_vlan_id_ranges + default=default_vid_ranges ) _total_vlan_ids = models.PositiveBigIntegerField( default=VLAN_VID_MAX - VLAN_VID_MIN + 1 @@ -96,20 +96,20 @@ class VLANGroup(OrganizationalModel): raise ValidationError(_("Cannot set scope_id without scope_type.")) # Validate vlan ranges - if self.vlan_id_ranges and check_ranges_overlap(self.vlan_id_ranges): - raise ValidationError({'vlan_id_ranges': _("Ranges cannot overlap.")}) + if self.vid_ranges and check_ranges_overlap(self.vid_ranges): + raise ValidationError({'vid_ranges': _("Ranges cannot overlap.")}) - for vid_range in self.vlan_id_ranges: + for vid_range in self.vid_ranges: if vid_range.lower >= vid_range.upper: raise ValidationError({ - 'vlan_id_ranges': _( + 'vid_ranges': _( "Maximum child VID must be greater than or equal to minimum child VID ({value})" ).format(value=vid_range) }) def save(self, *args, **kwargs): self._total_vlan_ids = 0 - for vid_range in self.vlan_id_ranges: + for vid_range in self.vid_ranges: self._total_vlan_ids += vid_range.upper - vid_range.lower + 1 super().save(*args, **kwargs) @@ -119,7 +119,7 @@ class VLANGroup(OrganizationalModel): Return all available VLANs within this group. """ available_vlans = {} - for vlan_range in self.vlan_id_ranges: + for vlan_range in self.vid_ranges: available_vlans = {vid for vid in range(vlan_range.lower, vlan_range.upper)} available_vlans -= set(VLAN.objects.filter(group=self).values_list('vid', flat=True)) @@ -141,8 +141,8 @@ class VLANGroup(OrganizationalModel): return VLAN.objects.filter(group=self).order_by('vid') @property - def vlan_ranges(self): - return ranges_to_string(self.vlan_id_ranges) + def vid_ranges_list(self): + return ranges_to_string(self.vid_ranges) class VLAN(PrimaryModel): @@ -250,9 +250,9 @@ class VLAN(PrimaryModel): ) # Validate group min/max VIDs - if self.group and self.group.vlan_id_ranges: + if self.group and self.group.vid_ranges: in_bounds = False - for vid_range in self.group.vlan_id_ranges: + for vid_range in self.group.vid_ranges: if vid_range.lower <= self.vid <= vid_range.upper: in_bounds = True @@ -260,7 +260,7 @@ class VLAN(PrimaryModel): raise ValidationError({ 'vid': _( "VID must be in ranges {ranges} for VLANs in group {group}" - ).format(ranges=ranges_to_string(self.group.vlan_id_ranges), group=self.group) + ).format(ranges=ranges_to_string(self.group.vid_ranges), group=self.group) }) def get_status_color(self): diff --git a/netbox/ipam/tables/vlans.py b/netbox/ipam/tables/vlans.py index b5a98a27e..1b428aeb6 100644 --- a/netbox/ipam/tables/vlans.py +++ b/netbox/ipam/tables/vlans.py @@ -72,8 +72,8 @@ class VLANGroupTable(NetBoxTable): linkify=True, orderable=False ) - vlan_ranges = tables.Column( - verbose_name=_('VLAN Ranges'), + vid_ranges_list = tables.Column( + verbose_name=_('VID Ranges'), orderable=False ) vlan_count = columns.LinkedCountColumn( @@ -95,7 +95,7 @@ class VLANGroupTable(NetBoxTable): class Meta(NetBoxTable.Meta): model = VLANGroup fields = ( - 'pk', 'id', 'name', 'scope_type', 'scope', 'vlan_ranges', 'vlan_count', 'slug', 'description', + 'pk', 'id', 'name', 'scope_type', 'scope', 'vid_ranges_list', 'vlan_count', 'slug', 'description', 'tags', 'created', 'last_updated', 'actions', 'utilization', ) default_columns = ('pk', 'name', 'scope_type', 'scope', 'vlan_count', 'utilization', 'description') diff --git a/netbox/ipam/tests/test_api.py b/netbox/ipam/tests/test_api.py index c6490856b..d2c90c8ad 100644 --- a/netbox/ipam/tests/test_api.py +++ b/netbox/ipam/tests/test_api.py @@ -883,7 +883,7 @@ class VLANGroupTest(APIViewTestCases.APIViewTestCase): vlangroup = VLANGroup.objects.create( name='VLAN Group X', slug='vlan-group-x', - vlan_id_ranges=string_to_range_array(f"{MIN_VID}-{MAX_VID}") + vid_ranges=string_to_range_array(f"{MIN_VID}-{MAX_VID}") ) # Create a set of VLANs within the group diff --git a/netbox/ipam/tests/test_filtersets.py b/netbox/ipam/tests/test_filtersets.py index a961c1e84..e149c0a8d 100644 --- a/netbox/ipam/tests/test_filtersets.py +++ b/netbox/ipam/tests/test_filtersets.py @@ -1466,7 +1466,7 @@ class FHRPGroupAssignmentTestCase(TestCase, ChangeLoggedFilterSetTests): class VLANGroupTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = VLANGroup.objects.all() filterset = VLANGroupFilterSet - ignore_fields = ('vlan_id_ranges',) + ignore_fields = ('vid_ranges',) @classmethod def setUpTestData(cls): @@ -1499,46 +1499,46 @@ class VLANGroupTestCase(TestCase, ChangeLoggedFilterSetTests): VLANGroup( name='VLAN Group 1', slug='vlan-group-1', - vlan_id_ranges=[NumericRange(1, 11), NumericRange(100, 200)], + vid_ranges=[NumericRange(1, 11), NumericRange(100, 200)], scope=region, description='foobar1' ), VLANGroup( name='VLAN Group 2', slug='vlan-group-2', - vlan_id_ranges=[NumericRange(1, 11), NumericRange(200, 300)], + vid_ranges=[NumericRange(1, 11), NumericRange(200, 300)], scope=sitegroup, description='foobar2' ), VLANGroup( name='VLAN Group 3', slug='vlan-group-3', - vlan_id_ranges=[NumericRange(1, 11), NumericRange(300, 400)], + vid_ranges=[NumericRange(1, 11), NumericRange(300, 400)], scope=site, description='foobar3' ), VLANGroup( name='VLAN Group 4', slug='vlan-group-4', - vlan_id_ranges=[NumericRange(1, 11), NumericRange(400, 500)], + vid_ranges=[NumericRange(1, 11), NumericRange(400, 500)], scope=location ), VLANGroup( name='VLAN Group 5', slug='vlan-group-5', - vlan_id_ranges=[NumericRange(1, 11), NumericRange(500, 600)], + vid_ranges=[NumericRange(1, 11), NumericRange(500, 600)], scope=rack ), VLANGroup( name='VLAN Group 6', slug='vlan-group-6', - vlan_id_ranges=[NumericRange(1, 11), NumericRange(600, 700)], + vid_ranges=[NumericRange(1, 11), NumericRange(600, 700)], scope=clustergroup ), VLANGroup( name='VLAN Group 7', slug='vlan-group-7', - vlan_id_ranges=[NumericRange(1, 11), NumericRange(700, 800)], + vid_ranges=[NumericRange(1, 11), NumericRange(700, 800)], scope=cluster ), VLANGroup( diff --git a/netbox/ipam/tests/test_models.py b/netbox/ipam/tests/test_models.py index 692569e6e..0af23bb57 100644 --- a/netbox/ipam/tests/test_models.py +++ b/netbox/ipam/tests/test_models.py @@ -510,7 +510,7 @@ class TestVLANGroup(TestCase): vlangroup = VLANGroup.objects.create( name='VLAN Group 1', slug='vlan-group-1', - vlan_id_ranges=string_to_range_array('100-199'), + vid_ranges=string_to_range_array('100-199'), ) VLAN.objects.bulk_create(( VLAN(name='VLAN 100', vid=100, group=vlangroup), diff --git a/netbox/ipam/tests/test_views.py b/netbox/ipam/tests/test_views.py index 75396e498..2acb80ac1 100644 --- a/netbox/ipam/tests/test_views.py +++ b/netbox/ipam/tests/test_views.py @@ -765,7 +765,7 @@ class VLANGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase): 'name': 'VLAN Group X', 'slug': 'vlan-group-x', 'description': 'A new VLAN group', - 'vlan_id_ranges': '100-199,300-399', + 'vid_ranges': '100-199,300-399', 'tags': [t.pk for t in tags], } diff --git a/netbox/ipam/utils.py b/netbox/ipam/utils.py index d32231143..3a6e96a6c 100644 --- a/netbox/ipam/utils.py +++ b/netbox/ipam/utils.py @@ -136,8 +136,8 @@ def add_available_vlans(vlans, vlan_group=None): Create fake records for all gaps between used VLANs """ new_vlans = [] - if vlan_group and vlan_group.vlan_id_ranges: - for vlan_range in vlan_group.vlan_id_ranges: + if vlan_group and vlan_group.vid_ranges: + for vlan_range in vlan_group.vid_ranges: new_vlans.extend(available_vlans_from_range(vlans, vlan_group, vlan_range)) else: new_vlans = available_vlans_from_range(vlans, vlan_group, vlan_range) diff --git a/netbox/utilities/data.py b/netbox/utilities/data.py index a24909e13..1e610e8a0 100644 --- a/netbox/utilities/data.py +++ b/netbox/utilities/data.py @@ -142,7 +142,12 @@ def ranges_to_string(ranges): """ if not ranges: return '' - return ','.join([f"{r.lower}-{r.upper - 1}" for r in ranges]) + output = [] + for r in ranges: + lower = r.lower if r.lower_inc else r.lower + 1 + upper = r.upper if r.upper_inc else r.upper - 1 + output.append(f'{lower}-{upper}') + return ','.join(output) def string_to_range_array(value):