1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

Merge pull request #309 from digitalocean/vlan-groups

Closes #111: Implement VLAN groups
This commit is contained in:
Jeremy Stretch
2016-07-15 13:36:32 -04:00
committed by GitHub
14 changed files with 377 additions and 29 deletions

View File

@ -1,7 +1,7 @@
from django.contrib import admin from django.contrib import admin
from .models import ( from .models import (
Aggregate, IPAddress, Prefix, RIR, Role, VLAN, VRF, Aggregate, IPAddress, Prefix, RIR, Role, VLAN, VLANGroup, VRF,
) )
@ -57,6 +57,14 @@ class IPAddressAdmin(admin.ModelAdmin):
return qs.select_related('vrf', 'nat_inside') return qs.select_related('vrf', 'nat_inside')
@admin.register(VLANGroup)
class VLANGroupAdmin(admin.ModelAdmin):
list_display = ['name', 'site', 'slug']
prepopulated_fields = {
'slug': ['name'],
}
@admin.register(VLAN) @admin.register(VLAN)
class VLANAdmin(admin.ModelAdmin): class VLANAdmin(admin.ModelAdmin):
list_display = ['site', 'vid', 'name', 'status', 'role'] list_display = ['site', 'vid', 'name', 'status', 'role']

View File

@ -1,7 +1,7 @@
from rest_framework import serializers from rest_framework import serializers
from dcim.api.serializers import SiteNestedSerializer, InterfaceNestedSerializer from dcim.api.serializers import SiteNestedSerializer, InterfaceNestedSerializer
from ipam.models import VRF, Role, RIR, Aggregate, Prefix, IPAddress, VLAN from ipam.models import VRF, Role, RIR, Aggregate, Prefix, IPAddress, VLAN, VLANGroup
# #
@ -73,17 +73,36 @@ class AggregateNestedSerializer(AggregateSerializer):
fields = ['id', 'family', 'prefix'] fields = ['id', 'family', 'prefix']
#
# VLAN groups
#
class VLANGroupSerializer(serializers.ModelSerializer):
site = SiteNestedSerializer()
class Meta:
model = VLANGroup
fields = ['id', 'name', 'slug', 'site']
class VLANGroupNestedSerializer(VLANGroupSerializer):
class Meta(VLANGroupSerializer.Meta):
fields = ['id', 'name', 'slug']
# #
# VLANs # VLANs
# #
class VLANSerializer(serializers.ModelSerializer): class VLANSerializer(serializers.ModelSerializer):
site = SiteNestedSerializer() site = SiteNestedSerializer()
group = VLANGroupNestedSerializer()
role = RoleNestedSerializer() role = RoleNestedSerializer()
class Meta: class Meta:
model = VLAN model = VLAN
fields = ['id', 'site', 'vid', 'name', 'status', 'role', 'display_name'] fields = ['id', 'site', 'group', 'vid', 'name', 'status', 'role', 'display_name']
class VLANNestedSerializer(VLANSerializer): class VLANNestedSerializer(VLANSerializer):

View File

