diff --git a/netbox/ipam/api/serializers_/vlans.py b/netbox/ipam/api/serializers_/vlans.py index b26b28a34..d3b8cc414 100644 --- a/netbox/ipam/api/serializers_/vlans.py +++ b/netbox/ipam/api/serializers_/vlans.py @@ -6,11 +6,10 @@ from dcim.api.serializers_.sites import SiteSerializer from ipam.choices import * from ipam.constants import VLANGROUP_SCOPE_TYPES from ipam.models import VLAN, VLANGroup -from netbox.api.fields import ChoiceField, ContentTypeField, RelatedObjectCountField +from netbox.api.fields import ChoiceField, ContentTypeField, IntegerRangeSerializer, RelatedObjectCountField from netbox.api.serializers import NetBoxModelSerializer from tenancy.api.serializers_.tenants import TenantSerializer from utilities.api import get_serializer_for_model -from utilities.data import ranges_to_string, string_to_range_array from vpn.api.serializers_.l2vpn import L2VPNTerminationSerializer from .roles import RoleSerializer @@ -22,14 +21,6 @@ __all__ = ( ) -class NumericRangeArraySerializer(serializers.CharField): - def to_internal_value(self, data): - return string_to_range_array(data) - - def to_representation(self, instance): - return ranges_to_string(instance) - - class VLANGroupSerializer(NetBoxModelSerializer): scope_type = ContentTypeField( queryset=ContentType.objects.filter( @@ -41,11 +32,11 @@ 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) utilization = serializers.CharField(read_only=True) # Related object counts vlan_count = RelatedObjectCountField('vlans') - vlan_id_ranges = NumericRangeArraySerializer(required=False) class Meta: model = VLANGroup diff --git a/netbox/ipam/models/vlans.py b/netbox/ipam/models/vlans.py index 7459efb75..389eff64c 100644 --- a/netbox/ipam/models/vlans.py +++ b/netbox/ipam/models/vlans.py @@ -96,19 +96,24 @@ class VLANGroup(OrganizationalModel): raise ValidationError(_("Cannot set scope_id without scope_type.")) # Validate vlan ranges - if check_ranges_overlap(self.vlan_id_ranges): + if self.vlan_id_ranges and check_ranges_overlap(self.vlan_id_ranges): raise ValidationError({'vlan_id_ranges': _("Ranges cannot overlap.")}) for ranges in self.vlan_id_ranges: if ranges.lower >= ranges.upper: raise ValidationError({ - 'vlan_id_ranges': _("Maximum child VID must be greater than or equal to minimum child VID Invalid range ({value})").format(value=ranges) + 'vlan_id_ranges': _( + "Maximum child VID must be greater than or equal to minimum child VID Invalid range ({value})" + ).format(value=ranges) }) def save(self, *args, **kwargs): - self._total_vlan_ids = 0 - for vlan_range in self.vlan_id_ranges: - self._total_vlan_ids += vlan_range.upper - vlan_range.lower + 1 + if self.vlan_id_ranges: + self._total_vlan_ids = 0 + for vlan_range in self.vlan_id_ranges: + self._total_vlan_ids += vlan_range.upper - vlan_range.lower + 1 + else: + self._total_vlan_ids = VLAN_VID_MAX - VLAN_VID_MIN + 1 super().save(*args, **kwargs) diff --git a/netbox/netbox/api/fields.py b/netbox/netbox/api/fields.py index 08ffd0bc4..8e0b29f89 100644 --- a/netbox/netbox/api/fields.py +++ b/netbox/netbox/api/fields.py @@ -1,4 +1,5 @@ from django.core.exceptions import ObjectDoesNotExist +from django.db.backends.postgresql.psycopg_any import NumericRange from django.utils.translation import gettext as _ from drf_spectacular.types import OpenApiTypes from drf_spectacular.utils import extend_schema_field @@ -11,6 +12,7 @@ __all__ = ( 'ChoiceField', 'ContentTypeField', 'IPNetworkSerializer', + 'IntegerRangeSerializer', 'RelatedObjectCountField', 'SerializedPKRelatedField', ) @@ -154,3 +156,19 @@ class RelatedObjectCountField(serializers.ReadOnlyField): self.relation = relation super().__init__(**kwargs) + + +class IntegerRangeSerializer(serializers.Serializer): + """ + Represents a range of integers. + """ + def to_internal_value(self, data): + if not isinstance(data, (list, tuple)) or len(data) != 2: + raise ValidationError(_("Ranges must be specified in the form (lower, upper).")) + if type(data[0]) is not int or type(data[1]) is not int: + raise ValidationError(_("Range boundaries must be defined as integers.")) + + return NumericRange(data[0], data[1]) + + def to_representation(self, instance): + return instance.lower, instance.upper diff --git a/netbox/utilities/data.py b/netbox/utilities/data.py index ad3e05abd..51b489340 100644 --- a/netbox/utilities/data.py +++ b/netbox/utilities/data.py @@ -140,6 +140,8 @@ def ranges_to_string(ranges): For example: [Range(1, 100), Range(200, 300)] => "1-100, 200-300" """ + if not ranges: + return '' return ', '.join([f"{r.lower}-{r.upper}" for r in ranges])