mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-14 09:51:22 -06:00
Closes #8168: Add min/max VID fields to VLANGroup
This commit is contained in:
parent
083fda3172
commit
544d991e1e
@ -2,4 +2,6 @@
|
||||
|
||||
VLAN groups can be used to organize VLANs within NetBox. Each VLAN group can be scoped to a particular region, site group, site, location, rack, cluster group, or cluster. Member VLANs will be available for assignment to devices and/or virtual machines within the specified scope.
|
||||
|
||||
A minimum and maximum child VLAN ID must be set for each group. (These default to 1 and 4094 respectively.) VLANs created within a group must have a VID that falls between these values (inclusive).
|
||||
|
||||
Groups can also be used to enforce uniqueness: Each VLAN within a group must have a unique ID and name. VLANs which are not assigned to a group may have overlapping names and IDs (including VLANs which belong to a common site). For example, you can create two VLANs with ID 123, but they cannot both be assigned to the same group.
|
||||
|
@ -41,6 +41,7 @@ FIELD_CHOICES = {
|
||||
### Enhancements
|
||||
|
||||
* [#7650](https://github.com/netbox-community/netbox/issues/7650) - Add support for local account password validation
|
||||
* [#8168](https://github.com/netbox-community/netbox/issues/8168) - Add `min_vid` and `max_vid` fields to VLAN group
|
||||
|
||||
### Other Changes
|
||||
|
||||
@ -53,3 +54,6 @@ FIELD_CHOICES = {
|
||||
|
||||
* dcim.Site
|
||||
* Removed the `asn`, `contact_name`, `contact_phone`, and `contact_email` fields
|
||||
* ipam.VLANGroup
|
||||
* Added the `/availables-vlans/` endpoint
|
||||
* Added the `min_vid` and `max_vid` fields
|
||||
|
@ -182,8 +182,8 @@ class VLANGroupSerializer(PrimaryModelSerializer):
|
||||
class Meta:
|
||||
model = VLANGroup
|
||||
fields = [
|
||||
'id', 'url', 'display', 'name', 'slug', 'scope_type', 'scope_id', 'scope', 'description', 'tags',
|
||||
'custom_fields', 'created', 'last_updated', 'vlan_count',
|
||||
'id', 'url', 'display', 'name', 'slug', 'scope_type', 'scope_id', 'scope', 'min_vid', 'max_vid',
|
||||
'description', 'tags', 'custom_fields', 'created', 'last_updated', 'vlan_count',
|
||||
]
|
||||
validators = []
|
||||
|
||||
|
@ -740,7 +740,7 @@ class VLANGroupFilterSet(OrganizationalModelFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = VLANGroup
|
||||
fields = ['id', 'name', 'slug', 'description', 'scope_id']
|
||||
fields = ['id', 'name', 'slug', 'min_vid', 'max_vid', 'description', 'scope_id']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
|
@ -359,6 +359,18 @@ class VLANGroupBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||
queryset=Site.objects.all(),
|
||||
required=False
|
||||
)
|
||||
min_vid = forms.IntegerField(
|
||||
min_value=VLAN_VID_MIN,
|
||||
max_value=VLAN_VID_MAX,
|
||||
required=False,
|
||||
label='Minimum child VLAN VID'
|
||||
)
|
||||
max_vid = forms.IntegerField(
|
||||
min_value=VLAN_VID_MIN,
|
||||
max_value=VLAN_VID_MAX,
|
||||
required=False,
|
||||
label='Maximum child VLAN VID'
|
||||
)
|
||||
description = forms.CharField(
|
||||
max_length=200,
|
||||
required=False
|
||||
|
@ -332,10 +332,22 @@ class VLANGroupCSVForm(CustomFieldModelCSVForm):
|
||||
required=False,
|
||||
label='Scope type (app & model)'
|
||||
)
|
||||
min_vid = forms.IntegerField(
|
||||
min_value=VLAN_VID_MIN,
|
||||
max_value=VLAN_VID_MAX,
|
||||
required=False,
|
||||
label=f'Minimum child VLAN VID (default: {VLAN_VID_MIN})'
|
||||
)
|
||||
max_vid = forms.IntegerField(
|
||||
min_value=VLAN_VID_MIN,
|
||||
max_value=VLAN_VID_MAX,
|
||||
required=False,
|
||||
label=f'Maximum child VLAN VID (default: {VLAN_VID_MIN})'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = VLANGroup
|
||||
fields = ('name', 'slug', 'scope_type', 'scope_id', 'description')
|
||||
fields = ('name', 'slug', 'scope_type', 'scope_id', 'min_vid', 'max_vid', 'description')
|
||||
labels = {
|
||||
'scope_id': 'Scope ID',
|
||||
}
|
||||
|
@ -370,7 +370,8 @@ class FHRPGroupFilterForm(CustomFieldModelFilterForm):
|
||||
class VLANGroupFilterForm(CustomFieldModelFilterForm):
|
||||
field_groups = [
|
||||
['q', 'tag'],
|
||||
['region', 'sitegroup', 'site', 'location', 'rack']
|
||||
['region', 'sitegroup', 'site', 'location', 'rack'],
|
||||
['min_vid', 'max_vid'],
|
||||
]
|
||||
model = VLANGroup
|
||||
region = DynamicModelMultipleChoiceField(
|
||||
@ -403,6 +404,14 @@ class VLANGroupFilterForm(CustomFieldModelFilterForm):
|
||||
label=_('Rack'),
|
||||
fetch_trigger='open'
|
||||
)
|
||||
min_vid = forms.IntegerField(
|
||||
min_value=VLAN_VID_MIN,
|
||||
max_value=VLAN_VID_MAX,
|
||||
)
|
||||
max_vid = forms.IntegerField(
|
||||
min_value=VLAN_VID_MIN,
|
||||
max_value=VLAN_VID_MAX,
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
|
@ -700,10 +700,11 @@ class VLANGroupForm(CustomFieldModelForm):
|
||||
model = VLANGroup
|
||||
fields = [
|
||||
'name', 'slug', 'description', 'scope_type', 'region', 'sitegroup', 'site', 'location', 'rack',
|
||||
'clustergroup', 'cluster', 'tags',
|
||||
'clustergroup', 'cluster', 'min_vid', 'max_vid', 'tags',
|
||||
]
|
||||
fieldsets = (
|
||||
('VLAN Group', ('name', 'slug', 'description', 'tags')),
|
||||
('Child VLANs', ('min_vid', 'max_vid')),
|
||||
('Scope', ('scope_type', 'region', 'sitegroup', 'site', 'location', 'rack', 'clustergroup', 'cluster')),
|
||||
)
|
||||
widgets = {
|
||||
|
24
netbox/ipam/migrations/0054_vlangroup_min_max_vids.py
Normal file
24
netbox/ipam/migrations/0054_vlangroup_min_max_vids.py
Normal file
@ -0,0 +1,24 @@
|
||||
# Generated by Django 3.2.10 on 2021-12-23 15:24
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('ipam', '0053_asn_model'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='vlangroup',
|
||||
name='max_vid',
|
||||
field=models.PositiveSmallIntegerField(default=4094, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(4094)]),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='vlangroup',
|
||||
name='min_vid',
|
||||
field=models.PositiveSmallIntegerField(default=1, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(4094)]),
|
||||
),
|
||||
]
|
@ -46,6 +46,24 @@ class VLANGroup(OrganizationalModel):
|
||||
ct_field='scope_type',
|
||||
fk_field='scope_id'
|
||||
)
|
||||
min_vid = models.PositiveSmallIntegerField(
|
||||
verbose_name='Minimum VLAN ID',
|
||||
default=VLAN_VID_MIN,
|
||||
validators=(
|
||||
MinValueValidator(VLAN_VID_MIN),
|
||||
MaxValueValidator(VLAN_VID_MAX)
|
||||
),
|
||||
help_text='Lowest permissible ID of a child VLAN'
|
||||
)
|
||||
max_vid = models.PositiveSmallIntegerField(
|
||||
verbose_name='Maximum VLAN ID',
|
||||
default=VLAN_VID_MAX,
|
||||
validators=(
|
||||
MinValueValidator(VLAN_VID_MIN),
|
||||
MaxValueValidator(VLAN_VID_MAX)
|
||||
),
|
||||
help_text='Highest permissible ID of a child VLAN'
|
||||
)
|
||||
description = models.CharField(
|
||||
max_length=200,
|
||||
blank=True
|
||||
@ -75,24 +93,28 @@ class VLANGroup(OrganizationalModel):
|
||||
if self.scope_id and not self.scope_type:
|
||||
raise ValidationError("Cannot set scope_id without scope_type.")
|
||||
|
||||
# Validate min/max child VID limits
|
||||
if self.max_vid < self.min_vid:
|
||||
raise ValidationError({
|
||||
'max_vid': "Maximum child VID must be greater than or equal to minimum child VID"
|
||||
})
|
||||
|
||||
def get_available_vids(self):
|
||||
"""
|
||||
Return all available VLANs within this group.
|
||||
"""
|
||||
available_vlans = {vid for vid in range(VLAN_VID_MIN, VLAN_VID_MAX + 1)}
|
||||
available_vlans = {vid for vid in range(self.min_vid, self.max_vid + 1)}
|
||||
available_vlans -= set(VLAN.objects.filter(group=self).values_list('vid', flat=True))
|
||||
|
||||
# TODO: Check ordering
|
||||
return list(available_vlans)
|
||||
return sorted(available_vlans)
|
||||
|
||||
def get_next_available_vid(self):
|
||||
"""
|
||||
Return the first available VLAN ID (1-4094) in the group.
|
||||
"""
|
||||
vlan_ids = VLAN.objects.filter(group=self).values_list('vid', flat=True)
|
||||
for i in range(1, 4095):
|
||||
if i not in vlan_ids:
|
||||
return i
|
||||
available_vids = self.get_available_vids()
|
||||
if available_vids:
|
||||
return available_vids[0]
|
||||
return None
|
||||
|
||||
|
||||
@ -122,7 +144,10 @@ class VLAN(PrimaryModel):
|
||||
)
|
||||
vid = models.PositiveSmallIntegerField(
|
||||
verbose_name='ID',
|
||||
validators=[MinValueValidator(1), MaxValueValidator(4094)]
|
||||
validators=(
|
||||
MinValueValidator(VLAN_VID_MIN),
|
||||
MaxValueValidator(VLAN_VID_MAX)
|
||||
)
|
||||
)
|
||||
name = models.CharField(
|
||||
max_length=64
|
||||
@ -182,6 +207,13 @@ class VLAN(PrimaryModel):
|
||||
f"site {self.site}."
|
||||
})
|
||||
|
||||
# Validate group min/max VIDs
|
||||
if self.group and not self.group.min_vid <= self.vid <= self.group.max_vid:
|
||||
raise ValidationError({
|
||||
'vid': f"VID must be between {self.group.min_vid} and {self.group.max_vid} for VLANs in group "
|
||||
f"{self.group}"
|
||||
})
|
||||
|
||||
def get_status_class(self):
|
||||
return VLANStatusChoices.colors.get(self.status, 'secondary')
|
||||
|
||||
|
@ -84,7 +84,10 @@ class VLANGroupTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = VLANGroup
|
||||
fields = ('pk', 'id', 'name', 'scope_type', 'scope', 'vlan_count', 'slug', 'description', 'tags', 'actions')
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'scope_type', 'scope', 'min_vid', 'max_vid', 'vlan_count', 'slug', 'description',
|
||||
'tags', 'actions',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'scope_type', 'scope', 'vlan_count', 'description', 'actions')
|
||||
|
||||
|
||||
|
@ -497,18 +497,32 @@ class TestIPAddress(TestCase):
|
||||
|
||||
class TestVLANGroup(TestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
vlangroup = VLANGroup.objects.create(
|
||||
name='VLAN Group 1',
|
||||
slug='vlan-group-1',
|
||||
min_vid=100,
|
||||
max_vid=199
|
||||
)
|
||||
VLAN.objects.bulk_create((
|
||||
VLAN(name='VLAN 100', vid=100, group=vlangroup),
|
||||
VLAN(name='VLAN 101', vid=101, group=vlangroup),
|
||||
VLAN(name='VLAN 102', vid=102, group=vlangroup),
|
||||
VLAN(name='VLAN 103', vid=103, group=vlangroup),
|
||||
))
|
||||
|
||||
def test_get_available_vids(self):
|
||||
vlangroup = VLANGroup.objects.first()
|
||||
child_vids = VLAN.objects.filter(group=vlangroup).values_list('vid', flat=True)
|
||||
self.assertEqual(len(child_vids), 4)
|
||||
|
||||
available_vids = vlangroup.get_available_vids()
|
||||
self.assertListEqual(available_vids, list(range(104, 200)))
|
||||
|
||||
def test_get_next_available_vid(self):
|
||||
vlangroup = VLANGroup.objects.first()
|
||||
self.assertEqual(vlangroup.get_next_available_vid(), 104)
|
||||
|
||||
vlangroup = VLANGroup.objects.create(name='VLAN Group 1', slug='vlan-group-1')
|
||||
VLAN.objects.bulk_create((
|
||||
VLAN(name='VLAN 1', vid=1, group=vlangroup),
|
||||
VLAN(name='VLAN 2', vid=2, group=vlangroup),
|
||||
VLAN(name='VLAN 3', vid=3, group=vlangroup),
|
||||
VLAN(name='VLAN 5', vid=5, group=vlangroup),
|
||||
))
|
||||
self.assertEqual(vlangroup.get_next_available_vid(), 4)
|
||||
|
||||
VLAN.objects.bulk_create((
|
||||
VLAN(name='VLAN 4', vid=4, group=vlangroup),
|
||||
))
|
||||
self.assertEqual(vlangroup.get_next_available_vid(), 6)
|
||||
VLAN.objects.create(name='VLAN 104', vid=104, group=vlangroup)
|
||||
self.assertEqual(vlangroup.get_next_available_vid(), 105)
|
||||
|
@ -485,6 +485,8 @@ class VLANGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
||||
cls.form_data = {
|
||||
'name': 'VLAN Group X',
|
||||
'slug': 'vlan-group-x',
|
||||
'min_vid': 1,
|
||||
'max_vid': 4094,
|
||||
'description': 'A new VLAN group',
|
||||
'tags': [t.pk for t in tags],
|
||||
}
|
||||
|
@ -23,9 +23,7 @@
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-6">
|
||||
<div class="card">
|
||||
<h5 class="card-header">
|
||||
VLAN Group
|
||||
</h5>
|
||||
<h5 class="card-header">VLAN Group</h5>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
@ -45,6 +43,10 @@
|
||||
<span class="text-muted">—</span>
|
||||
{% endif %}
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Permitted VIDs</th>
|
||||
<td>{{ object.min_vid }} - {{ object.max_vid }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">VLANs</th>
|
||||
<td>
|
||||
|
Loading…
Reference in New Issue
Block a user