@ -29,6 +29,10 @@ urlpatterns = [
url(r'^ip-addresses/$', IPAddressListView.as_view(), name='ipaddress_list'), url(r'^ip-addresses/$', IPAddressListView.as_view(), name='ipaddress_list'),
url(r'^ip-addresses/(?P<pk>\d+)/$', IPAddressDetailView.as_view(), name='ipaddress_detail'), url(r'^ip-addresses/(?P<pk>\d+)/$', IPAddressDetailView.as_view(), name='ipaddress_detail'),
# VLAN groups
url(r'^vlan-groups/$', VLANGroupListView.as_view(), name='vlangroup_list'),
url(r'^vlan-groups/(?P<pk>\d+)/$', VLANGroupDetailView.as_view(), name='vlangroup_detail'),
# VLANs # VLANs
url(r'^vlans/$', VLANListView.as_view(), name='vlan_list'), url(r'^vlans/$', VLANListView.as_view(), name='vlan_list'),
url(r'^vlans/(?P<pk>\d+)/$', VLANDetailView.as_view(), name='vlan_detail'), url(r'^vlans/(?P<pk>\d+)/$', VLANDetailView.as_view(), name='vlan_detail'),

View File

@ -1,18 +1,22 @@
from rest_framework import generics from rest_framework import generics
from ipam.models import VRF, Role, RIR, Aggregate, Prefix, IPAddress, VLAN from ipam.models import VRF, Role, RIR, Aggregate, Prefix, IPAddress, VLAN, VLANGroup
from ipam.filters import AggregateFilter, PrefixFilter, IPAddressFilter, VLANFilter, VRFFilter from ipam import filters
from . import serializers from . import serializers
#
# VRFs
#
class VRFListView(generics.ListAPIView): class VRFListView(generics.ListAPIView):
""" """
List all VRFs List all VRFs
""" """
queryset = VRF.objects.all() queryset = VRF.objects.all()
serializer_class = serializers.VRFSerializer serializer_class = serializers.VRFSerializer
filter_class = VRFFilter filter_class = filters.VRFFilter
class VRFDetailView(generics.RetrieveAPIView): class VRFDetailView(generics.RetrieveAPIView):
@ -23,6 +27,10 @@ class VRFDetailView(generics.RetrieveAPIView):
serializer_class = serializers.VRFSerializer serializer_class = serializers.VRFSerializer
#
# Roles
#
class RoleListView(generics.ListAPIView): class RoleListView(generics.ListAPIView):
""" """
List all roles List all roles
@ -39,6 +47,10 @@ class RoleDetailView(generics.RetrieveAPIView):
serializer_class = serializers.RoleSerializer serializer_class = serializers.RoleSerializer
#
# RIRs
#
class RIRListView(generics.ListAPIView): class RIRListView(generics.ListAPIView):
""" """
List all RIRs List all RIRs
@ -55,13 +67,17 @@ class RIRDetailView(generics.RetrieveAPIView):
serializer_class = serializers.RIRSerializer serializer_class = serializers.RIRSerializer
#
# Aggregates
#
class AggregateListView(generics.ListAPIView): class AggregateListView(generics.ListAPIView):
""" """
List aggregates (filterable) List aggregates (filterable)
""" """
queryset = Aggregate.objects.select_related('rir') queryset = Aggregate.objects.select_related('rir')
serializer_class = serializers.AggregateSerializer serializer_class = serializers.AggregateSerializer
filter_class = AggregateFilter filter_class = filters.AggregateFilter
class AggregateDetailView(generics.RetrieveAPIView): class AggregateDetailView(generics.RetrieveAPIView):
@ -72,13 +88,17 @@ class AggregateDetailView(generics.RetrieveAPIView):
serializer_class = serializers.AggregateSerializer serializer_class = serializers.AggregateSerializer
#
# Prefixes
#
class PrefixListView(generics.ListAPIView): class PrefixListView(generics.ListAPIView):
""" """
List prefixes (filterable) List prefixes (filterable)
""" """
queryset = Prefix.objects.select_related('site', 'vrf', 'vlan', 'role') queryset = Prefix.objects.select_related('site', 'vrf', 'vlan', 'role')
serializer_class = serializers.PrefixSerializer serializer_class = serializers.PrefixSerializer
filter_class = PrefixFilter filter_class = filters.PrefixFilter
class PrefixDetailView(generics.RetrieveAPIView): class PrefixDetailView(generics.RetrieveAPIView):
@ -89,6 +109,10 @@ class PrefixDetailView(generics.RetrieveAPIView):
serializer_class = serializers.PrefixSerializer serializer_class = serializers.PrefixSerializer
#
# IP addresses
#
class IPAddressListView(generics.ListAPIView): class IPAddressListView(generics.ListAPIView):
""" """
List IP addresses (filterable) List IP addresses (filterable)
@ -96,7 +120,7 @@ class IPAddressListView(generics.ListAPIView):
queryset = IPAddress.objects.select_related('vrf', 'interface__device', 'nat_inside')\ queryset = IPAddress.objects.select_related('vrf', 'interface__device', 'nat_inside')\
.prefetch_related('nat_outside') .prefetch_related('nat_outside')
serializer_class = serializers.IPAddressSerializer serializer_class = serializers.IPAddressSerializer
filter_class = IPAddressFilter filter_class = filters.IPAddressFilter
class IPAddressDetailView(generics.RetrieveAPIView): class IPAddressDetailView(generics.RetrieveAPIView):
@ -108,13 +132,38 @@ class IPAddressDetailView(generics.RetrieveAPIView):
serializer_class = serializers.IPAddressSerializer serializer_class = serializers.IPAddressSerializer
#
# VLAN groups
#
class VLANGroupListView(generics.ListAPIView):
"""
List all VLAN groups
"""
queryset = VLANGroup.objects.all()
serializer_class = serializers.VLANGroupSerializer
filter_class = filters.VLANGroupFilter
class VLANGroupDetailView(generics.RetrieveAPIView):
"""
Retrieve a single VLAN group
"""
queryset = VLANGroup.objects.all()
serializer_class = serializers.VLANGroupSerializer
#
# VLANs
#
class VLANListView(generics.ListAPIView): class VLANListView(generics.ListAPIView):
""" """
List VLANs (filterable) List VLANs (filterable)
""" """
queryset = VLAN.objects.select_related('site', 'role') queryset = VLAN.objects.select_related('site', 'role')
serializer_class = serializers.VLANSerializer serializer_class = serializers.VLANSerializer
filter_class = VLANFilter filter_class = filters.VLANFilter
class VLANDetailView(generics.RetrieveAPIView): class VLANDetailView(generics.RetrieveAPIView):

View File

@ -4,7 +4,7 @@ from netaddr.core import AddrFormatError
from dcim.models import Site, Device, Interface from dcim.models import Site, Device, Interface
from .models import RIR, Aggregate, VRF, Prefix, IPAddress, VLAN, Role from .models import RIR, Aggregate, VRF, Prefix, IPAddress, VLAN, VLANGroup, Role
class VRFFilter(django_filters.FilterSet): class VRFFilter(django_filters.FilterSet):
@ -176,6 +176,24 @@ class IPAddressFilter(django_filters.FilterSet):
return queryset.filter(vrf__pk=value) return queryset.filter(vrf__pk=value)
class VLANGroupFilter(django_filters.FilterSet):
site_id = django_filters.ModelMultipleChoiceFilter(
name='site',
queryset=Site.objects.all(),
label='Site (ID)',
)
site = django_filters.ModelMultipleChoiceFilter(
name='site',
queryset=Site.objects.all(),
to_field_name='slug',
label='Site (slug)',
)
class Meta:
model = VLANGroup
fields = ['site_id', 'site']
class VLANFilter(django_filters.FilterSet): class VLANFilter(django_filters.FilterSet):
site_id = django_filters.ModelMultipleChoiceFilter( site_id = django_filters.ModelMultipleChoiceFilter(
name='site', name='site',
@ -188,6 +206,17 @@ class VLANFilter(django_filters.FilterSet):
to_field_name='slug', to_field_name='slug',
label='Site (slug)', label='Site (slug)',
) )
group_id = django_filters.ModelMultipleChoiceFilter(
name='group',
queryset=VLANGroup.objects.all(),
label='Group (ID)',
)
group = django_filters.ModelMultipleChoiceFilter(
name='group',
queryset=VLANGroup.objects.all(),
to_field_name='slug',
label='Group',
)
name = django_filters.CharFilter( name = django_filters.CharFilter(
name='name', name='name',
lookup_type='icontains', lookup_type='icontains',

View File

@ -9,7 +9,7 @@ from utilities.forms import (
) )
from .models import ( from .models import (
Aggregate, IPAddress, Prefix, PREFIX_STATUS_CHOICES, RIR, Role, VLAN, VLAN_STATUS_CHOICES, VRF, Aggregate, IPAddress, Prefix, PREFIX_STATUS_CHOICES, RIR, Role, VLAN, VLANGroup, VLAN_STATUS_CHOICES, VRF,
) )
@ -407,22 +407,67 @@ class IPAddressFilterForm(forms.Form, BootstrapMixin):
vrf = forms.ChoiceField(required=False, choices=ipaddress_vrf_choices, label='VRF') vrf = forms.ChoiceField(required=False, choices=ipaddress_vrf_choices, label='VRF')
#
# VLAN groups
#
class VLANGroupForm(forms.ModelForm, BootstrapMixin):
slug = SlugField()
class Meta:
model = VLANGroup
fields = ['site', 'name', 'slug']
class VLANGroupBulkDeleteForm(ConfirmationForm):
pk = forms.ModelMultipleChoiceField(queryset=VLANGroup.objects.all(), widget=forms.MultipleHiddenInput)
def vlangroup_site_choices():
site_choices = Site.objects.annotate(vlangroup_count=Count('vlan_groups'))
return [(s.slug, '{} ({})'.format(s.name, s.vlangroup_count)) for s in site_choices]
class VLANGroupFilterForm(forms.Form, BootstrapMixin):
site = forms.MultipleChoiceField(required=False, choices=vlangroup_site_choices,
widget=forms.SelectMultiple(attrs={'size': 8}))
# #
# VLANs # VLANs
# #
class VLANForm(forms.ModelForm, BootstrapMixin): class VLANForm(forms.ModelForm, BootstrapMixin):
group = forms.ModelChoiceField(queryset=VLANGroup.objects.all(), required=False, label='Group', widget=APISelect(
api_url='/api/ipam/vlan-groups/?site_id={{site}}',
))
class Meta: class Meta:
model = VLAN model = VLAN
fields = ['site', 'vid', 'name', 'status', 'role'] fields = ['site', 'group', 'vid', 'name', 'status', 'role']
help_texts = { help_texts = {
'site': "The site at which this VLAN exists", 'site': "The site at which this VLAN exists",
'group': "VLAN group (optional)",
'vid': "Configured VLAN ID", 'vid': "Configured VLAN ID",
'name': "Configured VLAN name", 'name': "Configured VLAN name",
'status': "Operational status of this VLAN", 'status': "Operational status of this VLAN",
'role': "The primary function of this VLAN", 'role': "The primary function of this VLAN",
} }
widgets = {
'site': forms.Select(attrs={'filter-for': 'group'}),
}
def __init__(self, *args, **kwargs):
super(VLANForm, self).__init__(*args, **kwargs)
# Limit VLAN group choices
if self.is_bound and self.data.get('site'):
self.fields['group'].queryset = VLANGroup.objects.filter(site__pk=self.data['site'])
elif self.initial.get('site'):
self.fields['group'].queryset = VLANGroup.objects.filter(site=self.initial['site'])
else:
self.fields['group'].choices = []
class VLANFromCSVForm(forms.ModelForm): class VLANFromCSVForm(forms.ModelForm):
@ -465,6 +510,11 @@ def vlan_site_choices():
return [(s.slug, '{} ({})'.format(s.name, s.vlan_count)) for s in site_choices] return [(s.slug, '{} ({})'.format(s.name, s.vlan_count)) for s in site_choices]
def vlan_group_choices():
group_choices = VLANGroup.objects.select_related('site').annotate(vlan_count=Count('vlans'))
return [(g.pk, '{} ({})'.format(g, g.vlan_count)) for g in group_choices]
def vlan_status_choices(): def vlan_status_choices():
status_counts = {} status_counts = {}
for status in VLAN.objects.values('status').annotate(count=Count('status')).order_by('status'): for status in VLAN.objects.values('status').annotate(count=Count('status')).order_by('status'):
@ -480,6 +530,8 @@ def vlan_role_choices():
class VLANFilterForm(forms.Form, BootstrapMixin): class VLANFilterForm(forms.Form, BootstrapMixin):
site = forms.MultipleChoiceField(required=False, choices=vlan_site_choices, site = forms.MultipleChoiceField(required=False, choices=vlan_site_choices,
widget=forms.SelectMultiple(attrs={'size': 8})) widget=forms.SelectMultiple(attrs={'size': 8}))
group_id = forms.MultipleChoiceField(required=False, choices=vlan_group_choices, label='VLAN Group',
widget=forms.SelectMultiple(attrs={'size': 8}))
status = forms.MultipleChoiceField(required=False, choices=vlan_status_choices) status = forms.MultipleChoiceField(required=False, choices=vlan_status_choices)
role = forms.MultipleChoiceField(required=False, choices=vlan_role_choices, role = forms.MultipleChoiceField(required=False, choices=vlan_role_choices,
widget=forms.SelectMultiple(attrs={'size': 8})) widget=forms.SelectMultiple(attrs={'size': 8}))

View File

@ -0,0 +1,38 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.7 on 2016-07-15 16:22
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('dcim', '0010_devicebay_installed_device_set_null'),
('ipam', '0002_vrf_add_enforce_unique'),
]
operations = [
migrations.CreateModel(
name='VLANGroup',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=50)),
('slug', models.SlugField()),
('site', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='vlan_groups', to='dcim.Site')),
],
options={
'ordering': ['site', 'name'],
},
),
migrations.AddField(
model_name='vlan',
name='group',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='vlans', to='ipam.VLANGroup'),
),
migrations.AlterUniqueTogether(
name='vlangroup',
unique_together=set([('site', 'name'), ('site', 'slug')]),
),
]

