mirror of
https://github.com/netbox-community/netbox.git
synced 2024-05-10 07:54:54 +00:00
Merge pull request #908 from digitalocean/global-vlans
Closes #235: Global vlans
This commit is contained in:
@ -83,9 +83,11 @@ One IP address can be designated as the network address translation (NAT) IP add
|
|||||||
|
|
||||||
# VLANs
|
# VLANs
|
||||||
|
|
||||||
A VLAN represents an isolated layer two domain, identified by a name and a numeric ID (1-4094). Note that while it is good practice, neither VLAN names nor IDs must be unique within a site. This is to accommodate the fact that many real-world network use less-than-optimal VLAN allocations and may have overlapping VLAN ID assignments in practice.
|
A VLAN represents an isolated layer two domain, identified by a name and a numeric ID (1-4094) as defined in [IEEE 802.1Q](https://en.wikipedia.org/wiki/IEEE_802.1Q). VLANs may be assigned to a site and/or VLAN group. Like prefixes, each VLAN is assigned an operational status and (optionally) a functional role.
|
||||||
|
|
||||||
Like prefixes, each VLAN is assigned an operational status and (optionally) a functional role.
|
### VLAN Groups
|
||||||
|
|
||||||
|
VLAN groups can be employed for administrative organization within NetBox. 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 within a site.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -262,13 +262,13 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
|
|
||||||
|
|
||||||
class VLANGroupFilter(django_filters.FilterSet):
|
class VLANGroupFilter(django_filters.FilterSet):
|
||||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
site_id = NullableModelMultipleChoiceFilter(
|
||||||
name='site',
|
name='site',
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
label='Site (ID)',
|
label='Site (ID)',
|
||||||
)
|
)
|
||||||
site = django_filters.ModelMultipleChoiceFilter(
|
site = NullableModelMultipleChoiceFilter(
|
||||||
name='site__slug',
|
name='site',
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Site (slug)',
|
label='Site (slug)',
|
||||||
@ -283,13 +283,13 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
action='search',
|
action='search',
|
||||||
label='Search',
|
label='Search',
|
||||||
)
|
)
|
||||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
site_id = NullableModelMultipleChoiceFilter(
|
||||||
name='site',
|
name='site',
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
label='Site (ID)',
|
label='Site (ID)',
|
||||||
)
|
)
|
||||||
site = django_filters.ModelMultipleChoiceFilter(
|
site = NullableModelMultipleChoiceFilter(
|
||||||
name='site__slug',
|
name='site',
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Site (slug)',
|
label='Site (slug)',
|
||||||
|
@ -153,7 +153,7 @@ class RoleForm(BootstrapMixin, forms.ModelForm):
|
|||||||
|
|
||||||
class PrefixForm(BootstrapMixin, CustomFieldForm):
|
class PrefixForm(BootstrapMixin, CustomFieldForm):
|
||||||
site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False, label='Site',
|
site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False, label='Site',
|
||||||
widget=forms.Select(attrs={'filter-for': 'vlan'}))
|
widget=forms.Select(attrs={'filter-for': 'vlan', 'nullable': 'true'}))
|
||||||
vlan = forms.ModelChoiceField(queryset=VLAN.objects.all(), required=False, label='VLAN',
|
vlan = forms.ModelChoiceField(queryset=VLAN.objects.all(), required=False, label='VLAN',
|
||||||
widget=APISelect(api_url='/api/ipam/vlans/?site_id={{site}}',
|
widget=APISelect(api_url='/api/ipam/vlans/?site_id={{site}}',
|
||||||
display_field='display_name'))
|
display_field='display_name'))
|
||||||
@ -173,7 +173,7 @@ class PrefixForm(BootstrapMixin, CustomFieldForm):
|
|||||||
elif self.initial.get('site'):
|
elif self.initial.get('site'):
|
||||||
self.fields['vlan'].queryset = VLAN.objects.filter(site=self.initial['site'])
|
self.fields['vlan'].queryset = VLAN.objects.filter(site=self.initial['site'])
|
||||||
else:
|
else:
|
||||||
self.fields['vlan'].choices = []
|
self.fields['vlan'].queryset = VLAN.objects.filter(site=None)
|
||||||
|
|
||||||
|
|
||||||
class PrefixFromCSVForm(forms.ModelForm):
|
class PrefixFromCSVForm(forms.ModelForm):
|
||||||
@ -508,7 +508,11 @@ class VLANGroupForm(BootstrapMixin, forms.ModelForm):
|
|||||||
|
|
||||||
|
|
||||||
class VLANGroupFilterForm(BootstrapMixin, forms.Form):
|
class VLANGroupFilterForm(BootstrapMixin, forms.Form):
|
||||||
site = FilterChoiceField(queryset=Site.objects.annotate(filter_count=Count('vlan_groups')), to_field_name='slug')
|
site = FilterChoiceField(
|
||||||
|
queryset=Site.objects.annotate(filter_count=Count('vlan_groups')),
|
||||||
|
to_field_name='slug',
|
||||||
|
null_option=(0, 'Global')
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -524,7 +528,7 @@ class VLANForm(BootstrapMixin, CustomFieldForm):
|
|||||||
model = VLAN
|
model = VLAN
|
||||||
fields = ['site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description']
|
fields = ['site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description']
|
||||||
help_texts = {
|
help_texts = {
|
||||||
'site': "The site at which this VLAN exists",
|
'site': "Leave blank if this VLAN spans multiple sites",
|
||||||
'group': "VLAN group (optional)",
|
'group': "VLAN group (optional)",
|
||||||
'vid': "Configured VLAN ID",
|
'vid': "Configured VLAN ID",
|
||||||
'name': "Configured VLAN name",
|
'name': "Configured VLAN name",
|
||||||
@ -532,7 +536,7 @@ class VLANForm(BootstrapMixin, CustomFieldForm):
|
|||||||
'role': "The primary function of this VLAN",
|
'role': "The primary function of this VLAN",
|
||||||
}
|
}
|
||||||
widgets = {
|
widgets = {
|
||||||
'site': forms.Select(attrs={'filter-for': 'group'}),
|
'site': forms.Select(attrs={'filter-for': 'group', 'nullable': 'true'}),
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@ -545,11 +549,11 @@ class VLANForm(BootstrapMixin, CustomFieldForm):
|
|||||||
elif self.initial.get('site'):
|
elif self.initial.get('site'):
|
||||||
self.fields['group'].queryset = VLANGroup.objects.filter(site=self.initial['site'])
|
self.fields['group'].queryset = VLANGroup.objects.filter(site=self.initial['site'])
|
||||||
else:
|
else:
|
||||||
self.fields['group'].choices = []
|
self.fields['group'].queryset = VLANGroup.objects.filter(site=None)
|
||||||
|
|
||||||
|
|
||||||
class VLANFromCSVForm(forms.ModelForm):
|
class VLANFromCSVForm(forms.ModelForm):
|
||||||
site = forms.ModelChoiceField(queryset=Site.objects.all(), to_field_name='name',
|
site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False, to_field_name='name',
|
||||||
error_messages={'invalid_choice': 'Site not found.'})
|
error_messages={'invalid_choice': 'Site not found.'})
|
||||||
group = forms.ModelChoiceField(queryset=VLANGroup.objects.all(), required=False, to_field_name='name',
|
group = forms.ModelChoiceField(queryset=VLANGroup.objects.all(), required=False, to_field_name='name',
|
||||||
error_messages={'invalid_choice': 'VLAN group not found.'})
|
error_messages={'invalid_choice': 'VLAN group not found.'})
|
||||||
@ -599,7 +603,8 @@ def vlan_status_choices():
|
|||||||
class VLANFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
class VLANFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||||
model = VLAN
|
model = VLAN
|
||||||
q = forms.CharField(required=False, label='Search')
|
q = forms.CharField(required=False, label='Search')
|
||||||
site = FilterChoiceField(queryset=Site.objects.annotate(filter_count=Count('vlans')), to_field_name='slug')
|
site = FilterChoiceField(queryset=Site.objects.annotate(filter_count=Count('vlans')), to_field_name='slug',
|
||||||
|
null_option=(0, 'Global'))
|
||||||
group_id = FilterChoiceField(queryset=VLANGroup.objects.annotate(filter_count=Count('vlans')), label='VLAN group',
|
group_id = FilterChoiceField(queryset=VLANGroup.objects.annotate(filter_count=Count('vlans')), label='VLAN group',
|
||||||
null_option=(0, 'None'))
|
null_option=(0, 'None'))
|
||||||
tenant = FilterChoiceField(queryset=Tenant.objects.annotate(filter_count=Count('vlans')), to_field_name='slug',
|
tenant = FilterChoiceField(queryset=Tenant.objects.annotate(filter_count=Count('vlans')), to_field_name='slug',
|
||||||
|
26
netbox/ipam/migrations/0015_global_vlans.py
Normal file
26
netbox/ipam/migrations/0015_global_vlans.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.4 on 2017-02-21 18:45
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('ipam', '0014_ipaddress_status_add_deprecated'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='vlan',
|
||||||
|
name='site',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='vlans', to='dcim.Site'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='vlangroup',
|
||||||
|
name='site',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='vlan_groups', to='dcim.Site'),
|
||||||
|
),
|
||||||
|
]
|
@ -485,7 +485,7 @@ class VLANGroup(models.Model):
|
|||||||
"""
|
"""
|
||||||
name = models.CharField(max_length=50)
|
name = models.CharField(max_length=50)
|
||||||
slug = models.SlugField()
|
slug = models.SlugField()
|
||||||
site = models.ForeignKey('dcim.Site', related_name='vlan_groups')
|
site = models.ForeignKey('dcim.Site', related_name='vlan_groups', on_delete=models.PROTECT, blank=True, null=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['site', 'name']
|
ordering = ['site', 'name']
|
||||||
@ -497,6 +497,8 @@ class VLANGroup(models.Model):
|
|||||||
verbose_name_plural = 'VLAN groups'
|
verbose_name_plural = 'VLAN groups'
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
if self.site is None:
|
||||||
|
return self.name
|
||||||
return u'{} - {}'.format(self.site.name, self.name)
|
return u'{} - {}'.format(self.site.name, self.name)
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
@ -513,7 +515,7 @@ class VLAN(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
Like Prefixes, each VLAN is assigned an operational status and optionally a user-defined Role. A VLAN can have zero
|
Like Prefixes, each VLAN is assigned an operational status and optionally a user-defined Role. A VLAN can have zero
|
||||||
or more Prefixes assigned to it.
|
or more Prefixes assigned to it.
|
||||||
"""
|
"""
|
||||||
site = models.ForeignKey('dcim.Site', related_name='vlans', on_delete=models.PROTECT)
|
site = models.ForeignKey('dcim.Site', related_name='vlans', on_delete=models.PROTECT, blank=True, null=True)
|
||||||
group = models.ForeignKey('VLANGroup', related_name='vlans', blank=True, null=True, on_delete=models.PROTECT)
|
group = models.ForeignKey('VLANGroup', related_name='vlans', blank=True, null=True, on_delete=models.PROTECT)
|
||||||
vid = models.PositiveSmallIntegerField(verbose_name='ID', validators=[
|
vid = models.PositiveSmallIntegerField(verbose_name='ID', validators=[
|
||||||
MinValueValidator(1),
|
MinValueValidator(1),
|
||||||
@ -551,7 +553,7 @@ class VLAN(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
|
|
||||||
def to_csv(self):
|
def to_csv(self):
|
||||||
return csv_format([
|
return csv_format([
|
||||||
self.site.name,
|
self.site.name if self.site else None,
|
||||||
self.group.name if self.group else None,
|
self.group.name if self.group else None,
|
||||||
self.vid,
|
self.vid,
|
||||||
self.name,
|
self.name,
|
||||||
|
@ -8,9 +8,11 @@
|
|||||||
<div class="col-sm-8 col-md-9">
|
<div class="col-sm-8 col-md-9">
|
||||||
<ol class="breadcrumb">
|
<ol class="breadcrumb">
|
||||||
<li><a href="{% url 'ipam:vlan_list' %}">VLANs</a></li>
|
<li><a href="{% url 'ipam:vlan_list' %}">VLANs</a></li>
|
||||||
<li><a href="{% url 'ipam:vlan_list' %}?site={{ vlan.site.slug }}">{{ vlan.site }}</a></li>
|
{% if vlan.site %}
|
||||||
|
<li><a href="{% url 'ipam:vlan_list' %}?site={{ vlan.site.slug }}">{{ vlan.site }}</a></li>
|
||||||
|
{% endif %}
|
||||||
{% if vlan.group %}
|
{% if vlan.group %}
|
||||||
<li><a href="{% url 'ipam:vlan_list' %}?site={{ vlan.site.slug }}&group={{ vlan.group.slug }}">{{ vlan.group.name }}</a></li>
|
<li><a href="{% url 'ipam:vlan_list' %}?group={{ vlan.group.slug }}">{{ vlan.group.name }}</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li>{{ vlan.name }} ({{ vlan.vid }})</li>
|
<li>{{ vlan.name }} ({{ vlan.vid }})</li>
|
||||||
</ol>
|
</ol>
|
||||||
@ -53,7 +55,13 @@
|
|||||||
<table class="table table-hover panel-body attr-table">
|
<table class="table table-hover panel-body attr-table">
|
||||||
<tr>
|
<tr>
|
||||||
<td>Site</td>
|
<td>Site</td>
|
||||||
<td><a href="{% url 'dcim:site' slug=vlan.site.slug %}">{{ vlan.site }}</a></td>
|
<td>
|
||||||
|
{% if vlan.site %}
|
||||||
|
<a href="{% url 'dcim:site' slug=vlan.site.slug %}">{{ vlan.site }}</a>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">None</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Group</td>
|
<td>Group</td>
|
||||||
|
Reference in New Issue
Block a user