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

Implemented tenancy for VRFs and VLANs

This commit is contained in:
Jeremy Stretch
2016-07-27 11:29:20 -04:00
parent 65b008a493
commit 2abee211a2
16 changed files with 144 additions and 21 deletions

View File

@ -7,7 +7,12 @@ from .models import (
@admin.register(VRF)
class VRFAdmin(admin.ModelAdmin):
list_display = ['name', 'rd']
list_display = ['name', 'rd', 'tenant', 'enforce_unique']
list_filter = ['tenant']
def get_queryset(self, request):
qs = super(VRFAdmin, self).get_queryset(request)
return qs.select_related('tenant')
@admin.register(Role)
@ -67,10 +72,10 @@ class VLANGroupAdmin(admin.ModelAdmin):
@admin.register(VLAN)
class VLANAdmin(admin.ModelAdmin):
list_display = ['site', 'vid', 'name', 'status', 'role']
list_filter = ['site', 'status', 'role']
list_display = ['site', 'vid', 'name', 'tenant', 'status', 'role']
list_filter = ['site', 'tenant', 'status', 'role']
search_fields = ['vid', 'name']
def get_queryset(self, request):
qs = super(VLANAdmin, self).get_queryset(request)
return qs.select_related('site', 'role')
return qs.select_related('site', 'tenant', 'role')

View File

@ -3,6 +3,7 @@ from netaddr import IPNetwork
from netaddr.core import AddrFormatError
from dcim.models import Site, Device, Interface
from tenancy.models import Tenant
from .models import RIR, Aggregate, VRF, Prefix, IPAddress, VLAN, VLANGroup, Role
@ -13,6 +14,17 @@ class VRFFilter(django_filters.FilterSet):
lookup_type='icontains',
label='Name',
)
tenant_id = django_filters.ModelMultipleChoiceFilter(
name='tenant',
queryset=Tenant.objects.all(),
label='Tenant (ID)',
)
tenant = django_filters.ModelMultipleChoiceFilter(
name='tenant',
queryset=Tenant.objects.all(),
to_field_name='slug',
label='Tenant (slug)',
)
class Meta:
model = VRF
@ -226,6 +238,17 @@ class VLANFilter(django_filters.FilterSet):
name='vid',
label='VLAN number (1-4095)',
)
tenant_id = django_filters.ModelMultipleChoiceFilter(
name='tenant',
queryset=Tenant.objects.all(),
label='Tenant (ID)',
)
tenant = django_filters.ModelMultipleChoiceFilter(
name='tenant',
queryset=Tenant.objects.all(),
to_field_name='slug',
label='Tenant (slug)',
)
role_id = django_filters.ModelMultipleChoiceFilter(
name='role',
queryset=Role.objects.all(),

View File

@ -4,6 +4,7 @@ from django import forms
from django.db.models import Count
from dcim.models import Site, Device, Interface
from tenancy.models import Tenant
from utilities.forms import BootstrapMixin, APISelect, Livesearch, CSVDataField, BulkImportForm, SlugField
from .models import (
@ -23,7 +24,7 @@ class VRFForm(forms.ModelForm, BootstrapMixin):
class Meta:
model = VRF
fields = ['name', 'rd', 'enforce_unique', 'description']
fields = ['name', 'rd', 'tenant', 'enforce_unique', 'description']
labels = {
'rd': "RD",
}
@ -33,10 +34,12 @@ class VRFForm(forms.ModelForm, BootstrapMixin):
class VRFFromCSVForm(forms.ModelForm):
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
error_messages={'invalid_choice': 'Tenant not found.'})
class Meta:
model = VRF
fields = ['name', 'rd', 'enforce_unique', 'description']
fields = ['name', 'rd', 'tenant', 'enforce_unique', 'description']
class VRFImportForm(BulkImportForm, BootstrapMixin):
@ -45,9 +48,20 @@ class VRFImportForm(BulkImportForm, BootstrapMixin):
class VRFBulkEditForm(forms.Form, BootstrapMixin):
pk = forms.ModelMultipleChoiceField(queryset=VRF.objects.all(), widget=forms.MultipleHiddenInput)
tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
description = forms.CharField(max_length=100, required=False)
def vrf_tenant_choices():
tenant_choices = Tenant.objects.annotate(vrf_count=Count('vrfs'))
return [(t.slug, u'{} ({})'.format(t.name, t.vrf_count)) for t in tenant_choices]
class VRFFilterForm(forms.Form, BootstrapMixin):
tenant = forms.MultipleChoiceField(required=False, choices=vrf_tenant_choices,
widget=forms.SelectMultiple(attrs={'size': 8}))
#
# RIRs
#
@ -444,7 +458,7 @@ class VLANForm(forms.ModelForm, BootstrapMixin):
class Meta:
model = VLAN
fields = ['site', 'group', 'vid', 'name', 'description', 'status', 'role']
fields = ['site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description']
help_texts = {
'site': "The site at which this VLAN exists",
'group': "VLAN group (optional)",
@ -475,13 +489,15 @@ class VLANFromCSVForm(forms.ModelForm):
error_messages={'invalid_choice': 'Device not found.'})
group = forms.ModelChoiceField(queryset=VLANGroup.objects.all(), required=False, to_field_name='name',
error_messages={'invalid_choice': 'VLAN group not found.'})
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
error_messages={'invalid_choice': 'Tenant not found.'})
status_name = forms.ChoiceField(choices=[(s[1], s[0]) for s in VLAN_STATUS_CHOICES])
role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False, to_field_name='name',
error_messages={'invalid_choice': 'Invalid role.'})
class Meta:
model = VLAN
fields = ['site', 'group', 'vid', 'name', 'status_name', 'role', 'description']
fields = ['site', 'group', 'vid', 'name', 'tenant', 'status_name', 'role', 'description']
def save(self, *args, **kwargs):
m = super(VLANFromCSVForm, self).save(commit=False)
@ -500,6 +516,7 @@ class VLANBulkEditForm(forms.Form, BootstrapMixin):
pk = forms.ModelMultipleChoiceField(queryset=VLAN.objects.all(), widget=forms.MultipleHiddenInput)
site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False)
group = forms.ModelChoiceField(queryset=VLANGroup.objects.all(), required=False)
tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
status = forms.ChoiceField(choices=FORM_VLAN_STATUS_CHOICES, required=False)
role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False)
description = forms.CharField(max_length=100, required=False)
@ -515,6 +532,11 @@ def vlan_group_choices():
return [(g.pk, u'{} ({})'.format(g, g.vlan_count)) for g in group_choices]
def vlan_tenant_choices():
tenant_choices = Tenant.objects.annotate(vrf_count=Count('vlans'))
return [(t.slug, u'{} ({})'.format(t.name, t.vrf_count)) for t in tenant_choices]
def vlan_status_choices():
status_counts = {}
for status in VLAN.objects.values('status').annotate(count=Count('status')).order_by('status'):
@ -532,6 +554,8 @@ class VLANFilterForm(forms.Form, BootstrapMixin):
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}))
tenant = forms.MultipleChoiceField(required=False, choices=vlan_tenant_choices,
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}))

