mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-14 01:41: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.
|
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.
|
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
|
### Enhancements
|
||||||
|
|
||||||
* [#7650](https://github.com/netbox-community/netbox/issues/7650) - Add support for local account password validation
|
* [#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
|
### Other Changes
|
||||||
|
|
||||||
@ -53,3 +54,6 @@ FIELD_CHOICES = {
|
|||||||
|
|
||||||
* dcim.Site
|
* dcim.Site
|
||||||
* Removed the `asn`, `contact_name`, `contact_phone`, and `contact_email` fields
|
* 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:
|
class Meta:
|
||||||
model = VLANGroup
|
model = VLANGroup
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'name', 'slug', 'scope_type', 'scope_id', 'scope', 'description', 'tags',
|
'id', 'url', 'display', 'name', 'slug', 'scope_type', 'scope_id', 'scope', 'min_vid', 'max_vid',
|
||||||
'custom_fields', 'created', 'last_updated', 'vlan_count',
|
'description', 'tags', 'custom_fields', 'created', 'last_updated', 'vlan_count',
|
||||||
]
|
]
|
||||||
validators = []
|
validators = []
|
||||||
|
|
||||||
|
@ -740,7 +740,7 @@ class VLANGroupFilterSet(OrganizationalModelFilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = VLANGroup
|
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):
|
def search(self, queryset, name, value):
|
||||||
if not value.strip():
|
if not value.strip():
|
||||||
|
@ -359,6 +359,18 @@ class VLANGroupBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
|||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
required=False
|
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(
|
description = forms.CharField(
|
||||||
max_length=200,
|
max_length=200,
|
||||||
required=False
|
required=False
|
||||||
|
@ -332,10 +332,22 @@ class VLANGroupCSVForm(CustomFieldModelCSVForm):
|
|||||||
required=False,
|
required=False,
|
||||||
label='Scope type (app & model)'
|
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:
|
class Meta:
|
||||||
model = VLANGroup
|
model = VLANGroup
|
||||||
fields = ('name', 'slug', 'scope_type', 'scope_id', 'description')
|
fields = ('name', 'slug', 'scope_type', 'scope_id', 'min_vid', 'max_vid', 'description')
|
||||||
labels = {
|
labels = {
|
||||||
'scope_id': 'Scope ID',
|
'scope_id': 'Scope ID',
|
||||||
}
|
}
|
||||||
|
@ -370,7 +370,8 @@ class FHRPGroupFilterForm(CustomFieldModelFilterForm):
|
|||||||
class VLANGroupFilterForm(CustomFieldModelFilterForm):
|
class VLANGroupFilterForm(CustomFieldModelFilterForm):
|
||||||
field_groups = [
|
field_groups = [
|
||||||
['q', 'tag'],
|
['q', 'tag'],
|
||||||
['region', 'sitegroup', 'site', 'location', 'rack']
|
['region', 'sitegroup', 'site', 'location', 'rack'],
|
||||||
|
['min_vid', 'max_vid'],
|
||||||
]
|
]
|
||||||
model = VLANGroup
|
model = VLANGroup
|
||||||
region = DynamicModelMultipleChoiceField(
|
region = DynamicModelMultipleChoiceField(
|
||||||
@ -403,6 +404,14 @@ class VLANGroupFilterForm(CustomFieldModelFilterForm):
|
|||||||
label=_('Rack'),
|
label=_('Rack'),
|
||||||
fetch_trigger='open'
|
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)
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
|
|
||||||
|
@ -700,10 +700,11 @@ class VLANGroupForm(CustomFieldModelForm):
|
|||||||
model = VLANGroup
|
model = VLANGroup
|
||||||
fields = [
|
fields = [
|
||||||
'name', 'slug', 'description', 'scope_type', 'region', 'sitegroup', 'site', 'location', 'rack',
|
'name', 'slug', 'description', 'scope_type', 'region', 'sitegroup', 'site', 'location', 'rack',
|
||||||
'clustergroup', 'cluster', 'tags',
|
'clustergroup', 'cluster', 'min_vid', 'max_vid', 'tags',
|
||||||
]
|
]
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
('VLAN Group', ('name', 'slug', 'description', 'tags')),
|
('VLAN Group', ('name', 'slug', 'description', 'tags')),
|
||||||
|
('Child VLANs', ('min_vid', 'max_vid')),
|
||||||
('Scope', ('scope_type', 'region', 'sitegroup', 'site', 'location', 'rack', 'clustergroup', 'cluster')),
|
('Scope', ('scope_type', 'region', 'sitegroup', 'site', 'location', 'rack', 'clustergroup', 'cluster')),
|
||||||
)
|
)
|
||||||
widgets = {
|
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',
|
ct_field='scope_type',
|
||||||
fk_field='scope_id'
|
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(
|
description = models.CharField(
|
||||||
max_length=200,
|
max_length=200,
|
||||||
blank=True
|
blank=True
|
||||||
@ -75,24 +93,28 @@ class VLANGroup(OrganizationalModel):
|
|||||||
if self.scope_id and not self.scope_type:
|
if self.scope_id and not self.scope_type:
|
||||||
raise ValidationError("Cannot set scope_id without 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):
|
def get_available_vids(self):
|
||||||
"""
|
"""
|
||||||
Return all available VLANs within this group.
|
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))
|
available_vlans -= set(VLAN.objects.filter(group=self).values_list('vid', flat=True))
|
||||||
|
|
||||||
# TODO: Check ordering
|
return sorted(available_vlans)
|
||||||
return list(available_vlans)
|
|
||||||
|
|
||||||
def get_next_available_vid(self):
|
def get_next_available_vid(self):
|
||||||
"""
|
"""
|
||||||
Return the first available VLAN ID (1-4094) in the group.
|
Return the first available VLAN ID (1-4094) in the group.
|
||||||
"""
|
"""
|
||||||
vlan_ids = VLAN.objects.filter(group=self).values_list('vid', flat=True)
|
available_vids = self.get_available_vids()
|
||||||
for i in range(1, 4095):
|
if available_vids:
|
||||||
if i not in vlan_ids:
|
return available_vids[0]
|
||||||
return i
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@ -122,7 +144,10 @@ class VLAN(PrimaryModel):
|
|||||||
)
|
)
|
||||||
vid = models.PositiveSmallIntegerField(
|
vid = models.PositiveSmallIntegerField(
|
||||||
verbose_name='ID',
|
verbose_name='ID',
|
||||||
validators=[MinValueValidator(1), MaxValueValidator(4094)]
|
validators=(
|
||||||
|
MinValueValidator(VLAN_VID_MIN),
|
||||||
|
MaxValueValidator(VLAN_VID_MAX)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
name = models.CharField(
|
name = models.CharField(
|
||||||
max_length=64
|
max_length=64
|
||||||
@ -182,6 +207,13 @@ class VLAN(PrimaryModel):
|
|||||||
f"site {self.site}."
|
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):
|
def get_status_class(self):
|
||||||
return VLANStatusChoices.colors.get(self.status, 'secondary')
|
return VLANStatusChoices.colors.get(self.status, 'secondary')
|
||||||
|
|
||||||
|
@ -84,7 +84,10 @@ class VLANGroupTable(BaseTable):
|
|||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = VLANGroup
|
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')
|
default_columns = ('pk', 'name', 'scope_type', 'scope', 'vlan_count', 'description', 'actions')
|
||||||
|
|
||||||
|
|
||||||
|
@ -497,18 +497,32 @@ class TestIPAddress(TestCase):
|
|||||||
|
|
||||||
class TestVLANGroup(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):
|
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.create(name='VLAN 104', vid=104, group=vlangroup)
|
||||||
VLAN.objects.bulk_create((
|
self.assertEqual(vlangroup.get_next_available_vid(), 105)
|
||||||
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)
|
|
||||||
|
@ -485,6 +485,8 @@ class VLANGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
|||||||
cls.form_data = {
|
cls.form_data = {
|
||||||
'name': 'VLAN Group X',
|
'name': 'VLAN Group X',
|
||||||
'slug': 'vlan-group-x',
|
'slug': 'vlan-group-x',
|
||||||
|
'min_vid': 1,
|
||||||
|
'max_vid': 4094,
|
||||||
'description': 'A new VLAN group',
|
'description': 'A new VLAN group',
|
||||||
'tags': [t.pk for t in tags],
|
'tags': [t.pk for t in tags],
|
||||||
}
|
}
|
||||||
|
@ -23,9 +23,7 @@
|
|||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="col col-md-6">
|
<div class="col col-md-6">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h5 class="card-header">
|
<h5 class="card-header">VLAN Group</h5>
|
||||||
VLAN Group
|
|
||||||
</h5>
|
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<table class="table table-hover attr-table">
|
<table class="table table-hover attr-table">
|
||||||
<tr>
|
<tr>
|
||||||
@ -45,6 +43,10 @@
|
|||||||
<span class="text-muted">—</span>
|
<span class="text-muted">—</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Permitted VIDs</th>
|
||||||
|
<td>{{ object.min_vid }} - {{ object.max_vid }}</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">VLANs</th>
|
<th scope="row">VLANs</th>
|
||||||
<td>
|
<td>
|
||||||
|
Loading…
Reference in New Issue
Block a user