diff --git a/netbox/ipam/admin.py b/netbox/ipam/admin.py index 8668aeb77..5ab79b92d 100644 --- a/netbox/ipam/admin.py +++ b/netbox/ipam/admin.py @@ -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') diff --git a/netbox/ipam/filters.py b/netbox/ipam/filters.py index ef87bbaa1..badd9a08f 100644 --- a/netbox/ipam/filters.py +++ b/netbox/ipam/filters.py @@ -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(), diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index c2286122c..9c8f4a4b8 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -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})) diff --git a/netbox/ipam/migrations/0006_vrf_vlan_add_tenant.py b/netbox/ipam/migrations/0006_vrf_vlan_add_tenant.py new file mode 100644 index 000000000..8d519261d --- /dev/null +++ b/netbox/ipam/migrations/0006_vrf_vlan_add_tenant.py @@ -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'), + ), + ] diff --git a/netbox/ipam/models.py b/netbox/ipam/models.py index 510d8410a..d28e4eb12 100644 --- a/netbox/ipam/models.py +++ b/netbox/ipam/models.py @@ -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, diff --git a/netbox/ipam/tables.py b/netbox/ipam/tables.py index 602462a2e..796854105 100644 --- a/netbox/ipam/tables.py +++ b/netbox/ipam/tables.py @@ -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') diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index 193adf273..90afcfdd2 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -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] diff --git a/netbox/templates/ipam/vlan.html b/netbox/templates/ipam/vlan.html index 8adbcd1be..cf525ff58 100644 --- a/netbox/templates/ipam/vlan.html +++ b/netbox/templates/ipam/vlan.html @@ -70,10 +70,10 @@ {{ vlan.name }} - Description + Tenant - {% if vlan.description %} - {{ vlan.description }} + {% if vlan.tenant %} + {{ vlan.tenant }} {% else %} None {% endif %} @@ -89,6 +89,16 @@ Role {{ vlan.role }} + + Description + + {% if vlan.description %} + {{ vlan.description }} + {% else %} + None + {% endif %} + + Created {{ vlan.created }} diff --git a/netbox/templates/ipam/vlan_bulk_edit.html b/netbox/templates/ipam/vlan_bulk_edit.html index 67f98be08..38b01fe24 100644 --- a/netbox/templates/ipam/vlan_bulk_edit.html +++ b/netbox/templates/ipam/vlan_bulk_edit.html @@ -9,7 +9,8 @@ {{ vlan.vid }} {{ vlan.name }} {{ vlan.site }} - {{ vlan.status }} + {{ vlan.tenant }} + {{ vlan.get_status_display }} {{ vlan.role }} {{ vlan.description }} diff --git a/netbox/templates/ipam/vlan_import.html b/netbox/templates/ipam/vlan_import.html index affee3c18..2ba22feb7 100644 --- a/netbox/templates/ipam/vlan_import.html +++ b/netbox/templates/ipam/vlan_import.html @@ -48,6 +48,11 @@ Configured VLAN name Cameras + + Tenant + Name of tenant (optional) + Internal + Status Current status @@ -66,7 +71,7 @@

Example

-
LAS2,Backend Network,1400,Cameras,Active,Security,Security team only
+
LAS2,Backend Network,1400,Cameras,Internal,Active,Security,Security team only
{% endblock %} diff --git a/netbox/templates/ipam/vrf.html b/netbox/templates/ipam/vrf.html index e3ce30c3b..948ee1d89 100644 --- a/netbox/templates/ipam/vrf.html +++ b/netbox/templates/ipam/vrf.html @@ -30,6 +30,16 @@ Route Distinguisher {{ vrf.rd }} + + Tenant + + {% if vrf.tenant %} + {{ vrf.tenant }} + {% else %} + None + {% endif %} + + Enforce Uniqueness diff --git a/netbox/templates/ipam/vrf_bulk_edit.html b/netbox/templates/ipam/vrf_bulk_edit.html index 0c6d83be6..344fe0905 100644 --- a/netbox/templates/ipam/vrf_bulk_edit.html +++ b/netbox/templates/ipam/vrf_bulk_edit.html @@ -8,6 +8,7 @@ {{ vrf.name }} {{ vrf.rd }} + {{ vrf.tenant }} {{ vrf.description }} {% endfor %} diff --git a/netbox/templates/ipam/vrf_import.html b/netbox/templates/ipam/vrf_import.html index ce16181c4..cbdee420d 100644 --- a/netbox/templates/ipam/vrf_import.html +++ b/netbox/templates/ipam/vrf_import.html @@ -38,6 +38,11 @@ Route distinguisher 65000:123456 + + Tenant + Name of tenant (optional) + ABC01 + Enforce uniqueness Prevent duplicate prefixes/IP addresses @@ -51,7 +56,7 @@

Example

-
Customer_ABC,65000:123456,True,Native VRF for customer ABC
+
Customer_ABC,65000:123456,ABC01,True,Native VRF for customer ABC
{% endblock %} diff --git a/netbox/templates/ipam/vrf_list.html b/netbox/templates/ipam/vrf_list.html index 641aae1d6..eaa03e461 100644 --- a/netbox/templates/ipam/vrf_list.html +++ b/netbox/templates/ipam/vrf_list.html @@ -41,6 +41,7 @@ + {% include 'inc/filter_panel.html' %} {% endblock %} diff --git a/netbox/templates/tenancy/tenant_edit.html b/netbox/templates/tenancy/tenant_edit.html index cffb29510..3616e5966 100644 --- a/netbox/templates/tenancy/tenant_edit.html +++ b/netbox/templates/tenancy/tenant_edit.html @@ -9,6 +9,7 @@ {% render_field form.name %} {% render_field form.slug %} {% render_field form.group %} + {% render_field form.description %}
diff --git a/netbox/tenancy/models.py b/netbox/tenancy/models.py index 748a68ea5..72bc92cae 100644 --- a/netbox/tenancy/models.py +++ b/netbox/tenancy/models.py @@ -46,4 +46,5 @@ class Tenant(CreatedUpdatedModel): self.name, self.slug, self.group.name, + self.description, ])