Skip to content

Commit

Permalink
Merge pull request #309 from digitalocean/vlan-groups
Browse files Browse the repository at this point in the history
Closes #111: Implement VLAN groups
  • Loading branch information
jeremystretch authored Jul 15, 2016
2 parents 23451fe + 45a8ee7 commit a9ab0a0
Show file tree
Hide file tree
Showing 14 changed files with 377 additions and 29 deletions.
10 changes: 9 additions & 1 deletion netbox/ipam/admin.py
Original file line number Diff line number Diff line change
@@ -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,
)


Expand Down Expand Up @@ -57,6 +57,14 @@ def get_queryset(self, request):
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']
Expand Down
23 changes: 21 additions & 2 deletions netbox/ipam/api/serializers.py
Original file line number Diff line number Diff line change
@@ -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


#
Expand Down Expand Up @@ -73,17 +73,36 @@ class Meta(AggregateSerializer.Meta):
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):
Expand Down
4 changes: 4 additions & 0 deletions netbox/ipam/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@
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'),
Expand Down
63 changes: 56 additions & 7 deletions netbox/ipam/api/views.py
Original file line number Diff line number Diff line change
@@ -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):
Expand All @@ -23,6 +27,10 @@ class VRFDetailView(generics.RetrieveAPIView):
serializer_class = serializers.VRFSerializer


#
# Roles
#

class RoleListView(generics.ListAPIView):
"""
List all roles
Expand All @@ -39,6 +47,10 @@ class RoleDetailView(generics.RetrieveAPIView):
serializer_class = serializers.RoleSerializer


#
# RIRs
#

class RIRListView(generics.ListAPIView):
"""
List all RIRs
Expand All @@ -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):
Expand All @@ -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):
Expand All @@ -89,14 +109,18 @@ class PrefixDetailView(generics.RetrieveAPIView):
serializer_class = serializers.PrefixSerializer


#
# IP addresses
#

class IPAddressListView(generics.ListAPIView):
"""
List IP addresses (filterable)
"""
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):
Expand All @@ -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):
Expand Down
31 changes: 30 additions & 1 deletion netbox/ipam/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

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):
Expand Down Expand Up @@ -176,6 +176,24 @@ def _vrf(self, queryset, 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):
site_id = django_filters.ModelMultipleChoiceFilter(
name='site',
Expand All @@ -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',
Expand Down
56 changes: 54 additions & 2 deletions netbox/ipam/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
)

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


Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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'):
Expand All @@ -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}))
Loading

0 comments on commit a9ab0a0

Please sign in to comment.