Browse Source

Implemented tenancy for VRFs and VLANs

Jeremy Stretch 8 years ago
parent
commit
2abee211a2

+ 9 - 4
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')

+ 23 - 0
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(),

+ 28 - 4
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}))

+ 27 - 0
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'),
+        ),
+    ]

+ 8 - 2
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,

+ 4 - 2
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')

+ 4 - 3
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]
 

+ 13 - 3
netbox/templates/ipam/vlan.html

@@ -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 %}
@@ -90,6 +90,16 @@
                     <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>
                 </tr>

+ 2 - 1
netbox/templates/ipam/vlan_bulk_edit.html

@@ -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>

+ 6 - 1
netbox/templates/ipam/vlan_import.html

@@ -49,6 +49,11 @@
 					<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>
 					<td>Active</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 %}

+ 10 - 0
netbox/templates/ipam/vrf.html

@@ -31,6 +31,16 @@
                     <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>
                         {% if vrf.enforce_unique %}

+ 1 - 0
netbox/templates/ipam/vrf_bulk_edit.html

@@ -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 %}

+ 6 - 1
netbox/templates/ipam/vrf_import.html

@@ -39,6 +39,11 @@
 					<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>
 					<td>True</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 %}

+ 1 - 0
netbox/templates/ipam/vrf_list.html

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

+ 1 - 0
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 %}
         </div>
     </div>
     <div class="panel panel-default">

+ 1 - 0
netbox/tenancy/models.py

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