View File

@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.8 on 2016-07-27 14:39
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('tenancy', '0001_initial'),
('ipam', '0005_auto_20160725_1842'),
]
operations = [
migrations.AddField(
model_name='vlan',
name='tenant',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='vlans', to='tenancy.Tenant'),
),
migrations.AddField(
model_name='vrf',
name='tenant',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='vrfs', to='tenancy.Tenant'),
),
]

View File

@ -7,6 +7,7 @@ from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from dcim.models import Interface
from tenancy.models import Tenant
from utilities.models import CreatedUpdatedModel
from .fields import IPNetworkField, IPAddressField
@ -46,6 +47,7 @@ class VRF(CreatedUpdatedModel):
"""
name = models.CharField(max_length=50)
rd = models.CharField(max_length=21, unique=True, verbose_name='Route distinguisher')
tenant = models.ForeignKey(Tenant, related_name='vrfs', blank=True, null=True, on_delete=models.PROTECT)
enforce_unique = models.BooleanField(default=True, verbose_name='Enforce unique space',
help_text="Prevent duplicate prefixes/IP addresses within this VRF")
description = models.CharField(max_length=100, blank=True)
@ -65,6 +67,8 @@ class VRF(CreatedUpdatedModel):
return ','.join([
self.name,
self.rd,
self.tenant.name if self.tenant else '',
'True' if self.enforce_unique else '',
self.description,
])
@ -291,7 +295,7 @@ class Prefix(CreatedUpdatedModel):
class IPAddress(CreatedUpdatedModel):
"""
An IPAddress represents an individual IPV4 or IPv6 address and its mask. The mask length should match what is
An IPAddress represents an individual IPv4 or IPv6 address and its mask. The mask length should match what is
configured in the real world. (Typically, only loopback interfaces are configured with /32 or /128 masks.) Like
Prefixes, IPAddresses can optionally be assigned to a VRF. An IPAddress can optionally be assigned to an Interface.
Interfaces can have zero or more IPAddresses assigned to them.
@ -407,9 +411,10 @@ class VLAN(CreatedUpdatedModel):
MaxValueValidator(4094)
])
name = models.CharField(max_length=64)
description = models.CharField(max_length=100, blank=True)
tenant = models.ForeignKey(Tenant, related_name='vlans', blank=True, null=True, on_delete=models.PROTECT)
status = models.PositiveSmallIntegerField('Status', choices=VLAN_STATUS_CHOICES, default=1)
role = models.ForeignKey('Role', related_name='vlans', on_delete=models.SET_NULL, blank=True, null=True)
description = models.CharField(max_length=100, blank=True)
class Meta:
ordering = ['site', 'group', 'vid']
@ -438,6 +443,7 @@ class VLAN(CreatedUpdatedModel):
self.group.name if self.group else '',
str(self.vid),
self.name,
self.tenant.name if self.tenant else '',
self.get_status_display(),
self.role.name if self.role else '',
self.description,

