Skip to content

Commit

Permalink
Merge pull request #908 from digitalocean/global-vlans
Browse files Browse the repository at this point in the history
Closes #235: Global vlans
  • Loading branch information
jeremystretch authored Feb 21, 2017
2 parents 2876ef7 + b0f9035 commit c61bae3
Show file tree
Hide file tree
Showing 6 changed files with 65 additions and 22 deletions.
6 changes: 4 additions & 2 deletions docs/data-model/ipam.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,11 @@ One IP address can be designated as the network address translation (NAT) IP add

# 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.

---

Expand Down
12 changes: 6 additions & 6 deletions netbox/ipam/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -262,13 +262,13 @@ def search_by_parent(self, queryset, value):


class VLANGroupFilter(django_filters.FilterSet):
site_id = django_filters.ModelMultipleChoiceFilter(
site_id = NullableModelMultipleChoiceFilter(
name='site',
queryset=Site.objects.all(),
label='Site (ID)',
)
site = django_filters.ModelMultipleChoiceFilter(
name='site__slug',
site = NullableModelMultipleChoiceFilter(
name='site',
queryset=Site.objects.all(),
to_field_name='slug',
label='Site (slug)',
Expand All @@ -283,13 +283,13 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
action='search',
label='Search',
)
site_id = django_filters.ModelMultipleChoiceFilter(
site_id = NullableModelMultipleChoiceFilter(
name='site',
queryset=Site.objects.all(),
label='Site (ID)',
)
site = django_filters.ModelMultipleChoiceFilter(
name='site__slug',
site = NullableModelMultipleChoiceFilter(
name='site',
queryset=Site.objects.all(),
to_field_name='slug',
label='Site (slug)',
Expand Down
21 changes: 13 additions & 8 deletions netbox/ipam/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ class Meta:

class PrefixForm(BootstrapMixin, CustomFieldForm):
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',
widget=APISelect(api_url='/api/ipam/vlans/?site_id={{site}}',
display_field='display_name'))
Expand All @@ -173,7 +173,7 @@ def __init__(self, *args, **kwargs):
elif self.initial.get('site'):
self.fields['vlan'].queryset = VLAN.objects.filter(site=self.initial['site'])
else:
self.fields['vlan'].choices = []
self.fields['vlan'].queryset = VLAN.objects.filter(site=None)


class PrefixFromCSVForm(forms.ModelForm):
Expand Down Expand Up @@ -508,7 +508,11 @@ class Meta:


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')
)


#
Expand All @@ -524,15 +528,15 @@ class Meta:
model = VLAN
fields = ['site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description']
help_texts = {
'site': "The site at which this VLAN exists",
'site': "Leave blank if this VLAN spans multiple sites",
'group': "VLAN group (optional)",
'vid': "Configured VLAN ID",
'name': "Configured VLAN name",
'status': "Operational status of this VLAN",
'role': "The primary function of this VLAN",
}
widgets = {
'site': forms.Select(attrs={'filter-for': 'group'}),
'site': forms.Select(attrs={'filter-for': 'group', 'nullable': 'true'}),
}

def __init__(self, *args, **kwargs):
Expand All @@ -545,11 +549,11 @@ def __init__(self, *args, **kwargs):
elif self.initial.get('site'):
self.fields['group'].queryset = VLANGroup.objects.filter(site=self.initial['site'])
else:
self.fields['group'].choices = []
self.fields['group'].queryset = VLANGroup.objects.filter(site=None)


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.'})
group = forms.ModelChoiceField(queryset=VLANGroup.objects.all(), required=False, to_field_name='name',
error_messages={'invalid_choice': 'VLAN group not found.'})
Expand Down Expand Up @@ -599,7 +603,8 @@ def vlan_status_choices():
class VLANFilterForm(BootstrapMixin, CustomFieldFilterForm):
model = VLAN
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',
null_option=(0, 'None'))
tenant = FilterChoiceField(queryset=Tenant.objects.annotate(filter_count=Count('vlans')), to_field_name='slug',
Expand Down
26 changes: 26 additions & 0 deletions netbox/ipam/migrations/0015_global_vlans.py
Original file line number Diff line number Diff line change
@@ -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'),
),
]
8 changes: 5 additions & 3 deletions netbox/ipam/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -485,7 +485,7 @@ class VLANGroup(models.Model):
"""
name = models.CharField(max_length=50)
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:
ordering = ['site', 'name']
Expand All @@ -497,6 +497,8 @@ class Meta:
verbose_name_plural = 'VLAN groups'

def __str__(self):
if self.site is None:
return self.name
return u'{} - {}'.format(self.site.name, self.name)

def get_absolute_url(self):
Expand All @@ -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
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)
vid = models.PositiveSmallIntegerField(verbose_name='ID', validators=[
MinValueValidator(1),
Expand Down Expand Up @@ -551,7 +553,7 @@ def clean(self):

def to_csv(self):
return csv_format([
self.site.name,
self.site.name if self.site else None,
self.group.name if self.group else None,
self.vid,
self.name,
Expand Down
14 changes: 11 additions & 3 deletions netbox/templates/ipam/vlan.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@
<div class="col-sm-8 col-md-9">
<ol class="breadcrumb">
<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 %}
<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 %}
<li>{{ vlan.name }} ({{ vlan.vid }})</li>
</ol>
Expand Down Expand Up @@ -53,7 +55,13 @@ <h1>VLAN {{ vlan.display_name }}</h1>
<table class="table table-hover panel-body attr-table">
<tr>
<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>
<td>Group</td>
Expand Down

1 comment on commit c61bae3

@NicolasDEVOUGE
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello , I have just updated to test this commit, and I have a question :
If I have 1 vlan, with no site, and I want to add 3 prefix on this VLAN, it's OK, unless those prefix are attached to a site. My example is : I have a VOIP vlan, accross 3 sites, and I have 3 prefix /26 for these sites. If I wan to attach one of these prefix to my VLAN, I can't. Is it normal ?

Please sign in to comment.