From c1a0eb8b37665937d23845fb688e0ebd9dcd2ffd Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 15 Jul 2024 16:27:38 -0400 Subject: [PATCH] Fix logic for upper range boundaries --- .../ipam/migrations/0070_vlangroup_vlan_id_ranges.py | 6 ++++-- netbox/ipam/models/vlans.py | 6 +++--- netbox/ipam/querysets.py | 1 + netbox/netbox/api/fields.py | 4 ++-- netbox/utilities/data.py | 10 ++++------ netbox/utilities/testing/base.py | 6 +++++- 6 files changed, 19 insertions(+), 14 deletions(-) diff --git a/netbox/ipam/migrations/0070_vlangroup_vlan_id_ranges.py b/netbox/ipam/migrations/0070_vlangroup_vlan_id_ranges.py index b8cc987c3..4102ab991 100644 --- a/netbox/ipam/migrations/0070_vlangroup_vlan_id_ranges.py +++ b/netbox/ipam/migrations/0070_vlangroup_vlan_id_ranges.py @@ -10,7 +10,9 @@ 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 = [NumericRange(group.min_vid, group.max_vid)] + group.vlan_id_ranges = [ + NumericRange(group.min_vid, group.max_vid, bounds='[]') + ] group._total_vlan_ids = 0 for vlan_range in group.vlan_id_ranges: @@ -31,7 +33,7 @@ class Migration(migrations.Migration): name='vlan_id_ranges', field=django.contrib.postgres.fields.ArrayField( base_field=django.contrib.postgres.fields.ranges.IntegerRangeField(), - default=ipam.models.vlans.default_vland_id_ranges, + default=ipam.models.vlans.default_vlan_id_ranges, size=None ), ), diff --git a/netbox/ipam/models/vlans.py b/netbox/ipam/models/vlans.py index ceefa8905..aa954932a 100644 --- a/netbox/ipam/models/vlans.py +++ b/netbox/ipam/models/vlans.py @@ -21,7 +21,7 @@ __all__ = ( ) -def default_vland_id_ranges(): +def default_vlan_id_ranges(): return [ NumericRange(VLAN_VID_MIN, VLAN_VID_MAX, bounds='[]') ] @@ -57,7 +57,7 @@ class VLANGroup(OrganizationalModel): vlan_id_ranges = ArrayField( IntegerRangeField(), verbose_name=_('VLAN ID ranges'), - default=default_vland_id_ranges + default=default_vlan_id_ranges ) _total_vlan_ids = models.PositiveBigIntegerField( default=VLAN_VID_MAX - VLAN_VID_MIN + 1 @@ -120,7 +120,7 @@ class VLANGroup(OrganizationalModel): """ available_vlans = {} for vlan_range in self.vlan_id_ranges: - available_vlans = {vid for vid in range(vlan_range.lower, vlan_range.upper + 1)} + 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)) return sorted(available_vlans) diff --git a/netbox/ipam/querysets.py b/netbox/ipam/querysets.py index 076d05445..717c63a37 100644 --- a/netbox/ipam/querysets.py +++ b/netbox/ipam/querysets.py @@ -9,6 +9,7 @@ from utilities.querysets import RestrictedQuerySet __all__ = ( 'ASNRangeQuerySet', 'PrefixQuerySet', + 'VLANGroupQuerySet', 'VLANQuerySet', ) diff --git a/netbox/netbox/api/fields.py b/netbox/netbox/api/fields.py index 8e0b29f89..e7d1ef574 100644 --- a/netbox/netbox/api/fields.py +++ b/netbox/netbox/api/fields.py @@ -168,7 +168,7 @@ class IntegerRangeSerializer(serializers.Serializer): 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]) + return NumericRange(data[0], data[1], bounds='[]') def to_representation(self, instance): - return instance.lower, instance.upper + return instance.lower, instance.upper - 1 diff --git a/netbox/utilities/data.py b/netbox/utilities/data.py index 51b489340..a24909e13 100644 --- a/netbox/utilities/data.py +++ b/netbox/utilities/data.py @@ -138,27 +138,25 @@ def ranges_to_string(ranges): """ Generate a human-friendly string from a set of ranges. Intended for use with ArrayField. For example: - [Range(1, 100), Range(200, 300)] => "1-100, 200-300" + [[1, 100)], [200, 300)] => "1-99,200-299" """ if not ranges: return '' - return ', '.join([f"{r.lower}-{r.upper}" for r in ranges]) + return ','.join([f"{r.lower}-{r.upper - 1}" for r in ranges]) def string_to_range_array(value): """ Given a string in the format "1-100, 200-300" create an array of ranges. Intended for use with ArrayField. For example: - "1-100, 200-300" => [1-100, 200-300] + "1-99,200-299" => [NumericRange(1, 100), NumericRange(200, 300)] """ if not value: return None - ranges = value.split(",") values = [] for dash_range in value.split(','): if '-' not in dash_range: return None - lower, upper = dash_range.split('-') - values.append(NumericRange(int(lower), int(upper))) + values.append(NumericRange(int(lower), int(upper), bounds='[]')) return values diff --git a/netbox/utilities/testing/base.py b/netbox/utilities/testing/base.py index 397d830db..eb45aa784 100644 --- a/netbox/utilities/testing/base.py +++ b/netbox/utilities/testing/base.py @@ -2,7 +2,7 @@ import json from django.contrib.auth import get_user_model from django.contrib.contenttypes.models import ContentType -from django.contrib.postgres.fields import ArrayField +from django.contrib.postgres.fields import ArrayField, RangeField from django.core.exceptions import FieldDoesNotExist from django.db.models import ManyToManyField, ManyToManyRel, JSONField from django.forms.models import model_to_dict @@ -12,6 +12,7 @@ from taggit.managers import TaggableManager from core.models import ObjectType from users.models import ObjectPermission +from utilities.data import ranges_to_string from utilities.object_types import object_type_identifier from utilities.permissions import resolve_permission_type from .utils import DUMMY_CF_DATA, extract_form_failures @@ -139,6 +140,9 @@ class ModelTestCase(TestCase): if type(field.base_field) is ArrayField: # Handle nested arrays (e.g. choice sets) model_dict[key] = '\n'.join([f'{k},{v}' for k, v in value]) + elif issubclass(type(field.base_field), RangeField): + # Handle arrays of numeric ranges (e.g. VLANGroup VLAN ID ranges) + model_dict[key] = ranges_to_string(value) else: model_dict[key] = ','.join([str(v) for v in value])