diff --git a/netbox/ipam/models/vlans.py b/netbox/ipam/models/vlans.py
index a5e06bc29..ee994cf0c 100644
--- a/netbox/ipam/models/vlans.py
+++ b/netbox/ipam/models/vlans.py
@@ -95,7 +95,7 @@ class VLANGroup(OrganizationalModel):
if self.scope_id and not self.scope_type:
raise ValidationError(_("Cannot set scope_id without scope_type."))
- # Validate vlan ranges
+ # Validate VID ranges
if self.vid_ranges and check_ranges_overlap(self.vid_ranges):
raise ValidationError({'vid_ranges': _("Ranges cannot overlap.")})
diff --git a/netbox/ipam/tests/test_api.py b/netbox/ipam/tests/test_api.py
index d2c90c8ad..00c240769 100644
--- a/netbox/ipam/tests/test_api.py
+++ b/netbox/ipam/tests/test_api.py
@@ -8,7 +8,7 @@ from dcim.models import Device, DeviceRole, DeviceType, Interface, Manufacturer,
from ipam.choices import *
from ipam.models import *
from tenancy.models import Tenant
-from utilities.data import string_to_range_array
+from utilities.data import string_to_ranges
from utilities.testing import APITestCase, APIViewTestCases, create_test_device, disable_warnings
@@ -883,7 +883,7 @@ class VLANGroupTest(APIViewTestCases.APIViewTestCase):
vlangroup = VLANGroup.objects.create(
name='VLAN Group X',
slug='vlan-group-x',
- vid_ranges=string_to_range_array(f"{MIN_VID}-{MAX_VID}")
+ vid_ranges=string_to_ranges(f"{MIN_VID}-{MAX_VID}")
)
# Create a set of VLANs within the group
diff --git a/netbox/ipam/tests/test_models.py b/netbox/ipam/tests/test_models.py
index 0af23bb57..f7c165066 100644
--- a/netbox/ipam/tests/test_models.py
+++ b/netbox/ipam/tests/test_models.py
@@ -1,7 +1,7 @@
from django.core.exceptions import ValidationError
from django.test import TestCase, override_settings
from netaddr import IPNetwork, IPSet
-from utilities.data import string_to_range_array
+from utilities.data import string_to_ranges
from ipam.choices import *
from ipam.models import *
@@ -510,7 +510,7 @@ class TestVLANGroup(TestCase):
vlangroup = VLANGroup.objects.create(
name='VLAN Group 1',
slug='vlan-group-1',
- vid_ranges=string_to_range_array('100-199'),
+ vid_ranges=string_to_ranges('100-199'),
)
VLAN.objects.bulk_create((
VLAN(name='VLAN 100', vid=100, group=vlangroup),
diff --git a/netbox/templates/ipam/vlangroup.html b/netbox/templates/ipam/vlangroup.html
index f44bcd319..efd2edcfc 100644
--- a/netbox/templates/ipam/vlangroup.html
+++ b/netbox/templates/ipam/vlangroup.html
@@ -40,7 +40,7 @@
{% trans "VLAN IDs" %} |
- {{ object.vlan_ranges }} |
+ {{ object.vid_ranges_list }} |
Utilization |
diff --git a/netbox/utilities/data.py b/netbox/utilities/data.py
index 1e610e8a0..73c8476ec 100644
--- a/netbox/utilities/data.py
+++ b/netbox/utilities/data.py
@@ -11,7 +11,7 @@ __all__ = (
'flatten_dict',
'ranges_to_string',
'shallow_compare_dict',
- 'string_to_range_array',
+ 'string_to_ranges',
)
@@ -121,14 +121,15 @@ def drange(start, end, step=decimal.Decimal(1)):
def check_ranges_overlap(ranges):
"""
- Check if array of ranges overlap
+ Check for overlap in an iterable of NumericRanges.
"""
-
- # sort the ranges in increasing order
ranges.sort(key=lambda x: x.lower)
for i in range(1, len(ranges)):
- if (ranges[i - 1].upper >= ranges[i].lower):
+ prev_range = ranges[i - 1]
+ prev_upper = prev_range.upper if prev_range.upper_inc else prev_range.upper - 1
+ lower = ranges[i].lower if ranges[i].lower_inc else ranges[i].lower + 1
+ if prev_upper >= lower:
return True
return False
@@ -136,8 +137,7 @@ def check_ranges_overlap(ranges):
def ranges_to_string(ranges):
"""
- Generate a human-friendly string from a set of ranges. Intended for use with ArrayField.
- For example:
+ Generate a human-friendly string from a set of ranges. Intended for use with ArrayField. For example:
[[1, 100)], [200, 300)] => "1-99,200-299"
"""
if not ranges:
@@ -150,14 +150,15 @@ def ranges_to_string(ranges):
return ','.join(output)
-def string_to_range_array(value):
+def string_to_ranges(value):
"""
- Given a string in the format "1-100, 200-300" create an array of ranges. Intended for use with ArrayField.
+ Given a string in the format "1-100, 200-300" return an list of NumericRanges. Intended for use with ArrayField.
For example:
"1-99,200-299" => [NumericRange(1, 100), NumericRange(200, 300)]
"""
if not value:
return None
+ value.replace(' ', '') # Remove whitespace
values = []
for dash_range in value.split(','):
if '-' not in dash_range:
diff --git a/netbox/utilities/forms/fields/array.py b/netbox/utilities/forms/fields/array.py
index eef85672b..e6de2d89f 100644
--- a/netbox/utilities/forms/fields/array.py
+++ b/netbox/utilities/forms/fields/array.py
@@ -2,7 +2,7 @@ from django import forms
from django.contrib.postgres.forms import SimpleArrayField
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
-from utilities.data import ranges_to_string, string_to_range_array
+from utilities.data import ranges_to_string, string_to_ranges
from ..utils import parse_numeric_range
@@ -54,4 +54,4 @@ class NumericRangeArrayField(forms.CharField):
return ranges_to_string(value)
def to_python(self, value):
- return string_to_range_array(value)
+ return string_to_ranges(value)
diff --git a/netbox/utilities/tests/test_data.py b/netbox/utilities/tests/test_data.py
new file mode 100644
index 000000000..c83885233
--- /dev/null
+++ b/netbox/utilities/tests/test_data.py
@@ -0,0 +1,68 @@
+from django.db.backends.postgresql.psycopg_any import NumericRange
+from django.test import TestCase
+
+from utilities.data import check_ranges_overlap, ranges_to_string, string_to_ranges
+
+
+class RangeFunctionsTestCase(TestCase):
+
+ def test_check_ranges_overlap(self):
+ # Non-overlapping ranges
+ self.assertFalse(
+ check_ranges_overlap([
+ NumericRange(9, 19, bounds='(]'), # 10-19
+ NumericRange(19, 30, bounds='(]'), # 20-29
+ ])
+ )
+ self.assertFalse(
+ check_ranges_overlap([
+ NumericRange(10, 19, bounds='[]'), # 10-19
+ NumericRange(20, 29, bounds='[]'), # 20-29
+ ])
+ )
+ self.assertFalse(
+ check_ranges_overlap([
+ NumericRange(10, 20, bounds='[)'), # 10-19
+ NumericRange(20, 30, bounds='[)'), # 20-29
+ ])
+ )
+
+ # Overlapping ranges
+ self.assertTrue(
+ check_ranges_overlap([
+ NumericRange(9, 20, bounds='(]'), # 10-20
+ NumericRange(19, 30, bounds='(]'), # 20-30
+ ])
+ )
+ self.assertTrue(
+ check_ranges_overlap([
+ NumericRange(10, 20, bounds='[]'), # 10-20
+ NumericRange(20, 30, bounds='[]'), # 20-30
+ ])
+ )
+ self.assertTrue(
+ check_ranges_overlap([
+ NumericRange(10, 21, bounds='[)'), # 10-20
+ NumericRange(20, 31, bounds='[)'), # 10-30
+ ])
+ )
+
+ def test_ranges_to_string(self):
+ self.assertEqual(
+ ranges_to_string([
+ NumericRange(10, 20), # 10-19
+ NumericRange(30, 40), # 30-39
+ NumericRange(100, 200), # 100-199
+ ]),
+ '10-19,30-39,100-199'
+ )
+
+ def test_string_to_ranges(self):
+ self.assertEqual(
+ string_to_ranges('10-19, 30-39, 100-199'),
+ [
+ NumericRange(10, 19, bounds='[]'), # 10-19
+ NumericRange(30, 39, bounds='[]'), # 30-39
+ NumericRange(100, 199, bounds='[]'), # 100-199
+ ]
+ )