From 05fd14a59af4adc96b75c8096a43fad146c67eb9 Mon Sep 17 00:00:00 2001 From: Martin Hauser Date: Tue, 28 Oct 2025 14:11:46 +0100 Subject: [PATCH] fix(ipam): Correct VLAN ID range calculation logic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adjust VLAN ID range calculation to use half‑open intervals for consistency. Add a test to validate `_total_vlan_ids`. Fixes #20610 --- .../0083_vlangroup_populate_total_vlan_ids.py | 27 +++++++++++++++++++ netbox/ipam/models/vlans.py | 3 ++- netbox/ipam/tests/test_models.py | 4 +++ 3 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 netbox/ipam/migrations/0083_vlangroup_populate_total_vlan_ids.py diff --git a/netbox/ipam/migrations/0083_vlangroup_populate_total_vlan_ids.py b/netbox/ipam/migrations/0083_vlangroup_populate_total_vlan_ids.py new file mode 100644 index 00000000000..4308975881f --- /dev/null +++ b/netbox/ipam/migrations/0083_vlangroup_populate_total_vlan_ids.py @@ -0,0 +1,27 @@ +from django.db import migrations + + +def populate_vlangroup_total_vlan_ids(apps, schema_editor): + VLANGroup = apps.get_model('ipam', 'VLANGroup') + db_alias = schema_editor.connection.alias + + vlan_groups = VLANGroup.objects.using(db_alias).only('id', 'vid_ranges') + for group in vlan_groups: + total_vlan_ids = 0 + if group.vid_ranges: + for r in group.vid_ranges: + # Half-open [lo, hi): length is (hi - lo). + if r is not None and r.lower is not None and r.upper is not None: + total_vlan_ids += r.upper - r.lower + group._total_vlan_ids = total_vlan_ids + VLANGroup.objects.using(db_alias).bulk_update(vlan_groups, ['_total_vlan_ids'], batch_size=100) + + +class Migration(migrations.Migration): + dependencies = [ + ('ipam', '0082_add_prefix_network_containment_indexes'), + ] + + operations = [ + migrations.RunPython(populate_vlangroup_total_vlan_ids, migrations.RunPython.noop), + ] diff --git a/netbox/ipam/models/vlans.py b/netbox/ipam/models/vlans.py index efa1ed39ed3..030633330a4 100644 --- a/netbox/ipam/models/vlans.py +++ b/netbox/ipam/models/vlans.py @@ -132,7 +132,8 @@ def clean(self): def save(self, *args, **kwargs): self._total_vlan_ids = 0 for vid_range in self.vid_ranges: - self._total_vlan_ids += vid_range.upper - vid_range.lower + 1 + # VID range is inclusive on lower-bound, exclusive on upper-bound + self._total_vlan_ids += vid_range.upper - vid_range.lower super().save(*args, **kwargs) diff --git a/netbox/ipam/tests/test_models.py b/netbox/ipam/tests/test_models.py index 656f62d87c2..c93a47d21fe 100644 --- a/netbox/ipam/tests/test_models.py +++ b/netbox/ipam/tests/test_models.py @@ -661,6 +661,10 @@ def test_overlapping_vlan(self): vlangroup.full_clean() vlangroup.save() + def test_total_vlan_ids(self): + vlangroup = VLANGroup.objects.first() + self.assertEqual(vlangroup._total_vlan_ids, 100) + class TestVLAN(TestCase):