View File

@ -58,11 +58,12 @@ class VRFTable(BaseTable):
pk = ToggleColumn()
name = tables.LinkColumn('ipam:vrf', args=[Accessor('pk')], verbose_name='Name')
rd = tables.Column(verbose_name='RD')
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
description = tables.Column(orderable=False, verbose_name='Description')
class Meta(BaseTable.Meta):
model = VRF
fields = ('pk', 'name', 'rd', 'description')
fields = ('pk', 'name', 'rd', 'tenant', 'description')
#
@ -203,9 +204,10 @@ class VLANTable(BaseTable):
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')
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
status = tables.TemplateColumn(STATUS_LABEL, verbose_name='Status')
role = tables.Column(verbose_name='Role')
class Meta(BaseTable.Meta):
model = VLAN
fields = ('pk', 'vid', 'site', 'group', 'name', 'status', 'role')
fields = ('pk', 'vid', 'site', 'group', 'name', 'tenant', 'status', 'role')

View File

@ -36,8 +36,9 @@ def add_available_prefixes(parent, prefix_list):
#
class VRFListView(ObjectListView):
queryset = VRF.objects.all()
queryset = VRF.objects.select_related('tenant')
filter = filters.VRFFilter
filter_form = forms.VRFFilterForm
table = tables.VRFTable
edit_permissions = ['ipam.change_vrf', 'ipam.delete_vrf']
template_name = 'ipam/vrf_list.html'
@ -85,7 +86,7 @@ class VRFBulkEditView(PermissionRequiredMixin, BulkEditView):
def update_objects(self, pk_list, form):
fields_to_update = {}
for field in ['description']:
for field in ['tenant', 'description']:
if form.cleaned_data[field]:
fields_to_update[field] = form.cleaned_data[field]
@ -558,7 +559,7 @@ class VLANBulkEditView(PermissionRequiredMixin, BulkEditView):
def update_objects(self, pk_list, form):
fields_to_update = {}
for field in ['site', 'group', 'status', 'role', 'description']:
for field in ['site', 'group', 'tenant', 'status', 'role', 'description']:
if form.cleaned_data[field]:
fields_to_update[field] = form.cleaned_data[field]

