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:
@ -1,7 +1,7 @@
|
||||
from django.contrib import admin
|
||||
|
||||
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')
|
||||
|
||||
|
||||
@admin.register(VLANGroup)
|
||||
class VLANGroupAdmin(admin.ModelAdmin):
|
||||
list_display = ['name', 'site', 'slug']
|
||||
prepopulated_fields = {
|
||||
'slug': ['name'],
|
||||
}
|
||||
|
||||
|
||||
@admin.register(VLAN)
|
||||
class VLANAdmin(admin.ModelAdmin):
|
||||
list_display = ['site', 'vid', 'name', 'status', 'role']
|
||||
|
@ -1,7 +1,7 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
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']
|
||||
|
||||
|
||||
#
|
||||
# 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
|
||||
#
|
||||
|
||||
class VLANSerializer(serializers.ModelSerializer):
|
||||
site = SiteNestedSerializer()
|
||||
group = VLANGroupNestedSerializer()
|
||||
role = RoleNestedSerializer()
|
||||
|
||||
class Meta:
|
||||
model = VLAN
|
||||
fields = ['id', 'site', 'vid', 'name', 'status', 'role', 'display_name']
|
||||
fields = ['id', 'site', 'group', 'vid', 'name', 'status', 'role', 'display_name']
|
||||
|
||||
|
||||
class VLANNestedSerializer(VLANSerializer):
|
||||
|
@ -29,6 +29,10 @@ urlpatterns = [
|
||||
url(r'^ip-addresses/$', IPAddressListView.as_view(), name='ipaddress_list'),
|
||||
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
|
||||
url(r'^vlans/$', VLANListView.as_view(), name='vlan_list'),
|
||||
url(r'^vlans/(?P<pk>\d+)/$', VLANDetailView.as_view(), name='vlan_detail'),
|
||||
|
@ -1,18 +1,22 @@
|
||||
from rest_framework import generics
|
||||
|
||||
from ipam.models import VRF, Role, RIR, Aggregate, Prefix, IPAddress, VLAN
|
||||
from ipam.filters import AggregateFilter, PrefixFilter, IPAddressFilter, VLANFilter, VRFFilter
|
||||
from ipam.models import VRF, Role, RIR, Aggregate, Prefix, IPAddress, VLAN, VLANGroup
|
||||
from ipam import filters
|
||||
|
||||
from . import serializers
|
||||
|
||||
|
||||
#
|
||||
# VRFs
|
||||
#
|
||||
|
||||
class VRFListView(generics.ListAPIView):
|
||||
"""
|
||||
List all VRFs
|
||||
"""
|
||||
queryset = VRF.objects.all()
|
||||
serializer_class = serializers.VRFSerializer
|
||||
filter_class = VRFFilter
|
||||
filter_class = filters.VRFFilter
|
||||
|
||||
|
||||
class VRFDetailView(generics.RetrieveAPIView):
|
||||
@ -23,6 +27,10 @@ class VRFDetailView(generics.RetrieveAPIView):
|
||||
serializer_class = serializers.VRFSerializer
|
||||
|
||||
|
||||
#
|
||||
# Roles
|
||||
#
|
||||
|
||||
class RoleListView(generics.ListAPIView):
|
||||
"""
|
||||
List all roles
|
||||
@ -39,6 +47,10 @@ class RoleDetailView(generics.RetrieveAPIView):
|
||||
serializer_class = serializers.RoleSerializer
|
||||
|
||||
|
||||
#
|
||||
# RIRs
|
||||
#
|
||||
|
||||
class RIRListView(generics.ListAPIView):
|
||||
"""
|
||||
List all RIRs
|
||||
@ -55,13 +67,17 @@ class RIRDetailView(generics.RetrieveAPIView):
|
||||
serializer_class = serializers.RIRSerializer
|
||||
|
||||
|
||||
#
|
||||
# Aggregates
|
||||
#
|
||||
|
||||
class AggregateListView(generics.ListAPIView):
|
||||
"""
|
||||
List aggregates (filterable)
|
||||
"""
|
||||
queryset = Aggregate.objects.select_related('rir')
|
||||
serializer_class = serializers.AggregateSerializer
|
||||
filter_class = AggregateFilter
|
||||
filter_class = filters.AggregateFilter
|
||||
|
||||
|
||||
class AggregateDetailView(generics.RetrieveAPIView):
|
||||
@ -72,13 +88,17 @@ class AggregateDetailView(generics.RetrieveAPIView):
|
||||
serializer_class = serializers.AggregateSerializer
|
||||
|
||||
|
||||
#
|
||||
# Prefixes
|
||||
#
|
||||
|
||||
class PrefixListView(generics.ListAPIView):
|
||||
"""
|
||||
List prefixes (filterable)
|
||||
"""
|
||||
queryset = Prefix.objects.select_related('site', 'vrf', 'vlan', 'role')
|
||||
serializer_class = serializers.PrefixSerializer
|
||||
filter_class = PrefixFilter
|
||||
filter_class = filters.PrefixFilter
|
||||
|
||||
|
||||
class PrefixDetailView(generics.RetrieveAPIView):
|
||||
@ -89,6 +109,10 @@ class PrefixDetailView(generics.RetrieveAPIView):
|
||||
serializer_class = serializers.PrefixSerializer
|
||||
|
||||
|
||||
#
|
||||
# IP addresses
|
||||
#
|
||||
|
||||
class IPAddressListView(generics.ListAPIView):
|
||||
"""
|
||||
List IP addresses (filterable)
|
||||
@ -96,7 +120,7 @@ class IPAddressListView(generics.ListAPIView):
|
||||
queryset = IPAddress.objects.select_related('vrf', 'interface__device', 'nat_inside')\
|
||||
.prefetch_related('nat_outside')
|
||||
serializer_class = serializers.IPAddressSerializer
|
||||
filter_class = IPAddressFilter
|
||||
filter_class = filters.IPAddressFilter
|
||||
|
||||
|
||||
class IPAddressDetailView(generics.RetrieveAPIView):
|
||||
@ -108,13 +132,38 @@ class IPAddressDetailView(generics.RetrieveAPIView):
|
||||
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):
|
||||
"""
|
||||
List VLANs (filterable)
|
||||
"""
|
||||
queryset = VLAN.objects.select_related('site', 'role')
|
||||
serializer_class = serializers.VLANSerializer
|
||||
filter_class = VLANFilter
|
||||
filter_class = filters.VLANFilter
|
||||
|
||||
|
||||
class VLANDetailView(generics.RetrieveAPIView):
|
||||
|
@ -4,7 +4,7 @@ from netaddr.core import AddrFormatError
|
||||
|
||||
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):
|
||||
@ -176,6 +176,24 @@ class IPAddressFilter(django_filters.FilterSet):
|
||||
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):
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='site',
|
||||
@ -188,6 +206,17 @@ class VLANFilter(django_filters.FilterSet):
|
||||
to_field_name='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='name',
|
||||
lookup_type='icontains',
|
||||
|
@ -9,7 +9,7 @@ from utilities.forms 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')
|
||||
|
||||
|
||||
#
|
||||
# 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
|
||||
#
|
||||
|
||||
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:
|
||||
model = VLAN
|
||||
fields = ['site', 'vid', 'name', 'status', 'role']
|
||||
fields = ['site', 'group', 'vid', 'name', 'status', 'role']
|
||||
help_texts = {
|
||||
'site': "The site at which this VLAN exists",
|
||||
'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'}),
|
||||
}
|
||||
|
||||
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):
|
||||
@ -465,6 +510,11 @@ def vlan_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():
|
||||
status_counts = {}
|
||||
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):
|
||||
site = forms.MultipleChoiceField(required=False, choices=vlan_site_choices,
|
||||
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)
|
||||
role = forms.MultipleChoiceField(required=False, choices=vlan_role_choices,
|
||||
widget=forms.SelectMultiple(attrs={'size': 8}))
|
||||
|
38
netbox/ipam/migrations/0003_ipam_add_vlangroups.py
Normal file
38
netbox/ipam/migrations/0003_ipam_add_vlangroups.py
Normal 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')]),
|
||||
),
|
||||
]
|
27
netbox/ipam/migrations/0004_ipam_vlangroup_uniqueness.py
Normal file
27
netbox/ipam/migrations/0004_ipam_vlangroup_uniqueness.py
Normal 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')]),
|
||||
),
|
||||
]
|
@ -358,13 +358,41 @@ class IPAddress(CreatedUpdatedModel):
|
||||
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):
|
||||
"""
|
||||
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
|
||||
status and optionally a user-defined Role. A VLAN can have zero or more Prefixes assigned to it.
|
||||
to a Site, however VLAN IDs need not be unique within a Site. A VLAN may optionally be assigned to a VLANGroup,
|
||||
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)
|
||||
group = models.ForeignKey('VLANGroup', related_name='vlans', blank=True, null=True, on_delete=models.PROTECT)
|
||||
vid = models.PositiveSmallIntegerField(verbose_name='ID', validators=[
|
||||
MinValueValidator(1),
|
||||
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)
|
||||
|
||||
class Meta:
|
||||
ordering = ['site', 'vid']
|
||||
ordering = ['site', 'group', 'vid']
|
||||
unique_together = [
|
||||
['group', 'vid'],
|
||||
['group', 'name'],
|
||||
]
|
||||
verbose_name = 'VLAN'
|
||||
verbose_name_plural = 'VLANs'
|
||||
|
||||
@ -384,6 +416,12 @@ class VLAN(CreatedUpdatedModel):
|
||||
def get_absolute_url(self):
|
||||
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):
|
||||
return ','.join([
|
||||
self.site.name,
|
||||
|
@ -3,7 +3,7 @@ from django_tables2.utils import Accessor
|
||||
|
||||
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 = """
|
||||
@ -50,6 +50,12 @@ STATUS_LABEL = """
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
VLANGROUP_EDIT_LINK = """
|
||||
{% if perms.ipam.change_vlangroup %}
|
||||
<a href="{% url 'ipam:vlangroup_edit' pk=record.pk %}">Edit</a>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
|
||||
#
|
||||
# VRFs
|
||||
@ -177,6 +183,23 @@ class IPAddressBriefTable(BaseTable):
|
||||
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
|
||||
#
|
||||
@ -185,10 +208,11 @@ class VLANTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
vid = tables.LinkColumn('ipam:vlan', args=[Accessor('pk')], verbose_name='ID')
|
||||
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')
|
||||
status = tables.TemplateColumn(STATUS_LABEL, verbose_name='Status')
|
||||
role = tables.Column(verbose_name='Role')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = VLAN
|
||||
fields = ('pk', 'vid', 'site', 'name', 'status', 'role')
|
||||
fields = ('pk', 'vid', 'site', 'group', 'name', 'status', 'role')
|
||||
|
@ -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+)/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
|
||||
url(r'^vlans/$', views.VLANListView.as_view(), name='vlan_list'),
|
||||
url(r'^vlans/add/$', views.VLANEditView.as_view(), name='vlan_add'),
|
||||
|
@ -12,7 +12,7 @@ from utilities.views import (
|
||||
)
|
||||
|
||||
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):
|
||||
@ -483,6 +483,33 @@ class IPAddressBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
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
|
||||
#
|
||||
|
@ -110,7 +110,7 @@
|
||||
{% endif %}
|
||||
</ul>
|
||||
</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>
|
||||
<ul class="dropdown-menu">
|
||||
<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 %}
|
||||
</ul>
|
||||
</li>
|
||||
<li class="dropdown{% if request.path|startswith:'/ipam/vlans/' %} 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>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="{% url 'ipam:vlan_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> VLANs</a></li>
|
||||
<li class="dropdown{% if request.path|startswith:'/ipam/vlan' %} active{% endif %}">
|
||||
<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">
|
||||
<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_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 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>
|
||||
|
24
netbox/templates/ipam/vlangroup_list.html
Normal file
24
netbox/templates/ipam/vlangroup_list.html
Normal 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 %}
|
Reference in New Issue
Block a user