View File

@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.7 on 2016-07-15 17:14
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('ipam', '0003_ipam_add_vlangroups'),
]
operations = [
migrations.AlterModelOptions(
name='vlan',
options={'ordering': ['site', 'group', 'vid'], 'verbose_name': 'VLAN', 'verbose_name_plural': 'VLANs'},
),
migrations.AlterModelOptions(
name='vlangroup',
options={'ordering': ['site', 'name'], 'verbose_name': 'VLAN group', 'verbose_name_plural': 'VLAN groups'},
),
migrations.AlterUniqueTogether(
name='vlan',
unique_together=set([('group', 'name'), ('group', 'vid')]),
),
]

View File

@ -358,13 +358,41 @@ class IPAddress(CreatedUpdatedModel):
return None return None
class VLANGroup(models.Model):
"""
A VLAN group is an arbitrary collection of VLANs within which VLAN IDs and names must be unique.
"""
name = models.CharField(max_length=50)
slug = models.SlugField()
site = models.ForeignKey('dcim.Site', related_name='vlan_groups')
class Meta:
ordering = ['site', 'name']
unique_together = [
['site', 'name'],
['site', 'slug'],
]
verbose_name = 'VLAN group'
verbose_name_plural = 'VLAN groups'
def __unicode__(self):
return '{} - {}'.format(self.site.name, self.name)
def get_absolute_url(self):
return "{}?group_id={}".format(reverse('ipam:vlan_list'), self.pk)
class VLAN(CreatedUpdatedModel): class VLAN(CreatedUpdatedModel):
""" """
A VLAN is a distinct layer two forwarding domain identified by a 12-bit integer (1-4094). Each VLAN must be assigned A VLAN is a distinct layer two forwarding domain identified by a 12-bit integer (1-4094). Each VLAN must be assigned
to a Site, however VLAN IDs need not be unique within a Site. Like Prefixes, each VLAN is assigned an operational to a Site, however VLAN IDs need not be unique within a Site. A VLAN may optionally be assigned to a VLANGroup,
status and optionally a user-defined Role. A VLAN can have zero or more Prefixes assigned to it. within which all VLAN IDs and names but be unique.
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)
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),
MaxValueValidator(4094) MaxValueValidator(4094)
@ -374,7 +402,11 @@ class VLAN(CreatedUpdatedModel):
role = models.ForeignKey('Role', related_name='vlans', on_delete=models.SET_NULL, blank=True, null=True) role = models.ForeignKey('Role', related_name='vlans', on_delete=models.SET_NULL, blank=True, null=True)
class Meta: class Meta:
ordering = ['site', 'vid'] ordering = ['site', 'group', 'vid']
unique_together = [
['group', 'vid'],
['group', 'name'],
]
verbose_name = 'VLAN' verbose_name = 'VLAN'
verbose_name_plural = 'VLANs' verbose_name_plural = 'VLANs'
@ -384,6 +416,12 @@ class VLAN(CreatedUpdatedModel):
def get_absolute_url(self): def get_absolute_url(self):
return reverse('ipam:vlan', args=[self.pk]) return reverse('ipam:vlan', args=[self.pk])
def clean(self):
# Validate VLAN group
if self.vlan_group and self.vlan_group.site != self.site:
raise ValidationError("VLAN group must belong to the assigned site ({}).".format(self.site))
def to_csv(self): def to_csv(self):
return ','.join([ return ','.join([
self.site.name, self.site.name,

View File

@ -3,7 +3,7 @@ from django_tables2.utils import Accessor
from utilities.tables import BaseTable, ToggleColumn from utilities.tables import BaseTable, ToggleColumn
from .models import Aggregate, IPAddress, Prefix, RIR, Role, VLAN, VRF from .models import Aggregate, IPAddress, Prefix, RIR, Role, VLAN, VLANGroup, VRF
RIR_EDIT_LINK = """ RIR_EDIT_LINK = """
@ -50,6 +50,12 @@ STATUS_LABEL = """
{% endif %} {% endif %}
""" """
VLANGROUP_EDIT_LINK = """
{% if perms.ipam.change_vlangroup %}
<a href="{% url 'ipam:vlangroup_edit' pk=record.pk %}">Edit</a>
{% endif %}
"""
# #
# VRFs # VRFs
@ -177,6 +183,23 @@ class IPAddressBriefTable(BaseTable):
fields = ('address', 'device', 'interface', 'nat_inside') fields = ('address', 'device', 'interface', 'nat_inside')
#
# VLAN groups
#
class VLANGroupTable(BaseTable):
pk = ToggleColumn()
name = tables.LinkColumn(verbose_name='Name')
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
vlan_count = tables.Column(verbose_name='VLANs')
slug = tables.Column(verbose_name='Slug')
edit = tables.TemplateColumn(template_code=VLANGROUP_EDIT_LINK, verbose_name='')
class Meta(BaseTable.Meta):
model = VLANGroup
fields = ('pk', 'name', 'site', 'vlan_count', 'slug', 'edit')
# #
# VLANs # VLANs
# #
@ -185,10 +208,11 @@ class VLANTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
vid = tables.LinkColumn('ipam:vlan', args=[Accessor('pk')], verbose_name='ID') vid = tables.LinkColumn('ipam:vlan', args=[Accessor('pk')], verbose_name='ID')
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site') site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group')
name = tables.Column(verbose_name='Name') name = tables.Column(verbose_name='Name')
status = tables.TemplateColumn(STATUS_LABEL, verbose_name='Status') status = tables.TemplateColumn(STATUS_LABEL, verbose_name='Status')
role = tables.Column(verbose_name='Role') role = tables.Column(verbose_name='Role')
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = VLAN model = VLAN
fields = ('pk', 'vid', 'site', 'name', 'status', 'role') fields = ('pk', 'vid', 'site', 'group', 'name', 'status', 'role')

View File

@ -58,6 +58,12 @@ urlpatterns = [
url(r'^ip-addresses/(?P<pk>\d+)/edit/$', views.IPAddressEditView.as_view(), name='ipaddress_edit'), url(r'^ip-addresses/(?P<pk>\d+)/edit/$', views.IPAddressEditView.as_view(), name='ipaddress_edit'),
url(r'^ip-addresses/(?P<pk>\d+)/delete/$', views.IPAddressDeleteView.as_view(), name='ipaddress_delete'), url(r'^ip-addresses/(?P<pk>\d+)/delete/$', views.IPAddressDeleteView.as_view(), name='ipaddress_delete'),
# VLAN groups
url(r'^vlan-groups/$', views.VLANGroupListView.as_view(), name='vlangroup_list'),
url(r'^vlan-groups/add/$', views.VLANGroupEditView.as_view(), name='vlangroup_add'),
url(r'^vlan-groups/delete/$', views.VLANGroupBulkDeleteView.as_view(), name='vlangroup_bulk_delete'),
url(r'^vlan-groups/(?P<pk>\d+)/edit/$', views.VLANGroupEditView.as_view(), name='vlangroup_edit'),
# VLANs # VLANs
url(r'^vlans/$', views.VLANListView.as_view(), name='vlan_list'), url(r'^vlans/$', views.VLANListView.as_view(), name='vlan_list'),
url(r'^vlans/add/$', views.VLANEditView.as_view(), name='vlan_add'), url(r'^vlans/add/$', views.VLANEditView.as_view(), name='vlan_add'),

View File

@ -12,7 +12,7 @@ from utilities.views import (
) )
from . import filters, forms, tables from . import filters, forms, tables
from .models import Aggregate, IPAddress, Prefix, RIR, Role, VLAN, VRF from .models import Aggregate, IPAddress, Prefix, RIR, Role, VLAN, VLANGroup, VRF
def add_available_prefixes(parent, prefix_list): def add_available_prefixes(parent, prefix_list):
@ -483,6 +483,33 @@ class IPAddressBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
default_redirect_url = 'ipam:ipaddress_list' default_redirect_url = 'ipam:ipaddress_list'
#
# VLAN groups
#
class VLANGroupListView(ObjectListView):
queryset = VLANGroup.objects.annotate(vlan_count=Count('vlans'))
filter = filters.VLANGroupFilter
filter_form = forms.VLANGroupFilterForm
table = tables.VLANGroupTable
edit_permissions = ['ipam.change_vlangroup', 'ipam.delete_vlangroup']
template_name = 'ipam/vlangroup_list.html'
class VLANGroupEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.change_vlangroup'
model = VLANGroup
form_class = forms.VLANGroupForm
cancel_url = 'ipam:vlangroup_list'
class VLANGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'ipam.delete_vlangroup'
cls = VLANGroup
form = forms.VLANGroupBulkDeleteForm
default_redirect_url = 'ipam:vlangroup_list'
# #
# VLANs # VLANs
# #

View File

@ -110,7 +110,7 @@
{% endif %} {% endif %}
</ul> </ul>
</li> </li>
<li class="dropdown{% if request.path|startswith:'/ipam/' and not request.path|startswith:'/ipam/vlans/' %} active{% endif %}"> <li class="dropdown{% if request.path|startswith:'/ipam/' and not request.path|startswith:'/ipam/vlan' %} active{% endif %}">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">IP Space <span class="caret"></span></a> <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">IP Space <span class="caret"></span></a>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li><a href="{% url 'ipam:ipaddress_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> IP Addresses</a></li> <li><a href="{% url 'ipam:ipaddress_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> IP Addresses</a></li>
@ -156,17 +156,20 @@
{% endif %} {% endif %}
</ul> </ul>
</li> </li>
<li class="dropdown{% if request.path|startswith:'/ipam/vlans/' %} active{% endif %}"> <li class="dropdown{% if request.path|startswith:'/ipam/vlan' %} active{% endif %}">
{% if perms.ipam.add_vlan %}
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">VLANs <span class="caret"></span></a> <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">VLANs <span class="caret"></span></a>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li><a href="{% url 'ipam:vlan_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> VLANs</a></li> <li><a href="{% url 'ipam:vlan_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> VLANs</a></li>
{% if perms.ipam.add_vlan %}
<li><a href="{% url 'ipam:vlan_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a VLAN</a></li> <li><a href="{% url 'ipam:vlan_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a VLAN</a></li>
<li><a href="{% url 'ipam:vlan_import' %}"><i class="glyphicon glyphicon-import" aria-hidden="true"></i> Import VLANs</a></li> <li><a href="{% url 'ipam:vlan_import' %}"><i class="glyphicon glyphicon-import" aria-hidden="true"></i> Import VLANs</a></li>
</ul>
{% else %}
<a href="{% url 'ipam:vlan_list' %}">VLANs</a>
{% endif %} {% endif %}
<li class="divider"></li>
<li><a href="{% url 'ipam:vlangroup_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> VLAN Groups</a></li>
{% if perms.ipam.add_vlangroup %}
<li><a href="{% url 'ipam:vlangroup_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a VLAN Group</a></li>
{% endif %}
</ul>
</li> </li>
<li class="dropdown{% if request.path|startswith:'/circuits/' %} active{% endif %}"> <li class="dropdown{% if request.path|startswith:'/circuits/' %} active{% endif %}">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Circuits <span class="caret"></span></a> <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Circuits <span class="caret"></span></a>

View File

@ -0,0 +1,24 @@
{% extends '_base.html' %}
{% load helpers %}
{% block title %}VLAN Groups{% endblock %}
{% block content %}
<div class="pull-right">
{% if perms.ipam.add_vlangroup %}
<a href="{% url 'ipam:vlangroup_add' %}" class="btn btn-primary">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
Add a VLAN group
</a>
{% endif %}
</div>
<h1>VLAN Groups</h1>
<div class="row">
<div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_delete_url='ipam:vlangroup_bulk_delete' %}
</div>
<div class="col-md-3">
{% include 'inc/filter_panel.html' %}
</div>
</div>
{% endblock %}