View File

@ -70,10 +70,10 @@
<td>{{ vlan.name }}</td>
</tr>
<tr>
<td>Description</td>
<td>Tenant</td>
<td>
{% if vlan.description %}
{{ vlan.description }}
{% if vlan.tenant %}
<a href="{{ vlan.tenant.get_absolute_url }}">{{ vlan.tenant }}</a>
{% else %}
<span class="text-muted">None</span>
{% endif %}
@ -89,6 +89,16 @@
<td>Role</td>
<td>{{ vlan.role }}</td>
</tr>
<tr>
<td>Description</td>
<td>
{% if vlan.description %}
{{ vlan.description }}
{% else %}
<span class="text-muted">None</span>
{% endif %}
</td>
</tr>
<tr>
<td>Created</td>
<td>{{ vlan.created }}</td>

View File

@ -9,7 +9,8 @@
<td><a href="{% url 'ipam:vlan' pk=vlan.pk %}">{{ vlan.vid }}</a></td>
<td>{{ vlan.name }}</td>
<td>{{ vlan.site }}</td>
<td>{{ vlan.status }}</td>
<td>{{ vlan.tenant }}</td>
<td>{{ vlan.get_status_display }}</td>
<td>{{ vlan.role }}</td>
<td>{{ vlan.description }}</td>
</tr>

View File

@ -48,6 +48,11 @@
<td>Configured VLAN name</td>
<td>Cameras</td>
</tr>
<tr>
<td>Tenant</td>
<td>Name of tenant (optional)</td>
<td>Internal</td>
</tr>
<tr>
<td>Status</td>
<td>Current status</td>
@ -66,7 +71,7 @@
</tbody>
</table>
<h4>Example</h4>
<pre>LAS2,Backend Network,1400,Cameras,Active,Security,Security team only</pre>
<pre>LAS2,Backend Network,1400,Cameras,Internal,Active,Security,Security team only</pre>
</div>
</div>
{% endblock %}

View File

@ -30,6 +30,16 @@
<td>Route Distinguisher</td>
<td>{{ vrf.rd }}</td>
</tr>
<tr>
<td>Tenant</td>
<td>
{% if vrf.tenant %}
<a href="{{ vrf.tenant.get_absolute_url }}">{{ vrf.tenant }}</a>
{% else %}
<span class="text-muted">None</span>
{% endif %}
</td>
</tr>
<tr>
<td>Enforce Uniqueness</td>
<td>

View File

@ -8,6 +8,7 @@
<tr>
<td><a href="{% url 'ipam:vrf' pk=vrf.pk %}">{{ vrf.name }}</a></td>
<td>{{ vrf.rd }}</td>
<td>{{ vrf.tenant }}</td>
<td>{{ vrf.description }}</td>
</tr>
{% endfor %}

View File

@ -38,6 +38,11 @@
<td>Route distinguisher</td>
<td>65000:123456</td>
</tr>
<tr>
<td>Tenant</td>
<td>Name of tenant (optional)</td>
<td>ABC01</td>
</tr>
<tr>
<td>Enforce uniqueness</td>
<td>Prevent duplicate prefixes/IP addresses</td>
@ -51,7 +56,7 @@
</tbody>
</table>
<h4>Example</h4>
<pre>Customer_ABC,65000:123456,True,Native VRF for customer ABC</pre>
<pre>Customer_ABC,65000:123456,ABC01,True,Native VRF for customer ABC</pre>
</div>
</div>
{% endblock %}

View File

@ -41,6 +41,7 @@
</form>
</div>
</div>
{% include 'inc/filter_panel.html' %}
</div>
</div>
{% endblock %}

View File

@ -9,6 +9,7 @@
{% render_field form.name %}
{% render_field form.slug %}
{% render_field form.group %}
{% render_field form.description %}
</div>
</div>
<div class="panel panel-default">

View File

@ -46,4 +46,5 @@ class Tenant(CreatedUpdatedModel):
self.name,
self.slug,
self.group.name,
self.description,
])