Browse Source

Merge pull request #408 from digitalocean/develop

Release v1.4.0
Jeremy Stretch 9 years ago
parent
commit
9889e120bd
100 changed files with 1744 additions and 641 deletions
  1. 9 0
      docs/data-model/tenancy.md
  2. 12 0
      docs/installation/netbox.md
  3. 1 0
      mkdocs.yml
  4. 4 3
      netbox/circuits/admin.py
  5. 4 2
      netbox/circuits/api/serializers.py
  6. 2 2
      netbox/circuits/api/views.py
  7. 20 4
      netbox/circuits/filters.py
  8. 26 0
      netbox/circuits/fixtures/initial_data.json
  9. 15 3
      netbox/circuits/forms.py
  10. 22 0
      netbox/circuits/migrations/0004_circuit_add_tenant.py
  11. 3 0
      netbox/circuits/models.py
  12. 9 6
      netbox/circuits/tables.py
  13. 8 1
      netbox/circuits/views.py
  14. 10 6
      netbox/dcim/api/serializers.py
  15. 11 9
      netbox/dcim/api/views.py
  16. 40 7
      netbox/dcim/filters.py
  17. 201 0
      netbox/dcim/fixtures/initial_data.json
  18. 60 12
      netbox/dcim/forms.py
  19. 32 0
      netbox/dcim/migrations/0012_site_rack_device_add_tenant.py
  20. 7 0
      netbox/dcim/models.py
  21. 34 24
      netbox/dcim/tables.py
  22. 6 0
      netbox/dcim/tests/test_apis.py
  23. 1 0
      netbox/dcim/urls.py
  24. 42 10
      netbox/dcim/views.py
  25. 11 6
      netbox/ipam/admin.py
  26. 20 6
      netbox/ipam/api/serializers.py
  27. 10 10
      netbox/ipam/api/views.py
  28. 123 8
      netbox/ipam/filters.py
  29. 125 0
      netbox/ipam/fixtures/initial_data.json
  30. 70 25
      netbox/ipam/forms.py
  31. 27 0
      netbox/ipam/migrations/0006_vrf_vlan_add_tenant.py
  32. 27 0
      netbox/ipam/migrations/0007_prefix_ipaddress_add_tenant.py
  33. 10 2
      netbox/ipam/models.py
  34. 38 18
      netbox/ipam/tables.py
  35. 30 16
      netbox/ipam/views.py
  36. 2 1
      netbox/netbox/settings.py
  37. 2 0
      netbox/netbox/urls.py
  38. 7 2
      netbox/netbox/views.py
  39. 12 0
      netbox/secrets/filters.py
  40. 42 0
      netbox/secrets/fixtures/initial_data.json
  41. 5 4
      netbox/secrets/tables.py
  42. 1 1
      netbox/templates/500.html
  43. 79 70
      netbox/templates/_base.html
  44. 80 29
      netbox/templates/circuits/circuit.html
  45. 8 2
      netbox/templates/circuits/circuit_edit.html
  46. 6 1
      netbox/templates/circuits/circuit_import.html
  47. 2 18
      netbox/templates/circuits/circuit_list.html
  48. 1 1
      netbox/templates/circuits/circuittype_list.html
  49. 55 13
      netbox/templates/circuits/provider.html
  50. 2 18
      netbox/templates/circuits/provider_list.html
  51. 1 1
      netbox/templates/dcim/console_connections_list.html
  52. 15 5
      netbox/templates/dcim/device.html
  53. 1 0
      netbox/templates/dcim/device_bulk_edit.html
  54. 1 0
      netbox/templates/dcim/device_edit.html
  55. 6 1
      netbox/templates/dcim/device_import.html
  56. 1 1
      netbox/templates/dcim/device_inventory.html
  57. 3 20
      netbox/templates/dcim/device_list.html
  58. 1 1
      netbox/templates/dcim/devicerole_list.html
  59. 2 2
      netbox/templates/dcim/devicetype.html
  60. 1 1
      netbox/templates/dcim/devicetype_list.html
  61. 2 2
      netbox/templates/dcim/inc/_device_header.html
  62. 6 4
      netbox/templates/dcim/inc/_interface.html
  63. 1 1
      netbox/templates/dcim/interface_connections_list.html
  64. 1 1
      netbox/templates/dcim/manufacturer_list.html
  65. 1 1
      netbox/templates/dcim/platform_list.html
  66. 1 1
      netbox/templates/dcim/power_connections_list.html
  67. 19 9
      netbox/templates/dcim/rack.html
  68. 1 0
      netbox/templates/dcim/rack_bulk_edit.html
  69. 1 0
      netbox/templates/dcim/rack_edit.html
  70. 6 1
      netbox/templates/dcim/rack_import.html
  71. 3 20
      netbox/templates/dcim/rack_list.html
  72. 1 1
      netbox/templates/dcim/rackgroup_list.html
  73. 63 47
      netbox/templates/dcim/site.html
  74. 13 0
      netbox/templates/dcim/site_bulk_edit.html
  75. 1 0
      netbox/templates/dcim/site_edit.html
  76. 6 1
      netbox/templates/dcim/site_import.html
  77. 8 21
      netbox/templates/dcim/site_list.html
  78. 39 22
      netbox/templates/home.html
  79. 1 1
      netbox/templates/import_success.html
  80. 2 2
      netbox/templates/inc/export_button.html
  81. 2 2
      netbox/templates/inc/filter_panel.html
  82. 18 0
      netbox/templates/inc/search_panel.html
  83. 18 6
      netbox/templates/ipam/aggregate.html
  84. 2 1
      netbox/templates/ipam/aggregate_list.html
  85. 8 10
      netbox/templates/ipam/inc/prefix_header.html
  86. 24 12
      netbox/templates/ipam/ipaddress.html
  87. 2 1
      netbox/templates/ipam/ipaddress_bulk_edit.html
  88. 1 0
      netbox/templates/ipam/ipaddress_edit.html
  89. 6 1
      netbox/templates/ipam/ipaddress_import.html
  90. 3 20
      netbox/templates/ipam/ipaddress_list.html
  91. 25 6
      netbox/templates/ipam/prefix.html
  92. 1 0
      netbox/templates/ipam/prefix_bulk_edit.html
  93. 6 1
      netbox/templates/ipam/prefix_import.html
  94. 3 20
      netbox/templates/ipam/prefix_list.html
  95. 1 1
      netbox/templates/ipam/rir_list.html
  96. 1 1
      netbox/templates/ipam/role_list.html
  97. 30 30
      netbox/templates/ipam/vlan.html
  98. 2 1
      netbox/templates/ipam/vlan_bulk_edit.html
  99. 6 1
      netbox/templates/ipam/vlan_import.html
  100. 3 20
      netbox/templates/ipam/vlan_list.html

+ 9 - 0
docs/data-model/tenancy.md

@@ -0,0 +1,9 @@
+NetBox supports the concept of individual tenants within its parent organization. Typically, these are used to represent individual customers or internal departments.
+
+# Tenants
+
+A tenant represents a discrete organization. Certain resources within NetBox can be assigned to a tenant. This makes it very convenient to track which resources are assigned to which customers, for instance.
+
+### Tenant Groups
+
+Tenants are grouped by type. For instance, you might create one group called "Customers" and one called "Acquisitions."

+ 12 - 0
docs/installation/netbox.md

@@ -163,6 +163,18 @@ Are you sure you want to do this?
 Type 'yes' to continue, or 'no' to cancel: yes
 Type 'yes' to continue, or 'no' to cancel: yes
 ```
 ```
 
 
+# Load Initial Data (Optional)
+
+NetBox ships with some initial data to help you get started: RIR definitions, common devices roles, etc. You can delete any seed data that you don't want to keep.
+
+!!! note
+    This step is optional. It's perfectly fine to start using NetBox without using this initial data if you'd rather create everything from scratch.
+
+```
+# ./manage.py loaddata initial_data
+Installed 43 object(s) from 4 fixture(s)
+```
+
 # Test the Application
 # Test the Application
 
 
 At this point, NetBox should be able to run. We can verify this by starting a development instance:
 At this point, NetBox should be able to run. We can verify this by starting a development instance:

+ 1 - 0
mkdocs.yml

@@ -17,6 +17,7 @@ pages:
         - 'DCIM': 'data-model/dcim.md'
         - 'DCIM': 'data-model/dcim.md'
         - 'IPAM': 'data-model/ipam.md'
         - 'IPAM': 'data-model/ipam.md'
         - 'Secrets': 'data-model/secrets.md'
         - 'Secrets': 'data-model/secrets.md'
+        - 'Tenancy': 'data-model/tenancy.md'
         - 'Extras': 'data-model/extras.md'
         - 'Extras': 'data-model/extras.md'
     - 'API Integration': 'api-integration.md'
     - 'API Integration': 'api-integration.md'
 
 

+ 4 - 3
netbox/circuits/admin.py

@@ -21,10 +21,11 @@ class CircuitTypeAdmin(admin.ModelAdmin):
 
 
 @admin.register(Circuit)
 @admin.register(Circuit)
 class CircuitAdmin(admin.ModelAdmin):
 class CircuitAdmin(admin.ModelAdmin):
-    list_display = ['cid', 'provider', 'type', 'site', 'install_date', 'port_speed', 'commit_rate', 'xconnect_id']
-    list_filter = ['provider']
+    list_display = ['cid', 'provider', 'type', 'tenant', 'site', 'install_date', 'port_speed', 'commit_rate',
+                    'xconnect_id']
+    list_filter = ['provider', 'type', 'tenant']
     exclude = ['interface']
     exclude = ['interface']
 
 
     def get_queryset(self, request):
     def get_queryset(self, request):
         qs = super(CircuitAdmin, self).get_queryset(request)
         qs = super(CircuitAdmin, self).get_queryset(request)
-        return qs.select_related('provider', 'type', 'site')
+        return qs.select_related('provider', 'type', 'tenant', 'site')

+ 4 - 2
netbox/circuits/api/serializers.py

@@ -2,6 +2,7 @@ from rest_framework import serializers
 
 
 from circuits.models import Provider, CircuitType, Circuit
 from circuits.models import Provider, CircuitType, Circuit
 from dcim.api.serializers import SiteNestedSerializer, InterfaceNestedSerializer
 from dcim.api.serializers import SiteNestedSerializer, InterfaceNestedSerializer
+from tenancy.api.serializers import TenantNestedSerializer
 
 
 
 
 #
 #
@@ -45,13 +46,14 @@ class CircuitTypeNestedSerializer(CircuitTypeSerializer):
 class CircuitSerializer(serializers.ModelSerializer):
 class CircuitSerializer(serializers.ModelSerializer):
     provider = ProviderNestedSerializer()
     provider = ProviderNestedSerializer()
     type = CircuitTypeNestedSerializer()
     type = CircuitTypeNestedSerializer()
+    tenant = TenantNestedSerializer()
     site = SiteNestedSerializer()
     site = SiteNestedSerializer()
     interface = InterfaceNestedSerializer()
     interface = InterfaceNestedSerializer()
 
 
     class Meta:
     class Meta:
         model = Circuit
         model = Circuit
-        fields = ['id', 'cid', 'provider', 'type', 'site', 'interface', 'install_date', 'port_speed', 'commit_rate',
-                  'xconnect_id', 'comments']
+        fields = ['id', 'cid', 'provider', 'type', 'tenant', 'site', 'interface', 'install_date', 'port_speed',
+                  'commit_rate', 'xconnect_id', 'comments']
 
 
 
 
 class CircuitNestedSerializer(CircuitSerializer):
 class CircuitNestedSerializer(CircuitSerializer):

+ 2 - 2
netbox/circuits/api/views.py

@@ -42,7 +42,7 @@ class CircuitListView(generics.ListAPIView):
     """
     """
     List circuits (filterable)
     List circuits (filterable)
     """
     """
-    queryset = Circuit.objects.select_related('type', 'provider', 'site', 'interface__device')
+    queryset = Circuit.objects.select_related('type', 'tenant', 'provider', 'site', 'interface__device')
     serializer_class = serializers.CircuitSerializer
     serializer_class = serializers.CircuitSerializer
     filter_class = CircuitFilter
     filter_class = CircuitFilter
 
 
@@ -51,5 +51,5 @@ class CircuitDetailView(generics.RetrieveAPIView):
     """
     """
     Retrieve a single circuit
     Retrieve a single circuit
     """
     """
-    queryset = Circuit.objects.select_related('type', 'provider', 'site', 'interface__device')
+    queryset = Circuit.objects.select_related('type', 'tenant', 'provider', 'site', 'interface__device')
     serializer_class = serializers.CircuitSerializer
     serializer_class = serializers.CircuitSerializer

+ 20 - 4
netbox/circuits/filters.py

@@ -3,6 +3,7 @@ import django_filters
 from django.db.models import Q
 from django.db.models import Q
 
 
 from dcim.models import Site
 from dcim.models import Site
+from tenancy.models import Tenant
 from .models import Provider, Circuit, CircuitType
 from .models import Provider, Circuit, CircuitType
 
 
 
 
@@ -28,10 +29,10 @@ class ProviderFilter(django_filters.FilterSet):
         fields = ['q', 'name', 'account', 'asn']
         fields = ['q', 'name', 'account', 'asn']
 
 
     def search(self, queryset, value):
     def search(self, queryset, value):
-        value = value.strip()
         return queryset.filter(
         return queryset.filter(
             Q(name__icontains=value) |
             Q(name__icontains=value) |
-            Q(account__icontains=value)
+            Q(account__icontains=value) |
+            Q(comments__icontains=value)
         )
         )
 
 
 
 
@@ -62,6 +63,17 @@ class CircuitFilter(django_filters.FilterSet):
         to_field_name='slug',
         to_field_name='slug',
         label='Circuit type (slug)',
         label='Circuit type (slug)',
     )
     )
+    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)',
+    )
     site_id = django_filters.ModelMultipleChoiceFilter(
     site_id = django_filters.ModelMultipleChoiceFilter(
         name='site',
         name='site',
         queryset=Site.objects.all(),
         queryset=Site.objects.all(),
@@ -79,5 +91,9 @@ class CircuitFilter(django_filters.FilterSet):
         fields = ['q', 'provider_id', 'provider', 'type_id', 'type', 'site_id', 'site', 'interface', 'install_date']
         fields = ['q', 'provider_id', 'provider', 'type_id', 'type', 'site_id', 'site', 'interface', 'install_date']
 
 
     def search(self, queryset, value):
     def search(self, queryset, value):
-        value = value.strip()
-        return queryset.filter(cid__icontains=value)
+        return queryset.filter(
+            Q(cid__icontains=value) |
+            Q(xconnect_id__icontains=value) |
+            Q(pp_info__icontains=value) |
+            Q(comments__icontains=value)
+        )

+ 26 - 0
netbox/circuits/fixtures/initial_data.json

@@ -0,0 +1,26 @@
+[
+{
+    "model": "circuits.circuittype",
+    "pk": 1,
+    "fields": {
+        "name": "Internet",
+        "slug": "internet"
+    }
+},
+{
+    "model": "circuits.circuittype",
+    "pk": 2,
+    "fields": {
+        "name": "Private WAN",
+        "slug": "private-wan"
+    }
+},
+{
+    "model": "circuits.circuittype",
+    "pk": 3,
+    "fields": {
+        "name": "Out-of-Band",
+        "slug": "out-of-band"
+    }
+}
+]

+ 15 - 3
netbox/circuits/forms.py

@@ -2,6 +2,8 @@ from django import forms
 from django.db.models import Count
 from django.db.models import Count
 
 
 from dcim.models import Site, Device, Interface, Rack, IFACE_FF_VIRTUAL
 from dcim.models import Site, Device, Interface, Rack, IFACE_FF_VIRTUAL
+from tenancy.forms import bulkedit_tenant_choices
+from tenancy.models import Tenant
 from utilities.forms import (
 from utilities.forms import (
     APISelect, BootstrapMixin, BulkImportForm, CommentField, CSVDataField, Livesearch, SmallTextarea, SlugField,
     APISelect, BootstrapMixin, BulkImportForm, CommentField, CSVDataField, Livesearch, SmallTextarea, SlugField,
 )
 )
@@ -99,7 +101,7 @@ class CircuitForm(forms.ModelForm, BootstrapMixin):
     class Meta:
     class Meta:
         model = Circuit
         model = Circuit
         fields = [
         fields = [
-            'cid', 'type', 'provider', 'site', 'rack', 'device', 'livesearch', 'interface', 'install_date',
+            'cid', 'type', 'provider', 'tenant', 'site', 'rack', 'device', 'livesearch', 'interface', 'install_date',
             'port_speed', 'commit_rate', 'xconnect_id', 'pp_info', 'comments'
             'port_speed', 'commit_rate', 'xconnect_id', 'pp_info', 'comments'
         ]
         ]
         help_texts = {
         help_texts = {
@@ -160,13 +162,15 @@ class CircuitFromCSVForm(forms.ModelForm):
                                       error_messages={'invalid_choice': 'Provider not found.'})
                                       error_messages={'invalid_choice': 'Provider not found.'})
     type = forms.ModelChoiceField(CircuitType.objects.all(), to_field_name='name',
     type = forms.ModelChoiceField(CircuitType.objects.all(), to_field_name='name',
                                   error_messages={'invalid_choice': 'Invalid circuit type.'})
                                   error_messages={'invalid_choice': 'Invalid circuit type.'})
+    tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
+                                    error_messages={'invalid_choice': 'Tenant not found.'})
     site = forms.ModelChoiceField(Site.objects.all(), to_field_name='name',
     site = forms.ModelChoiceField(Site.objects.all(), to_field_name='name',
                                   error_messages={'invalid_choice': 'Site not found.'})
                                   error_messages={'invalid_choice': 'Site not found.'})
 
 
     class Meta:
     class Meta:
         model = Circuit
         model = Circuit
-        fields = ['cid', 'provider', 'type', 'site', 'install_date', 'port_speed', 'commit_rate', 'xconnect_id',
-                  'pp_info']
+        fields = ['cid', 'provider', 'type', 'tenant', 'site', 'install_date', 'port_speed', 'commit_rate',
+                  'xconnect_id', 'pp_info']
 
 
 
 
 class CircuitImportForm(BulkImportForm, BootstrapMixin):
 class CircuitImportForm(BulkImportForm, BootstrapMixin):
@@ -177,6 +181,7 @@ class CircuitBulkEditForm(forms.Form, BootstrapMixin):
     pk = forms.ModelMultipleChoiceField(queryset=Circuit.objects.all(), widget=forms.MultipleHiddenInput)
     pk = forms.ModelMultipleChoiceField(queryset=Circuit.objects.all(), widget=forms.MultipleHiddenInput)
     type = forms.ModelChoiceField(queryset=CircuitType.objects.all(), required=False)
     type = forms.ModelChoiceField(queryset=CircuitType.objects.all(), required=False)
     provider = forms.ModelChoiceField(queryset=Provider.objects.all(), required=False)
     provider = forms.ModelChoiceField(queryset=Provider.objects.all(), required=False)
+    tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant')
     port_speed = forms.IntegerField(required=False, label='Port speed (Kbps)')
     port_speed = forms.IntegerField(required=False, label='Port speed (Kbps)')
     commit_rate = forms.IntegerField(required=False, label='Commit rate (Kbps)')
     commit_rate = forms.IntegerField(required=False, label='Commit rate (Kbps)')
     comments = CommentField()
     comments = CommentField()
@@ -192,6 +197,11 @@ def circuit_provider_choices():
     return [(p.slug, u'{} ({})'.format(p.name, p.circuit_count)) for p in provider_choices]
     return [(p.slug, u'{} ({})'.format(p.name, p.circuit_count)) for p in provider_choices]
 
 
 
 
+def circuit_tenant_choices():
+    tenant_choices = Tenant.objects.annotate(circuit_count=Count('circuits'))
+    return [(t.slug, u'{} ({})'.format(t.name, t.circuit_count)) for t in tenant_choices]
+
+
 def circuit_site_choices():
 def circuit_site_choices():
     site_choices = Site.objects.annotate(circuit_count=Count('circuits'))
     site_choices = Site.objects.annotate(circuit_count=Count('circuits'))
     return [(s.slug, u'{} ({})'.format(s.name, s.circuit_count)) for s in site_choices]
     return [(s.slug, u'{} ({})'.format(s.name, s.circuit_count)) for s in site_choices]
@@ -201,5 +211,7 @@ class CircuitFilterForm(forms.Form, BootstrapMixin):
     type = forms.MultipleChoiceField(required=False, choices=circuit_type_choices)
     type = forms.MultipleChoiceField(required=False, choices=circuit_type_choices)
     provider = forms.MultipleChoiceField(required=False, choices=circuit_provider_choices,
     provider = forms.MultipleChoiceField(required=False, choices=circuit_provider_choices,
                                          widget=forms.SelectMultiple(attrs={'size': 8}))
                                          widget=forms.SelectMultiple(attrs={'size': 8}))
+    tenant = forms.MultipleChoiceField(required=False, choices=circuit_tenant_choices,
+                                       widget=forms.SelectMultiple(attrs={'size': 8}))
     site = forms.MultipleChoiceField(required=False, choices=circuit_site_choices,
     site = forms.MultipleChoiceField(required=False, choices=circuit_site_choices,
                                      widget=forms.SelectMultiple(attrs={'size': 8}))
                                      widget=forms.SelectMultiple(attrs={'size': 8}))

+ 22 - 0
netbox/circuits/migrations/0004_circuit_add_tenant.py

@@ -0,0 +1,22 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.8 on 2016-07-26 21:59
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('tenancy', '0001_initial'),
+        ('circuits', '0003_provider_32bit_asn_support'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='circuit',
+            name='tenant',
+            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='circuits', to='tenancy.Tenant'),
+        ),
+    ]

+ 3 - 0
netbox/circuits/models.py

@@ -3,6 +3,7 @@ from django.db import models
 
 
 from dcim.fields import ASNField
 from dcim.fields import ASNField
 from dcim.models import Site, Interface
 from dcim.models import Site, Interface
+from tenancy.models import Tenant
 from utilities.models import CreatedUpdatedModel
 from utilities.models import CreatedUpdatedModel
 
 
 
 
@@ -66,6 +67,7 @@ class Circuit(CreatedUpdatedModel):
     cid = models.CharField(max_length=50, verbose_name='Circuit ID')
     cid = models.CharField(max_length=50, verbose_name='Circuit ID')
     provider = models.ForeignKey('Provider', related_name='circuits', on_delete=models.PROTECT)
     provider = models.ForeignKey('Provider', related_name='circuits', on_delete=models.PROTECT)
     type = models.ForeignKey('CircuitType', related_name='circuits', on_delete=models.PROTECT)
     type = models.ForeignKey('CircuitType', related_name='circuits', on_delete=models.PROTECT)
+    tenant = models.ForeignKey(Tenant, related_name='circuits', blank=True, null=True, on_delete=models.PROTECT)
     site = models.ForeignKey(Site, related_name='circuits', on_delete=models.PROTECT)
     site = models.ForeignKey(Site, related_name='circuits', on_delete=models.PROTECT)
     interface = models.OneToOneField(Interface, related_name='circuit', blank=True, null=True)
     interface = models.OneToOneField(Interface, related_name='circuit', blank=True, null=True)
     install_date = models.DateField(blank=True, null=True, verbose_name='Date installed')
     install_date = models.DateField(blank=True, null=True, verbose_name='Date installed')
@@ -90,6 +92,7 @@ class Circuit(CreatedUpdatedModel):
             self.cid,
             self.cid,
             self.provider.name,
             self.provider.name,
             self.type.name,
             self.type.name,
+            self.tenant.name if self.tenant else '',
             self.site.name,
             self.site.name,
             self.install_date.isoformat() if self.install_date else '',
             self.install_date.isoformat() if self.install_date else '',
             str(self.port_speed),
             str(self.port_speed),

+ 9 - 6
netbox/circuits/tables.py

@@ -6,9 +6,9 @@ from utilities.tables import BaseTable, ToggleColumn
 from .models import Circuit, CircuitType, Provider
 from .models import Circuit, CircuitType, Provider
 
 
 
 
-CIRCUITTYPE_EDIT_LINK = """
+CIRCUITTYPE_ACTIONS = """
 {% if perms.circuit.change_circuittype %}
 {% if perms.circuit.change_circuittype %}
-    <a href="{% url 'circuits:circuittype_edit' slug=record.slug %}">Edit</a>
+    <a href="{% url 'circuits:circuittype_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
 {% endif %}
 {% endif %}
 """
 """
 
 
@@ -21,11 +21,12 @@ class ProviderTable(BaseTable):
     pk = ToggleColumn()
     pk = ToggleColumn()
     name = tables.LinkColumn('circuits:provider', args=[Accessor('slug')], verbose_name='Name')
     name = tables.LinkColumn('circuits:provider', args=[Accessor('slug')], verbose_name='Name')
     asn = tables.Column(verbose_name='ASN')
     asn = tables.Column(verbose_name='ASN')
+    account = tables.Column(verbose_name='Account')
     circuit_count = tables.Column(accessor=Accessor('count_circuits'), verbose_name='Circuits')
     circuit_count = tables.Column(accessor=Accessor('count_circuits'), verbose_name='Circuits')
 
 
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):
         model = Provider
         model = Provider
-        fields = ('pk', 'name', 'asn', 'circuit_count')
+        fields = ('pk', 'name', 'asn', 'account', 'circuit_count')
 
 
 
 
 #
 #
@@ -37,11 +38,12 @@ class CircuitTypeTable(BaseTable):
     name = tables.LinkColumn(verbose_name='Name')
     name = tables.LinkColumn(verbose_name='Name')
     circuit_count = tables.Column(verbose_name='Circuits')
     circuit_count = tables.Column(verbose_name='Circuits')
     slug = tables.Column(verbose_name='Slug')
     slug = tables.Column(verbose_name='Slug')
-    edit = tables.TemplateColumn(template_code=CIRCUITTYPE_EDIT_LINK, verbose_name='')
+    actions = tables.TemplateColumn(template_code=CIRCUITTYPE_ACTIONS, attrs={'td': {'class': 'text-right'}},
+                                    verbose_name='')
 
 
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):
         model = CircuitType
         model = CircuitType
-        fields = ('pk', 'name', 'circuit_count', 'slug', 'edit')
+        fields = ('pk', 'name', 'circuit_count', 'slug', 'actions')
 
 
 
 
 #
 #
@@ -53,10 +55,11 @@ class CircuitTable(BaseTable):
     cid = tables.LinkColumn('circuits:circuit', args=[Accessor('pk')], verbose_name='ID')
     cid = tables.LinkColumn('circuits:circuit', args=[Accessor('pk')], verbose_name='ID')
     type = tables.Column(verbose_name='Type')
     type = tables.Column(verbose_name='Type')
     provider = tables.LinkColumn('circuits:provider', args=[Accessor('provider.slug')], verbose_name='Provider')
     provider = tables.LinkColumn('circuits:provider', args=[Accessor('provider.slug')], verbose_name='Provider')
+    tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
     site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
     site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
     port_speed_human = tables.Column(verbose_name='Port Speed')
     port_speed_human = tables.Column(verbose_name='Port Speed')
     commit_rate_human = tables.Column(verbose_name='Commit Rate')
     commit_rate_human = tables.Column(verbose_name='Commit Rate')
 
 
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):
         model = Circuit
         model = Circuit
-        fields = ('pk', 'cid', 'type', 'provider', 'site', 'port_speed_human', 'commit_rate_human')
+        fields = ('pk', 'cid', 'type', 'provider', 'tenant', 'site', 'port_speed_human', 'commit_rate_human')

+ 8 - 1
netbox/circuits/views.py

@@ -2,6 +2,7 @@ from django.contrib.auth.mixins import PermissionRequiredMixin
 from django.db.models import Count
 from django.db.models import Count
 from django.shortcuts import get_object_or_404, render
 from django.shortcuts import get_object_or_404, render
 
 
+from extras.models import Graph, GRAPH_TYPE_PROVIDER
 from utilities.views import (
 from utilities.views import (
     BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
     BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
 )
 )
@@ -27,10 +28,12 @@ def provider(request, slug):
 
 
     provider = get_object_or_404(Provider, slug=slug)
     provider = get_object_or_404(Provider, slug=slug)
     circuits = Circuit.objects.filter(provider=provider).select_related('site', 'interface__device')
     circuits = Circuit.objects.filter(provider=provider).select_related('site', 'interface__device')
+    show_graphs = Graph.objects.filter(type=GRAPH_TYPE_PROVIDER).exists()
 
 
     return render(request, 'circuits/provider.html', {
     return render(request, 'circuits/provider.html', {
         'provider': provider,
         'provider': provider,
         'circuits': circuits,
         'circuits': circuits,
+        'show_graphs': show_graphs,
     })
     })
 
 
 
 
@@ -109,7 +112,7 @@ class CircuitTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 #
 #
 
 
 class CircuitListView(ObjectListView):
 class CircuitListView(ObjectListView):
-    queryset = Circuit.objects.select_related('provider', 'type', 'site')
+    queryset = Circuit.objects.select_related('provider', 'type', 'tenant', 'site')
     filter = filters.CircuitFilter
     filter = filters.CircuitFilter
     filter_form = forms.CircuitFilterForm
     filter_form = forms.CircuitFilterForm
     table = tables.CircuitTable
     table = tables.CircuitTable
@@ -159,6 +162,10 @@ class CircuitBulkEditView(PermissionRequiredMixin, BulkEditView):
     def update_objects(self, pk_list, form):
     def update_objects(self, pk_list, form):
 
 
         fields_to_update = {}
         fields_to_update = {}
+        if form.cleaned_data['tenant'] == 0:
+            fields_to_update['tenant'] = None
+        elif form.cleaned_data['tenant']:
+            fields_to_update['tenant'] = form.cleaned_data['tenant']
         for field in ['type', 'provider', 'port_speed', 'commit_rate', 'comments']:
         for field in ['type', 'provider', 'port_speed', 'commit_rate', 'comments']:
             if form.cleaned_data[field]:
             if form.cleaned_data[field]:
                 fields_to_update[field] = form.cleaned_data[field]
                 fields_to_update[field] = form.cleaned_data[field]

+ 10 - 6
netbox/dcim/api/serializers.py

@@ -6,6 +6,7 @@ from dcim.models import (
     DeviceRole, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer, Platform, PowerOutlet,
     DeviceRole, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer, Platform, PowerOutlet,
     PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RACK_FACE_FRONT, RACK_FACE_REAR, Site,
     PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RACK_FACE_FRONT, RACK_FACE_REAR, Site,
 )
 )
+from tenancy.api.serializers import TenantNestedSerializer
 
 
 
 
 #
 #
@@ -13,10 +14,11 @@ from dcim.models import (
 #
 #
 
 
 class SiteSerializer(serializers.ModelSerializer):
 class SiteSerializer(serializers.ModelSerializer):
+    tenant = TenantNestedSerializer()
 
 
     class Meta:
     class Meta:
         model = Site
         model = Site
-        fields = ['id', 'name', 'slug', 'facility', 'asn', 'physical_address', 'shipping_address', 'comments',
+        fields = ['id', 'name', 'slug', 'tenant', 'facility', 'asn', 'physical_address', 'shipping_address', 'comments',
                   'count_prefixes', 'count_vlans', 'count_racks', 'count_devices', 'count_circuits']
                   'count_prefixes', 'count_vlans', 'count_racks', 'count_devices', 'count_circuits']
 
 
 
 
@@ -52,10 +54,11 @@ class RackGroupNestedSerializer(RackGroupSerializer):
 class RackSerializer(serializers.ModelSerializer):
 class RackSerializer(serializers.ModelSerializer):
     site = SiteNestedSerializer()
     site = SiteNestedSerializer()
     group = RackGroupNestedSerializer()
     group = RackGroupNestedSerializer()
+    tenant = TenantNestedSerializer()
 
 
     class Meta:
     class Meta:
         model = Rack
         model = Rack
-        fields = ['id', 'name', 'facility_id', 'display_name', 'site', 'group', 'u_height', 'comments']
+        fields = ['id', 'name', 'facility_id', 'display_name', 'site', 'group', 'tenant', 'u_height', 'comments']
 
 
 
 
 class RackNestedSerializer(RackSerializer):
 class RackNestedSerializer(RackSerializer):
@@ -69,8 +72,8 @@ class RackDetailSerializer(RackSerializer):
     rear_units = serializers.SerializerMethodField()
     rear_units = serializers.SerializerMethodField()
 
 
     class Meta(RackSerializer.Meta):
     class Meta(RackSerializer.Meta):
-        fields = ['id', 'name', 'facility_id', 'display_name', 'site', 'group', 'u_height', 'comments', 'front_units',
-                  'rear_units']
+        fields = ['id', 'name', 'facility_id', 'display_name', 'site', 'group', 'tenant', 'u_height', 'comments',
+                  'front_units', 'rear_units']
 
 
     def get_front_units(self, obj):
     def get_front_units(self, obj):
         units = obj.get_rack_units(face=RACK_FACE_FRONT)
         units = obj.get_rack_units(face=RACK_FACE_FRONT)
@@ -218,6 +221,7 @@ class DeviceIPAddressNestedSerializer(serializers.ModelSerializer):
 class DeviceSerializer(serializers.ModelSerializer):
 class DeviceSerializer(serializers.ModelSerializer):
     device_type = DeviceTypeNestedSerializer()
     device_type = DeviceTypeNestedSerializer()
     device_role = DeviceRoleNestedSerializer()
     device_role = DeviceRoleNestedSerializer()
+    tenant = TenantNestedSerializer()
     platform = PlatformNestedSerializer()
     platform = PlatformNestedSerializer()
     rack = RackNestedSerializer()
     rack = RackNestedSerializer()
     primary_ip = DeviceIPAddressNestedSerializer()
     primary_ip = DeviceIPAddressNestedSerializer()
@@ -227,8 +231,8 @@ class DeviceSerializer(serializers.ModelSerializer):
 
 
     class Meta:
     class Meta:
         model = Device
         model = Device
-        fields = ['id', 'name', 'display_name', 'device_type', 'device_role', 'platform', 'serial', 'rack', 'position',
-                  'face', 'parent_device', 'status', 'primary_ip', 'primary_ip4', 'primary_ip6', 'comments']
+        fields = ['id', 'name', 'display_name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'rack',
+                  'position', 'face', 'parent_device', 'status', 'primary_ip', 'primary_ip4', 'primary_ip6', 'comments']
 
 
     def get_parent_device(self, obj):
     def get_parent_device(self, obj):
         try:
         try:

+ 11 - 9
netbox/dcim/api/views.py

@@ -27,7 +27,7 @@ class SiteListView(generics.ListAPIView):
     """
     """
     List all sites
     List all sites
     """
     """
-    queryset = Site.objects.all()
+    queryset = Site.objects.select_related('tenant')
     serializer_class = serializers.SiteSerializer
     serializer_class = serializers.SiteSerializer
 
 
 
 
@@ -35,7 +35,7 @@ class SiteDetailView(generics.RetrieveAPIView):
     """
     """
     Retrieve a single site
     Retrieve a single site
     """
     """
-    queryset = Site.objects.all()
+    queryset = Site.objects.select_related('tenant')
     serializer_class = serializers.SiteSerializer
     serializer_class = serializers.SiteSerializer
 
 
 
 
@@ -47,7 +47,7 @@ class RackGroupListView(generics.ListAPIView):
     """
     """
     List all rack groups
     List all rack groups
     """
     """
-    queryset = RackGroup.objects.all()
+    queryset = RackGroup.objects.select_related('site')
     serializer_class = serializers.RackGroupSerializer
     serializer_class = serializers.RackGroupSerializer
     filter_class = filters.RackGroupFilter
     filter_class = filters.RackGroupFilter
 
 
@@ -56,7 +56,7 @@ class RackGroupDetailView(generics.RetrieveAPIView):
     """
     """
     Retrieve a single rack group
     Retrieve a single rack group
     """
     """
-    queryset = RackGroup.objects.all()
+    queryset = RackGroup.objects.select_related('site')
     serializer_class = serializers.RackGroupSerializer
     serializer_class = serializers.RackGroupSerializer
 
 
 
 
@@ -68,7 +68,7 @@ class RackListView(generics.ListAPIView):
     """
     """
     List racks (filterable)
     List racks (filterable)
     """
     """
-    queryset = Rack.objects.select_related('site')
+    queryset = Rack.objects.select_related('site', 'group', 'tenant')
     serializer_class = serializers.RackSerializer
     serializer_class = serializers.RackSerializer
     filter_class = filters.RackFilter
     filter_class = filters.RackFilter
 
 
@@ -77,7 +77,7 @@ class RackDetailView(generics.RetrieveAPIView):
     """
     """
     Retrieve a single rack
     Retrieve a single rack
     """
     """
-    queryset = Rack.objects.select_related('site')
+    queryset = Rack.objects.select_related('site', 'group', 'tenant')
     serializer_class = serializers.RackDetailSerializer
     serializer_class = serializers.RackDetailSerializer
 
 
 
 
@@ -193,8 +193,9 @@ class DeviceListView(generics.ListAPIView):
     """
     """
     List devices (filterable)
     List devices (filterable)
     """
     """
-    queryset = Device.objects.select_related('device_type__manufacturer', 'device_role', 'platform', 'rack__site')\
-        .prefetch_related('primary_ip4__nat_outside', 'primary_ip6__nat_outside')
+    queryset = Device.objects.select_related('device_type__manufacturer', 'device_role', 'tenant', 'platform',
+                                             'rack__site', 'parent_bay').prefetch_related('primary_ip4__nat_outside',
+                                                                                          'primary_ip6__nat_outside')
     serializer_class = serializers.DeviceSerializer
     serializer_class = serializers.DeviceSerializer
     filter_class = filters.DeviceFilter
     filter_class = filters.DeviceFilter
     renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES + [BINDZoneRenderer, FlatJSONRenderer]
     renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES + [BINDZoneRenderer, FlatJSONRenderer]
@@ -204,7 +205,8 @@ class DeviceDetailView(generics.RetrieveAPIView):
     """
     """
     Retrieve a single device
     Retrieve a single device
     """
     """
-    queryset = Device.objects.all()
+    queryset = Device.objects.select_related('device_type__manufacturer', 'device_role', 'tenant', 'platform',
+                                             'rack__site', 'parent_bay')
     serializer_class = serializers.DeviceSerializer
     serializer_class = serializers.DeviceSerializer
 
 
 
 

+ 40 - 7
netbox/dcim/filters.py

@@ -6,6 +6,7 @@ from .models import (
     ConsolePort, ConsoleServerPort, Device, DeviceRole, DeviceType, Interface, InterfaceConnection, Manufacturer,
     ConsolePort, ConsoleServerPort, Device, DeviceRole, DeviceType, Interface, InterfaceConnection, Manufacturer,
     Platform, PowerOutlet, PowerPort, Rack, RackGroup, Site,
     Platform, PowerOutlet, PowerPort, Rack, RackGroup, Site,
 )
 )
+from tenancy.models import Tenant
 
 
 
 
 class SiteFilter(django_filters.FilterSet):
 class SiteFilter(django_filters.FilterSet):
@@ -13,17 +14,27 @@ class SiteFilter(django_filters.FilterSet):
         action='search',
         action='search',
         label='Search',
         label='Search',
     )
     )
+    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:
     class Meta:
         model = Site
         model = Site
         fields = ['q', 'name', 'facility', 'asn']
         fields = ['q', 'name', 'facility', 'asn']
 
 
     def search(self, queryset, value):
     def search(self, queryset, value):
-        value = value.strip()
         qs_filter = Q(name__icontains=value) | Q(facility__icontains=value) | Q(physical_address__icontains=value) | \
         qs_filter = Q(name__icontains=value) | Q(facility__icontains=value) | Q(physical_address__icontains=value) | \
-            Q(shipping_address__icontains=value)
+            Q(shipping_address__icontains=value) | Q(comments__icontains=value)
         try:
         try:
-            qs_filter |= Q(asn=int(value))
+            qs_filter |= Q(asn=int(value.strip()))
         except ValueError:
         except ValueError:
             pass
             pass
         return queryset.filter(qs_filter)
         return queryset.filter(qs_filter)
@@ -74,16 +85,27 @@ class RackFilter(django_filters.FilterSet):
         to_field_name='slug',
         to_field_name='slug',
         label='Group',
         label='Group',
     )
     )
+    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:
     class Meta:
         model = Rack
         model = Rack
         fields = ['q', 'site_id', 'site', 'u_height']
         fields = ['q', 'site_id', 'site', 'u_height']
 
 
     def search(self, queryset, value):
     def search(self, queryset, value):
-        value = value.strip()
         return queryset.filter(
         return queryset.filter(
             Q(name__icontains=value) |
             Q(name__icontains=value) |
-            Q(facility_id__icontains=value)
+            Q(facility_id__icontains=value) |
+            Q(comments__icontains=value)
         )
         )
 
 
 
 
@@ -143,6 +165,17 @@ class DeviceFilter(django_filters.FilterSet):
         to_field_name='slug',
         to_field_name='slug',
         label='Role (slug)',
         label='Role (slug)',
     )
     )
+    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)',
+    )
     device_type_id = django_filters.ModelMultipleChoiceFilter(
     device_type_id = django_filters.ModelMultipleChoiceFilter(
         name='device_type',
         name='device_type',
         queryset=DeviceType.objects.all(),
         queryset=DeviceType.objects.all(),
@@ -200,11 +233,11 @@ class DeviceFilter(django_filters.FilterSet):
                   'is_network_device']
                   'is_network_device']
 
 
     def search(self, queryset, value):
     def search(self, queryset, value):
-        value = value.strip()
         return queryset.filter(
         return queryset.filter(
             Q(name__icontains=value) |
             Q(name__icontains=value) |
             Q(serial__icontains=value) |
             Q(serial__icontains=value) |
-            Q(modules__serial__icontains=value)
+            Q(modules__serial__icontains=value) |
+            Q(comments__icontains=value)
         ).distinct()
         ).distinct()
 
 
 
 

+ 201 - 0
netbox/dcim/fixtures/initial_data.json

@@ -0,0 +1,201 @@
+[
+{
+    "model": "dcim.devicerole",
+    "pk": 1,
+    "fields": {
+        "name": "Console Server",
+        "slug": "console-server",
+        "color": "teal"
+    }
+},
+{
+    "model": "dcim.devicerole",
+    "pk": 2,
+    "fields": {
+        "name": "Core Switch",
+        "slug": "core-switch",
+        "color": "blue"
+    }
+},
+{
+    "model": "dcim.devicerole",
+    "pk": 3,
+    "fields": {
+        "name": "Distribution Switch",
+        "slug": "distribution-switch",
+        "color": "blue"
+    }
+},
+{
+    "model": "dcim.devicerole",
+    "pk": 4,
+    "fields": {
+        "name": "Access Switch",
+        "slug": "access-switch",
+        "color": "blue"
+    }
+},
+{
+    "model": "dcim.devicerole",
+    "pk": 5,
+    "fields": {
+        "name": "Management Switch",
+        "slug": "management-switch",
+        "color": "orange"
+    }
+},
+{
+    "model": "dcim.devicerole",
+    "pk": 6,
+    "fields": {
+        "name": "Firewall",
+        "slug": "firewall",
+        "color": "red"
+    }
+},
+{
+    "model": "dcim.devicerole",
+    "pk": 7,
+    "fields": {
+        "name": "Router",
+        "slug": "router",
+        "color": "purple"
+    }
+},
+{
+    "model": "dcim.devicerole",
+    "pk": 8,
+    "fields": {
+        "name": "Server",
+        "slug": "server",
+        "color": "medium_gray"
+    }
+},
+{
+    "model": "dcim.devicerole",
+    "pk": 9,
+    "fields": {
+        "name": "PDU",
+        "slug": "pdu",
+        "color": "dark_gray"
+    }
+},
+{
+    "model": "dcim.manufacturer",
+    "pk": 1,
+    "fields": {
+        "name": "APC",
+        "slug": "apc"
+    }
+},
+{
+    "model": "dcim.manufacturer",
+    "pk": 2,
+    "fields": {
+        "name": "Cisco",
+        "slug": "cisco"
+    }
+},
+{
+    "model": "dcim.manufacturer",
+    "pk": 3,
+    "fields": {
+        "name": "Dell",
+        "slug": "dell"
+    }
+},
+{
+    "model": "dcim.manufacturer",
+    "pk": 4,
+    "fields": {
+        "name": "HP",
+        "slug": "hp"
+    }
+},
+{
+    "model": "dcim.manufacturer",
+    "pk": 5,
+    "fields": {
+        "name": "Juniper",
+        "slug": "juniper"
+    }
+},
+{
+    "model": "dcim.manufacturer",
+    "pk": 6,
+    "fields": {
+        "name": "Arista",
+        "slug": "arista"
+    }
+},
+{
+    "model": "dcim.manufacturer",
+    "pk": 7,
+    "fields": {
+        "name": "Opengear",
+        "slug": "opengear"
+    }
+},
+{
+    "model": "dcim.manufacturer",
+    "pk": 8,
+    "fields": {
+        "name": "Super Micro",
+        "slug": "super-micro"
+    }
+},
+{
+    "model": "dcim.platform",
+    "pk": 1,
+    "fields": {
+        "name": "Cisco IOS",
+        "slug": "cisco-ios",
+        "rpc_client": "cisco-ios"
+    }
+},
+{
+    "model": "dcim.platform",
+    "pk": 2,
+    "fields": {
+        "name": "Cisco NX-OS",
+        "slug": "cisco-nx-os",
+        "rpc_client": ""
+    }
+},
+{
+    "model": "dcim.platform",
+    "pk": 3,
+    "fields": {
+        "name": "Juniper Junos",
+        "slug": "juniper-junos",
+        "rpc_client": "juniper-junos"
+    }
+},
+{
+    "model": "dcim.platform",
+    "pk": 4,
+    "fields": {
+        "name": "Arista EOS",
+        "slug": "arista-eos",
+        "rpc_client": ""
+    }
+},
+{
+    "model": "dcim.platform",
+    "pk": 5,
+    "fields": {
+        "name": "Linux",
+        "slug": "linux",
+        "rpc_client": ""
+    }
+},
+{
+    "model": "dcim.platform",
+    "pk": 6,
+    "fields": {
+        "name": "Opengear",
+        "slug": "opengear",
+        "rpc_client": "opengear"
+    }
+}
+]

+ 60 - 12
netbox/dcim/forms.py

@@ -4,6 +4,8 @@ from django import forms
 from django.db.models import Count, Q
 from django.db.models import Count, Q
 
 
 from ipam.models import IPAddress
 from ipam.models import IPAddress
+from tenancy.forms import bulkedit_tenant_choices
+from tenancy.models import Tenant
 from utilities.forms import (
 from utilities.forms import (
     APISelect, BootstrapMixin, BulkImportForm, CommentField, CSVDataField, ExpandableNameField,
     APISelect, BootstrapMixin, BulkImportForm, CommentField, CSVDataField, ExpandableNameField,
     FlexibleModelChoiceField, Livesearch, SelectWithDisabled, SmallTextarea, SlugField,
     FlexibleModelChoiceField, Livesearch, SelectWithDisabled, SmallTextarea, SlugField,
@@ -38,6 +40,15 @@ def get_device_by_name_or_pk(name):
     return device
     return device
 
 
 
 
+def bulkedit_platform_choices():
+    choices = [
+        (None, '---------'),
+        (0, 'None'),
+    ]
+    choices += [(p.pk, p.name) for p in Platform.objects.all()]
+    return choices
+
+
 #
 #
 # Sites
 # Sites
 #
 #
@@ -48,7 +59,7 @@ class SiteForm(forms.ModelForm, BootstrapMixin):
 
 
     class Meta:
     class Meta:
         model = Site
         model = Site
-        fields = ['name', 'slug', 'facility', 'asn', 'physical_address', 'shipping_address', 'comments']
+        fields = ['name', 'slug', 'tenant', 'facility', 'asn', 'physical_address', 'shipping_address', 'comments']
         widgets = {
         widgets = {
             'physical_address': SmallTextarea(attrs={'rows': 3}),
             'physical_address': SmallTextarea(attrs={'rows': 3}),
             'shipping_address': SmallTextarea(attrs={'rows': 3}),
             'shipping_address': SmallTextarea(attrs={'rows': 3}),
@@ -63,16 +74,33 @@ class SiteForm(forms.ModelForm, BootstrapMixin):
 
 
 
 
 class SiteFromCSVForm(forms.ModelForm):
 class SiteFromCSVForm(forms.ModelForm):
+    tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
+                                    error_messages={'invalid_choice': 'Tenant not found.'})
 
 
     class Meta:
     class Meta:
         model = Site
         model = Site
-        fields = ['name', 'slug', 'facility', 'asn']
+        fields = ['name', 'slug', 'tenant', 'facility', 'asn']
 
 
 
 
 class SiteImportForm(BulkImportForm, BootstrapMixin):
 class SiteImportForm(BulkImportForm, BootstrapMixin):
     csv = CSVDataField(csv_form=SiteFromCSVForm)
     csv = CSVDataField(csv_form=SiteFromCSVForm)
 
 
 
 
+class SiteBulkEditForm(forms.Form, BootstrapMixin):
+    pk = forms.ModelMultipleChoiceField(queryset=Site.objects.all(), widget=forms.MultipleHiddenInput)
+    tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant')
+
+
+def site_tenant_choices():
+    tenant_choices = Tenant.objects.annotate(site_count=Count('sites'))
+    return [(t.slug, u'{} ({})'.format(t.name, t.site_count)) for t in tenant_choices]
+
+
+class SiteFilterForm(forms.Form, BootstrapMixin):
+    tenant = forms.MultipleChoiceField(required=False, choices=site_tenant_choices,
+                                       widget=forms.SelectMultiple(attrs={'size': 8}))
+
+
 #
 #
 # Rack groups
 # Rack groups
 #
 #
@@ -107,7 +135,7 @@ class RackForm(forms.ModelForm, BootstrapMixin):
 
 
     class Meta:
     class Meta:
         model = Rack
         model = Rack
-        fields = ['site', 'group', 'name', 'facility_id', 'u_height', 'comments']
+        fields = ['site', 'group', 'name', 'facility_id', 'tenant', 'u_height', 'comments']
         help_texts = {
         help_texts = {
             'site': "The site at which the rack exists",
             'site': "The site at which the rack exists",
             'name': "Organizational rack name",
             'name': "Organizational rack name",
@@ -135,10 +163,12 @@ class RackFromCSVForm(forms.ModelForm):
     site = forms.ModelChoiceField(queryset=Site.objects.all(), to_field_name='name',
     site = forms.ModelChoiceField(queryset=Site.objects.all(), to_field_name='name',
                                   error_messages={'invalid_choice': 'Site not found.'})
                                   error_messages={'invalid_choice': 'Site not found.'})
     group_name = forms.CharField(required=False)
     group_name = forms.CharField(required=False)
+    tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
+                                    error_messages={'invalid_choice': 'Tenant not found.'})
 
 
     class Meta:
     class Meta:
         model = Rack
         model = Rack
-        fields = ['site', 'group_name', 'name', 'facility_id', 'u_height']
+        fields = ['site', 'group_name', 'name', 'facility_id', 'tenant', 'u_height']
 
 
     def clean(self):
     def clean(self):
 
 
@@ -161,6 +191,7 @@ class RackBulkEditForm(forms.Form, BootstrapMixin):
     pk = forms.ModelMultipleChoiceField(queryset=Rack.objects.all(), widget=forms.MultipleHiddenInput)
     pk = forms.ModelMultipleChoiceField(queryset=Rack.objects.all(), widget=forms.MultipleHiddenInput)
     site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False)
     site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False)
     group = forms.ModelChoiceField(queryset=RackGroup.objects.all(), required=False)
     group = forms.ModelChoiceField(queryset=RackGroup.objects.all(), required=False)
+    tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant')
     u_height = forms.IntegerField(required=False, label='Height (U)')
     u_height = forms.IntegerField(required=False, label='Height (U)')
     comments = CommentField()
     comments = CommentField()
 
 
@@ -175,11 +206,18 @@ def rack_group_choices():
     return [(g.pk, u'{} ({})'.format(g, g.rack_count)) for g in group_choices]
     return [(g.pk, u'{} ({})'.format(g, g.rack_count)) for g in group_choices]
 
 
 
 
+def rack_tenant_choices():
+    tenant_choices = Tenant.objects.annotate(rack_count=Count('racks'))
+    return [(t.slug, u'{} ({})'.format(t.name, t.rack_count)) for t in tenant_choices]
+
+
 class RackFilterForm(forms.Form, BootstrapMixin):
 class RackFilterForm(forms.Form, BootstrapMixin):
     site = forms.MultipleChoiceField(required=False, choices=rack_site_choices,
     site = forms.MultipleChoiceField(required=False, choices=rack_site_choices,
                                      widget=forms.SelectMultiple(attrs={'size': 8}))
                                      widget=forms.SelectMultiple(attrs={'size': 8}))
     group_id = forms.MultipleChoiceField(required=False, choices=rack_group_choices, label='Rack Group',
     group_id = forms.MultipleChoiceField(required=False, choices=rack_group_choices, label='Rack Group',
                                          widget=forms.SelectMultiple(attrs={'size': 8}))
                                          widget=forms.SelectMultiple(attrs={'size': 8}))
+    tenant = forms.MultipleChoiceField(required=False, choices=rack_tenant_choices,
+                                       widget=forms.SelectMultiple(attrs={'size': 8}))
 
 
 
 
 #
 #
@@ -203,8 +241,8 @@ class DeviceTypeForm(forms.ModelForm, BootstrapMixin):
 
 
     class Meta:
     class Meta:
         model = DeviceType
         model = DeviceType
-        fields = ['manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'is_console_server', 'is_pdu',
-                  'is_network_device', 'subdevice_role']
+        fields = ['manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'is_console_server',
+                  'is_pdu', 'is_network_device', 'subdevice_role']
 
 
 
 
 class DeviceTypeBulkEditForm(forms.Form, BootstrapMixin):
 class DeviceTypeBulkEditForm(forms.Form, BootstrapMixin):
@@ -324,7 +362,7 @@ class DeviceForm(forms.ModelForm, BootstrapMixin):
 
 
     class Meta:
     class Meta:
         model = Device
         model = Device
-        fields = ['name', 'device_role', 'device_type', 'serial', 'site', 'rack', 'position', 'face', 'status',
+        fields = ['name', 'device_role', 'tenant', 'device_type', 'serial', 'site', 'rack', 'position', 'face', 'status',
                   'platform', 'primary_ip4', 'primary_ip6', 'comments']
                   'platform', 'primary_ip4', 'primary_ip6', 'comments']
         help_texts = {
         help_texts = {
             'device_role': "The function this device serves",
             'device_role': "The function this device serves",
@@ -410,6 +448,8 @@ class DeviceForm(forms.ModelForm, BootstrapMixin):
 class BaseDeviceFromCSVForm(forms.ModelForm):
 class BaseDeviceFromCSVForm(forms.ModelForm):
     device_role = forms.ModelChoiceField(queryset=DeviceRole.objects.all(), to_field_name='name',
     device_role = forms.ModelChoiceField(queryset=DeviceRole.objects.all(), to_field_name='name',
                                          error_messages={'invalid_choice': 'Invalid device role.'})
                                          error_messages={'invalid_choice': 'Invalid device role.'})
+    tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
+                                    error_messages={'invalid_choice': 'Tenant not found.'})
     manufacturer = forms.ModelChoiceField(queryset=Manufacturer.objects.all(), to_field_name='name',
     manufacturer = forms.ModelChoiceField(queryset=Manufacturer.objects.all(), to_field_name='name',
                                           error_messages={'invalid_choice': 'Invalid manufacturer.'})
                                           error_messages={'invalid_choice': 'Invalid manufacturer.'})
     model_name = forms.CharField()
     model_name = forms.CharField()
@@ -441,8 +481,8 @@ class DeviceFromCSVForm(BaseDeviceFromCSVForm):
     face = forms.CharField(required=False)
     face = forms.CharField(required=False)
 
 
     class Meta(BaseDeviceFromCSVForm.Meta):
     class Meta(BaseDeviceFromCSVForm.Meta):
-        fields = ['name', 'device_role', 'manufacturer', 'model_name', 'platform', 'serial', 'site', 'rack_name',
-                  'position', 'face']
+        fields = ['name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'site',
+                  'rack_name', 'position', 'face']
 
 
     def clean(self):
     def clean(self):
 
 
@@ -477,7 +517,7 @@ class ChildDeviceFromCSVForm(BaseDeviceFromCSVForm):
     device_bay_name = forms.CharField(required=False)
     device_bay_name = forms.CharField(required=False)
 
 
     class Meta(BaseDeviceFromCSVForm.Meta):
     class Meta(BaseDeviceFromCSVForm.Meta):
-        fields = ['name', 'device_role', 'manufacturer', 'model_name', 'platform', 'serial', 'parent',
+        fields = ['name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'parent',
                   'device_bay_name']
                   'device_bay_name']
 
 
     def clean(self):
     def clean(self):
@@ -512,8 +552,9 @@ class DeviceBulkEditForm(forms.Form, BootstrapMixin):
     pk = forms.ModelMultipleChoiceField(queryset=Device.objects.all(), widget=forms.MultipleHiddenInput)
     pk = forms.ModelMultipleChoiceField(queryset=Device.objects.all(), widget=forms.MultipleHiddenInput)
     device_type = forms.ModelChoiceField(queryset=DeviceType.objects.all(), required=False, label='Type')
     device_type = forms.ModelChoiceField(queryset=DeviceType.objects.all(), required=False, label='Type')
     device_role = forms.ModelChoiceField(queryset=DeviceRole.objects.all(), required=False, label='Role')
     device_role = forms.ModelChoiceField(queryset=DeviceRole.objects.all(), required=False, label='Role')
-    platform = forms.ModelChoiceField(queryset=Platform.objects.all(), required=False, label='Platform')
-    platform_delete = forms.BooleanField(required=False, label='Set platform to "none"')
+    tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant')
+    platform = forms.TypedChoiceField(choices=bulkedit_platform_choices, coerce=int, required=False,
+                                      label='Platform')
     status = forms.ChoiceField(choices=FORM_STATUS_CHOICES, required=False, initial='', label='Status')
     status = forms.ChoiceField(choices=FORM_STATUS_CHOICES, required=False, initial='', label='Status')
     serial = forms.CharField(max_length=50, required=False, label='Serial Number')
     serial = forms.CharField(max_length=50, required=False, label='Serial Number')
 
 
@@ -533,6 +574,11 @@ def device_role_choices():
     return [(r.slug, u'{} ({})'.format(r.name, r.device_count)) for r in role_choices]
     return [(r.slug, u'{} ({})'.format(r.name, r.device_count)) for r in role_choices]
 
 
 
 
+def device_tenant_choices():
+    tenant_choices = Tenant.objects.annotate(device_count=Count('devices'))
+    return [(t.slug, u'{} ({})'.format(t.name, t.device_count)) for t in tenant_choices]
+
+
 def device_type_choices():
 def device_type_choices():
     type_choices = DeviceType.objects.select_related('manufacturer').annotate(device_count=Count('instances'))
     type_choices = DeviceType.objects.select_related('manufacturer').annotate(device_count=Count('instances'))
     return [(t.pk, u'{} ({})'.format(t, t.device_count)) for t in type_choices]
     return [(t.pk, u'{} ({})'.format(t, t.device_count)) for t in type_choices]
@@ -550,6 +596,8 @@ class DeviceFilterForm(forms.Form, BootstrapMixin):
                                               widget=forms.SelectMultiple(attrs={'size': 8}))
                                               widget=forms.SelectMultiple(attrs={'size': 8}))
     role = forms.MultipleChoiceField(required=False, choices=device_role_choices,
     role = forms.MultipleChoiceField(required=False, choices=device_role_choices,
                                      widget=forms.SelectMultiple(attrs={'size': 8}))
                                      widget=forms.SelectMultiple(attrs={'size': 8}))
+    tenant = forms.MultipleChoiceField(required=False, choices=device_tenant_choices,
+                                       widget=forms.SelectMultiple(attrs={'size': 8}))
     device_type_id = forms.MultipleChoiceField(required=False, choices=device_type_choices, label='Type',
     device_type_id = forms.MultipleChoiceField(required=False, choices=device_type_choices, label='Type',
                                                widget=forms.SelectMultiple(attrs={'size': 8}))
                                                widget=forms.SelectMultiple(attrs={'size': 8}))
     platform = forms.MultipleChoiceField(required=False, choices=device_platform_choices)
     platform = forms.MultipleChoiceField(required=False, choices=device_platform_choices)

+ 32 - 0
netbox/dcim/migrations/0012_site_rack_device_add_tenant.py

@@ -0,0 +1,32 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.8 on 2016-07-26 21:59
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('tenancy', '0001_initial'),
+        ('dcim', '0011_devicetype_part_number'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='device',
+            name='tenant',
+            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='devices', to='tenancy.Tenant'),
+        ),
+        migrations.AddField(
+            model_name='rack',
+            name='tenant',
+            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='racks', to='tenancy.Tenant'),
+        ),
+        migrations.AddField(
+            model_name='site',
+            name='tenant',
+            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='sites', to='tenancy.Tenant'),
+        ),
+    ]

+ 7 - 0
netbox/dcim/models.py

@@ -8,6 +8,7 @@ from django.db import models
 from django.db.models import Count, Q, ObjectDoesNotExist
 from django.db.models import Count, Q, ObjectDoesNotExist
 
 
 from extras.rpc import RPC_CLIENTS
 from extras.rpc import RPC_CLIENTS
+from tenancy.models import Tenant
 from utilities.fields import NullableCharField
 from utilities.fields import NullableCharField
 from utilities.managers import NaturalOrderByManager
 from utilities.managers import NaturalOrderByManager
 from utilities.models import CreatedUpdatedModel
 from utilities.models import CreatedUpdatedModel
@@ -152,6 +153,7 @@ class Site(CreatedUpdatedModel):
     """
     """
     name = models.CharField(max_length=50, unique=True)
     name = models.CharField(max_length=50, unique=True)
     slug = models.SlugField(unique=True)
     slug = models.SlugField(unique=True)
+    tenant = models.ForeignKey(Tenant, blank=True, null=True, related_name='sites', on_delete=models.PROTECT)
     facility = models.CharField(max_length=50, blank=True)
     facility = models.CharField(max_length=50, blank=True)
     asn = ASNField(blank=True, null=True, verbose_name='ASN')
     asn = ASNField(blank=True, null=True, verbose_name='ASN')
     physical_address = models.CharField(max_length=200, blank=True)
     physical_address = models.CharField(max_length=200, blank=True)
@@ -173,6 +175,7 @@ class Site(CreatedUpdatedModel):
         return ','.join([
         return ','.join([
             self.name,
             self.name,
             self.slug,
             self.slug,
+            self.tenant.name if self.tenant else '',
             self.facility,
             self.facility,
             str(self.asn),
             str(self.asn),
         ])
         ])
@@ -237,6 +240,7 @@ class Rack(CreatedUpdatedModel):
     facility_id = NullableCharField(max_length=30, blank=True, null=True, verbose_name='Facility ID')
     facility_id = NullableCharField(max_length=30, blank=True, null=True, verbose_name='Facility ID')
     site = models.ForeignKey('Site', related_name='racks', on_delete=models.PROTECT)
     site = models.ForeignKey('Site', related_name='racks', on_delete=models.PROTECT)
     group = models.ForeignKey('RackGroup', related_name='racks', blank=True, null=True, on_delete=models.SET_NULL)
     group = models.ForeignKey('RackGroup', related_name='racks', blank=True, null=True, on_delete=models.SET_NULL)
+    tenant = models.ForeignKey(Tenant, blank=True, null=True, related_name='racks', on_delete=models.PROTECT)
     u_height = models.PositiveSmallIntegerField(default=42, verbose_name='Height (U)')
     u_height = models.PositiveSmallIntegerField(default=42, verbose_name='Height (U)')
     comments = models.TextField(blank=True)
     comments = models.TextField(blank=True)
 
 
@@ -272,6 +276,7 @@ class Rack(CreatedUpdatedModel):
             self.group.name if self.group else '',
             self.group.name if self.group else '',
             self.name,
             self.name,
             self.facility_id or '',
             self.facility_id or '',
+            self.tenant.name if self.tenant else '',
             str(self.u_height),
             str(self.u_height),
         ])
         ])
 
 
@@ -631,6 +636,7 @@ class Device(CreatedUpdatedModel):
     """
     """
     device_type = models.ForeignKey('DeviceType', related_name='instances', on_delete=models.PROTECT)
     device_type = models.ForeignKey('DeviceType', related_name='instances', on_delete=models.PROTECT)
     device_role = models.ForeignKey('DeviceRole', related_name='devices', on_delete=models.PROTECT)
     device_role = models.ForeignKey('DeviceRole', related_name='devices', on_delete=models.PROTECT)
+    tenant = models.ForeignKey(Tenant, blank=True, null=True, related_name='devices', on_delete=models.PROTECT)
     platform = models.ForeignKey('Platform', related_name='devices', blank=True, null=True, on_delete=models.SET_NULL)
     platform = models.ForeignKey('Platform', related_name='devices', blank=True, null=True, on_delete=models.SET_NULL)
     name = NullableCharField(max_length=50, blank=True, null=True, unique=True)
     name = NullableCharField(max_length=50, blank=True, null=True, unique=True)
     serial = models.CharField(max_length=50, blank=True, verbose_name='Serial number')
     serial = models.CharField(max_length=50, blank=True, verbose_name='Serial number')
@@ -724,6 +730,7 @@ class Device(CreatedUpdatedModel):
         return ','.join([
         return ','.join([
             self.name or '',
             self.name or '',
             self.device_role.name,
             self.device_role.name,
+            self.tenant.name if self.tenant else '',
             self.device_type.manufacturer.name,
             self.device_type.manufacturer.name,
             self.device_type.model,
             self.device_type.model,
             self.platform.name if self.platform else '',
             self.platform.name if self.platform else '',

+ 34 - 24
netbox/dcim/tables.py

@@ -16,27 +16,27 @@ DEVICE_LINK = """
 </a>
 </a>
 """
 """
 
 
-RACKGROUP_EDIT_LINK = """
+RACKGROUP_ACTIONS = """
 {% if perms.dcim.change_rackgroup %}
 {% if perms.dcim.change_rackgroup %}
-    <a href="{% url 'dcim:rackgroup_edit' pk=record.pk %}">Edit</a>
+    <a href="{% url 'dcim:rackgroup_edit' pk=record.pk %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
 {% endif %}
 {% endif %}
 """
 """
 
 
-DEVICEROLE_EDIT_LINK = """
+DEVICEROLE_ACTIONS = """
 {% if perms.dcim.change_devicerole %}
 {% if perms.dcim.change_devicerole %}
-    <a href="{% url 'dcim:devicerole_edit' slug=record.slug %}">Edit</a>
+    <a href="{% url 'dcim:devicerole_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
 {% endif %}
 {% endif %}
 """
 """
 
 
-MANUFACTURER_EDIT_LINK = """
+MANUFACTURER_ACTIONS = """
 {% if perms.dcim.change_manufacturer %}
 {% if perms.dcim.change_manufacturer %}
-    <a href="{% url 'dcim:manufacturer_edit' slug=record.slug %}">Edit</a>
+    <a href="{% url 'dcim:manufacturer_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
 {% endif %}
 {% endif %}
 """
 """
 
 
-PLATFORM_EDIT_LINK = """
+PLATFORM_ACTIONS = """
 {% if perms.dcim.change_platform %}
 {% if perms.dcim.change_platform %}
-    <a href="{% url 'dcim:platform_edit' slug=record.slug %}">Edit</a>
+    <a href="{% url 'dcim:platform_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
 {% endif %}
 {% endif %}
 """
 """
 
 
@@ -59,8 +59,10 @@ UTILIZATION_GRAPH = """
 #
 #
 
 
 class SiteTable(BaseTable):
 class SiteTable(BaseTable):
+    pk = ToggleColumn()
     name = tables.LinkColumn('dcim:site', args=[Accessor('slug')], verbose_name='Name')
     name = tables.LinkColumn('dcim:site', args=[Accessor('slug')], verbose_name='Name')
     facility = tables.Column(verbose_name='Facility')
     facility = tables.Column(verbose_name='Facility')
+    tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
     asn = tables.Column(verbose_name='ASN')
     asn = tables.Column(verbose_name='ASN')
     rack_count = tables.Column(accessor=Accessor('count_racks'), orderable=False, verbose_name='Racks')
     rack_count = tables.Column(accessor=Accessor('count_racks'), orderable=False, verbose_name='Racks')
     device_count = tables.Column(accessor=Accessor('count_devices'), orderable=False, verbose_name='Devices')
     device_count = tables.Column(accessor=Accessor('count_devices'), orderable=False, verbose_name='Devices')
@@ -70,8 +72,8 @@ class SiteTable(BaseTable):
 
 
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):
         model = Site
         model = Site
-        fields = ('name', 'facility', 'asn', 'rack_count', 'device_count', 'prefix_count', 'vlan_count',
-                  'circuit_count')
+        fields = ('pk', 'name', 'facility', 'tenant', 'asn', 'rack_count', 'device_count', 'prefix_count',
+                  'vlan_count', 'circuit_count')
 
 
 
 
 #
 #
@@ -84,11 +86,12 @@ class RackGroupTable(BaseTable):
     site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
     site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
     rack_count = tables.Column(verbose_name='Racks')
     rack_count = tables.Column(verbose_name='Racks')
     slug = tables.Column(verbose_name='Slug')
     slug = tables.Column(verbose_name='Slug')
-    edit = tables.TemplateColumn(template_code=RACKGROUP_EDIT_LINK, verbose_name='')
+    actions = tables.TemplateColumn(template_code=RACKGROUP_ACTIONS, attrs={'td': {'class': 'text-right'}},
+                                    verbose_name='')
 
 
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):
         model = RackGroup
         model = RackGroup
-        fields = ('pk', 'name', 'site', 'rack_count', 'slug', 'edit')
+        fields = ('pk', 'name', 'site', 'rack_count', 'slug', 'actions')
 
 
 
 
 #
 #
@@ -101,14 +104,16 @@ class RackTable(BaseTable):
     site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
     site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
     group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group')
     group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group')
     facility_id = tables.Column(verbose_name='Facility ID')
     facility_id = tables.Column(verbose_name='Facility ID')
-    u_height = tables.Column(verbose_name='Height (U)')
+    tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
+    u_height = tables.TemplateColumn("{{ record.u_height }}U", verbose_name='Height')
     devices = tables.Column(accessor=Accessor('device_count'), verbose_name='Devices')
     devices = tables.Column(accessor=Accessor('device_count'), verbose_name='Devices')
-    u_consumed = tables.Column(accessor=Accessor('u_consumed'), verbose_name='Used (U)')
+    u_consumed = tables.TemplateColumn("{{ record.u_consumed|default:'0' }}U", verbose_name='Used')
     utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False, verbose_name='Utilization')
     utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False, verbose_name='Utilization')
 
 
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):
         model = Rack
         model = Rack
-        fields = ('pk', 'name', 'site', 'group', 'facility_id', 'u_height', 'devices', 'u_consumed', 'utilization')
+        fields = ('pk', 'name', 'site', 'group', 'facility_id', 'tenant', 'u_height', 'devices', 'u_consumed',
+                  'utilization')
 
 
 
 
 class RackImportTable(BaseTable):
 class RackImportTable(BaseTable):
@@ -116,11 +121,12 @@ class RackImportTable(BaseTable):
     site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
     site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
     group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group')
     group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group')
     facility_id = tables.Column(verbose_name='Facility ID')
     facility_id = tables.Column(verbose_name='Facility ID')
+    tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
     u_height = tables.Column(verbose_name='Height (U)')
     u_height = tables.Column(verbose_name='Height (U)')
 
 
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):
         model = Rack
         model = Rack
-        fields = ('site', 'group', 'name', 'facility_id', 'u_height')
+        fields = ('site', 'group', 'name', 'facility_id', 'tenant', 'u_height')
 
 
 
 
 #
 #
@@ -132,11 +138,12 @@ class ManufacturerTable(BaseTable):
     name = tables.LinkColumn(verbose_name='Name')
     name = tables.LinkColumn(verbose_name='Name')
     devicetype_count = tables.Column(verbose_name='Device Types')
     devicetype_count = tables.Column(verbose_name='Device Types')
     slug = tables.Column(verbose_name='Slug')
     slug = tables.Column(verbose_name='Slug')
-    edit = tables.TemplateColumn(template_code=MANUFACTURER_EDIT_LINK, verbose_name='')
+    actions = tables.TemplateColumn(template_code=MANUFACTURER_ACTIONS, attrs={'td': {'class': 'text-right'}},
+                                    verbose_name='')
 
 
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):
         model = Manufacturer
         model = Manufacturer
-        fields = ('pk', 'name', 'devicetype_count', 'slug', 'edit')
+        fields = ('pk', 'name', 'devicetype_count', 'slug', 'actions')
 
 
 
 
 #
 #
@@ -228,11 +235,12 @@ class DeviceRoleTable(BaseTable):
     device_count = tables.Column(verbose_name='Devices')
     device_count = tables.Column(verbose_name='Devices')
     slug = tables.Column(verbose_name='Slug')
     slug = tables.Column(verbose_name='Slug')
     color = tables.Column(verbose_name='Color')
     color = tables.Column(verbose_name='Color')
-    edit = tables.TemplateColumn(template_code=DEVICEROLE_EDIT_LINK, verbose_name='')
+    actions = tables.TemplateColumn(template_code=DEVICEROLE_ACTIONS, attrs={'td': {'class': 'text-right'}},
+                                    verbose_name='')
 
 
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):
         model = DeviceRole
         model = DeviceRole
-        fields = ('pk', 'name', 'device_count', 'slug', 'color')
+        fields = ('pk', 'name', 'device_count', 'slug', 'color', 'actions')
 
 
 
 
 #
 #
@@ -244,11 +252,11 @@ class PlatformTable(BaseTable):
     name = tables.LinkColumn(verbose_name='Name')
     name = tables.LinkColumn(verbose_name='Name')
     device_count = tables.Column(verbose_name='Devices')
     device_count = tables.Column(verbose_name='Devices')
     slug = tables.Column(verbose_name='Slug')
     slug = tables.Column(verbose_name='Slug')
-    edit = tables.TemplateColumn(template_code=PLATFORM_EDIT_LINK, verbose_name='')
+    actions = tables.TemplateColumn(template_code=PLATFORM_ACTIONS, attrs={'td': {'class': 'text-right'}}, verbose_name='')
 
 
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):
         model = Platform
         model = Platform
-        fields = ('pk', 'name', 'device_count', 'slug', 'edit')
+        fields = ('pk', 'name', 'device_count', 'slug', 'actions')
 
 
 
 
 #
 #
@@ -259,6 +267,7 @@ class DeviceTable(BaseTable):
     pk = ToggleColumn()
     pk = ToggleColumn()
     status = tables.TemplateColumn(template_code=STATUS_ICON, verbose_name='')
     status = tables.TemplateColumn(template_code=STATUS_ICON, verbose_name='')
     name = tables.TemplateColumn(template_code=DEVICE_LINK, verbose_name='Name')
     name = tables.TemplateColumn(template_code=DEVICE_LINK, verbose_name='Name')
+    tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
     site = tables.Column(accessor=Accessor('rack.site'), verbose_name='Site')
     site = tables.Column(accessor=Accessor('rack.site'), verbose_name='Site')
     rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')], verbose_name='Rack')
     rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')], verbose_name='Rack')
     device_role = tables.Column(verbose_name='Role')
     device_role = tables.Column(verbose_name='Role')
@@ -268,11 +277,12 @@ class DeviceTable(BaseTable):
 
 
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):
         model = Device
         model = Device
-        fields = ('pk', 'name', 'status', 'site', 'rack', 'device_role', 'device_type', 'primary_ip')
+        fields = ('pk', 'name', 'status', 'tenant', 'site', 'rack', 'device_role', 'device_type', 'primary_ip')
 
 
 
 
 class DeviceImportTable(BaseTable):
 class DeviceImportTable(BaseTable):
     name = tables.TemplateColumn(template_code=DEVICE_LINK, verbose_name='Name')
     name = tables.TemplateColumn(template_code=DEVICE_LINK, verbose_name='Name')
+    tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
     site = tables.Column(accessor=Accessor('rack.site'), verbose_name='Site')
     site = tables.Column(accessor=Accessor('rack.site'), verbose_name='Site')
     rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')], verbose_name='Rack')
     rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')], verbose_name='Rack')
     position = tables.Column(verbose_name='Position')
     position = tables.Column(verbose_name='Position')
@@ -281,7 +291,7 @@ class DeviceImportTable(BaseTable):
 
 
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):
         model = Device
         model = Device
-        fields = ('name', 'site', 'rack', 'position', 'device_role', 'device_type')
+        fields = ('name', 'tenant', 'site', 'rack', 'position', 'device_role', 'device_type')
         empty_text = False
         empty_text = False
 
 
 
 

+ 6 - 0
netbox/dcim/tests/test_apis.py

@@ -15,6 +15,7 @@ class SiteTest(APITestCase):
         'id',
         'id',
         'name',
         'name',
         'slug',
         'slug',
+        'tenant',
         'facility',
         'facility',
         'asn',
         'asn',
         'physical_address',
         'physical_address',
@@ -40,6 +41,7 @@ class SiteTest(APITestCase):
         'display_name',
         'display_name',
         'site',
         'site',
         'group',
         'group',
+        'tenant',
         'u_height',
         'u_height',
         'comments'
         'comments'
     ]
     ]
@@ -115,6 +117,7 @@ class RackTest(APITestCase):
         'display_name',
         'display_name',
         'site',
         'site',
         'group',
         'group',
+        'tenant',
         'u_height',
         'u_height',
         'comments'
         'comments'
     ]
     ]
@@ -126,6 +129,7 @@ class RackTest(APITestCase):
         'display_name',
         'display_name',
         'site',
         'site',
         'group',
         'group',
+        'tenant',
         'u_height',
         'u_height',
         'comments',
         'comments',
         'front_units',
         'front_units',
@@ -311,6 +315,7 @@ class DeviceTest(APITestCase):
         'display_name',
         'display_name',
         'device_type',
         'device_type',
         'device_role',
         'device_role',
+        'tenant',
         'platform',
         'platform',
         'serial',
         'serial',
         'rack',
         'rack',
@@ -388,6 +393,7 @@ class DeviceTest(APITestCase):
             'rack_name',
             'rack_name',
             'serial',
             'serial',
             'status',
             'status',
+            'tenant',
         ]
         ]
 
 
         response = self.client.get(endpoint)
         response = self.client.get(endpoint)

+ 1 - 0
netbox/dcim/urls.py

@@ -15,6 +15,7 @@ urlpatterns = [
     url(r'^sites/$', views.SiteListView.as_view(), name='site_list'),
     url(r'^sites/$', views.SiteListView.as_view(), name='site_list'),
     url(r'^sites/add/$', views.SiteEditView.as_view(), name='site_add'),
     url(r'^sites/add/$', views.SiteEditView.as_view(), name='site_add'),
     url(r'^sites/import/$', views.SiteBulkImportView.as_view(), name='site_import'),
     url(r'^sites/import/$', views.SiteBulkImportView.as_view(), name='site_import'),
+    url(r'^sites/edit/$', views.SiteBulkEditView.as_view(), name='site_bulk_edit'),
     url(r'^sites/(?P<slug>[\w-]+)/$', views.site, name='site'),
     url(r'^sites/(?P<slug>[\w-]+)/$', views.site, name='site'),
     url(r'^sites/(?P<slug>[\w-]+)/edit/$', views.SiteEditView.as_view(), name='site_edit'),
     url(r'^sites/(?P<slug>[\w-]+)/edit/$', views.SiteEditView.as_view(), name='site_edit'),
     url(r'^sites/(?P<slug>[\w-]+)/delete/$', views.SiteDeleteView.as_view(), name='site_delete'),
     url(r'^sites/(?P<slug>[\w-]+)/delete/$', views.SiteDeleteView.as_view(), name='site_delete'),

+ 42 - 10
netbox/dcim/views.py

@@ -15,7 +15,7 @@ from django.views.generic import View
 
 
 from ipam.models import Prefix, IPAddress, VLAN
 from ipam.models import Prefix, IPAddress, VLAN
 from circuits.models import Circuit
 from circuits.models import Circuit
-from extras.models import TopologyMap
+from extras.models import Graph, TopologyMap, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE
 from utilities.forms import ConfirmationForm
 from utilities.forms import ConfirmationForm
 from utilities.views import (
 from utilities.views import (
     BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
     BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
@@ -61,9 +61,11 @@ def expand_pattern(string):
 #
 #
 
 
 class SiteListView(ObjectListView):
 class SiteListView(ObjectListView):
-    queryset = Site.objects.all()
+    queryset = Site.objects.select_related('tenant')
     filter = filters.SiteFilter
     filter = filters.SiteFilter
+    filter_form = forms.SiteFilterForm
     table = tables.SiteTable
     table = tables.SiteTable
+    edit_permissions = ['dcim.change_rack', 'dcim.delete_rack']
     template_name = 'dcim/site_list.html'
     template_name = 'dcim/site_list.html'
 
 
 
 
@@ -79,12 +81,14 @@ def site(request, slug):
     }
     }
     rack_groups = RackGroup.objects.filter(site=site).annotate(rack_count=Count('racks'))
     rack_groups = RackGroup.objects.filter(site=site).annotate(rack_count=Count('racks'))
     topology_maps = TopologyMap.objects.filter(site=site)
     topology_maps = TopologyMap.objects.filter(site=site)
+    show_graphs = Graph.objects.filter(type=GRAPH_TYPE_SITE).exists()
 
 
     return render(request, 'dcim/site.html', {
     return render(request, 'dcim/site.html', {
         'site': site,
         'site': site,
         'stats': stats,
         'stats': stats,
         'rack_groups': rack_groups,
         'rack_groups': rack_groups,
         'topology_maps': topology_maps,
         'topology_maps': topology_maps,
+        'show_graphs': show_graphs,
     })
     })
 
 
 
 
@@ -110,6 +114,24 @@ class SiteBulkImportView(PermissionRequiredMixin, BulkImportView):
     obj_list_url = 'dcim:site_list'
     obj_list_url = 'dcim:site_list'
 
 
 
 
+class SiteBulkEditView(PermissionRequiredMixin, BulkEditView):
+    permission_required = 'dcim.change_site'
+    cls = Site
+    form = forms.SiteBulkEditForm
+    template_name = 'dcim/site_bulk_edit.html'
+    default_redirect_url = 'dcim:site_list'
+
+    def update_objects(self, pk_list, form):
+
+        fields_to_update = {}
+        if form.cleaned_data['tenant'] == 0:
+            fields_to_update['tenant'] = None
+        elif form.cleaned_data['tenant']:
+            fields_to_update['tenant'] = form.cleaned_data['tenant']
+
+        return self.cls.objects.filter(pk__in=pk_list).update(**fields_to_update)
+
+
 #
 #
 # Rack groups
 # Rack groups
 #
 #
@@ -141,7 +163,8 @@ class RackGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 #
 #
 
 
 class RackListView(ObjectListView):
 class RackListView(ObjectListView):
-    queryset = Rack.objects.select_related('site').prefetch_related('devices__device_type').annotate(device_count=Count('devices', distinct=True), u_consumed=Sum('devices__device_type__u_height'))
+    queryset = Rack.objects.select_related('site').prefetch_related('devices__device_type')\
+        .annotate(device_count=Count('devices', distinct=True), u_consumed=Sum('devices__device_type__u_height'))
     filter = filters.RackFilter
     filter = filters.RackFilter
     filter_form = forms.RackFilterForm
     filter_form = forms.RackFilterForm
     table = tables.RackTable
     table = tables.RackTable
@@ -153,7 +176,7 @@ def rack(request, pk):
 
 
     rack = get_object_or_404(Rack, pk=pk)
     rack = get_object_or_404(Rack, pk=pk)
 
 
-    nonracked_devices = Device.objects.filter(rack=rack, position__isnull=True)\
+    nonracked_devices = Device.objects.filter(rack=rack, position__isnull=True, parent_bay__isnull=True)\
         .select_related('device_type__manufacturer')
         .select_related('device_type__manufacturer')
     next_rack = Rack.objects.filter(site=rack.site, name__gt=rack.name).order_by('name').first()
     next_rack = Rack.objects.filter(site=rack.site, name__gt=rack.name).order_by('name').first()
     prev_rack = Rack.objects.filter(site=rack.site, name__lt=rack.name).order_by('-name').first()
     prev_rack = Rack.objects.filter(site=rack.site, name__lt=rack.name).order_by('-name').first()
@@ -200,7 +223,11 @@ class RackBulkEditView(PermissionRequiredMixin, BulkEditView):
     def update_objects(self, pk_list, form):
     def update_objects(self, pk_list, form):
 
 
         fields_to_update = {}
         fields_to_update = {}
-        for field in ['site', 'group', 'u_height', 'comments']:
+        if form.cleaned_data['tenant'] == 0:
+            fields_to_update['tenant'] = None
+        elif form.cleaned_data['tenant']:
+            fields_to_update['tenant'] = form.cleaned_data['tenant']
+        for field in ['site', 'group', 'tenant', 'u_height', 'comments']:
             if form.cleaned_data[field]:
             if form.cleaned_data[field]:
                 fields_to_update[field] = form.cleaned_data[field]
                 fields_to_update[field] = form.cleaned_data[field]
 
 
@@ -560,6 +587,9 @@ def device(request, pk):
             related_devices = Device.objects.filter(name__istartswith=base_name).exclude(pk=device.pk)\
             related_devices = Device.objects.filter(name__istartswith=base_name).exclude(pk=device.pk)\
                 .select_related('rack', 'device_type__manufacturer')[:10]
                 .select_related('rack', 'device_type__manufacturer')[:10]
 
 
+    # Show graph button on interfaces only if at least one graph has been created.
+    show_graphs = Graph.objects.filter(type=GRAPH_TYPE_INTERFACE).exists()
+
     return render(request, 'dcim/device.html', {
     return render(request, 'dcim/device.html', {
         'device': device,
         'device': device,
         'console_ports': console_ports,
         'console_ports': console_ports,
@@ -572,6 +602,7 @@ def device(request, pk):
         'ip_addresses': ip_addresses,
         'ip_addresses': ip_addresses,
         'secrets': secrets,
         'secrets': secrets,
         'related_devices': related_devices,
         'related_devices': related_devices,
+        'show_graphs': show_graphs,
     })
     })
 
 
 
 
@@ -625,14 +656,15 @@ class DeviceBulkEditView(PermissionRequiredMixin, BulkEditView):
     def update_objects(self, pk_list, form):
     def update_objects(self, pk_list, form):
 
 
         fields_to_update = {}
         fields_to_update = {}
-        if form.cleaned_data['platform']:
-            fields_to_update['platform'] = form.cleaned_data['platform']
-        elif form.cleaned_data['platform_delete']:
-            fields_to_update['platform'] = None
+        for field in ['tenant', 'platform']:
+            if form.cleaned_data[field] == 0:
+                fields_to_update[field] = None
+            elif form.cleaned_data[field]:
+                fields_to_update[field] = form.cleaned_data[field]
         if form.cleaned_data['status']:
         if form.cleaned_data['status']:
             status = form.cleaned_data['status']
             status = form.cleaned_data['status']
             fields_to_update['status'] = True if status == 'True' else False
             fields_to_update['status'] = True if status == 'True' else False
-        for field in ['device_type', 'device_role', 'serial']:
+        for field in ['tenant', 'device_type', 'device_role', 'serial']:
             if form.cleaned_data[field]:
             if form.cleaned_data[field]:
                 fields_to_update[field] = form.cleaned_data[field]
                 fields_to_update[field] = form.cleaned_data[field]
 
 

+ 11 - 6
netbox/ipam/admin.py

@@ -7,7 +7,12 @@ from .models import (
 
 
 @admin.register(VRF)
 @admin.register(VRF)
 class VRFAdmin(admin.ModelAdmin):
 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)
 @admin.register(Role)
@@ -35,7 +40,7 @@ class AggregateAdmin(admin.ModelAdmin):
 
 
 @admin.register(Prefix)
 @admin.register(Prefix)
 class PrefixAdmin(admin.ModelAdmin):
 class PrefixAdmin(admin.ModelAdmin):
-    list_display = ['prefix', 'vrf', 'site', 'status', 'role', 'vlan']
+    list_display = ['prefix', 'vrf', 'tenant', 'site', 'status', 'role', 'vlan']
     list_filter = ['family', 'site', 'status', 'role']
     list_filter = ['family', 'site', 'status', 'role']
     search_fields = ['prefix']
     search_fields = ['prefix']
 
 
@@ -46,7 +51,7 @@ class PrefixAdmin(admin.ModelAdmin):
 
 
 @admin.register(IPAddress)
 @admin.register(IPAddress)
 class IPAddressAdmin(admin.ModelAdmin):
 class IPAddressAdmin(admin.ModelAdmin):
-    list_display = ['address', 'vrf', 'nat_inside']
+    list_display = ['address', 'vrf', 'tenant', 'nat_inside']
     list_filter = ['family']
     list_filter = ['family']
     fields = ['address', 'vrf', 'device', 'interface', 'nat_inside']
     fields = ['address', 'vrf', 'device', 'interface', 'nat_inside']
     readonly_fields = ['interface', 'device', 'nat_inside']
     readonly_fields = ['interface', 'device', 'nat_inside']
@@ -67,10 +72,10 @@ class VLANGroupAdmin(admin.ModelAdmin):
 
 
 @admin.register(VLAN)
 @admin.register(VLAN)
 class VLANAdmin(admin.ModelAdmin):
 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']
     search_fields = ['vid', 'name']
 
 
     def get_queryset(self, request):
     def get_queryset(self, request):
         qs = super(VLANAdmin, self).get_queryset(request)
         qs = super(VLANAdmin, self).get_queryset(request)
-        return qs.select_related('site', 'role')
+        return qs.select_related('site', 'tenant', 'role')

+ 20 - 6
netbox/ipam/api/serializers.py

@@ -2,6 +2,7 @@ from rest_framework import serializers
 
 
 from dcim.api.serializers import SiteNestedSerializer, InterfaceNestedSerializer
 from dcim.api.serializers import SiteNestedSerializer, InterfaceNestedSerializer
 from ipam.models import VRF, Role, RIR, Aggregate, Prefix, IPAddress, VLAN, VLANGroup
 from ipam.models import VRF, Role, RIR, Aggregate, Prefix, IPAddress, VLAN, VLANGroup
+from tenancy.api.serializers import TenantNestedSerializer
 
 
 
 
 #
 #
@@ -9,10 +10,11 @@ from ipam.models import VRF, Role, RIR, Aggregate, Prefix, IPAddress, VLAN, VLAN
 #
 #
 
 
 class VRFSerializer(serializers.ModelSerializer):
 class VRFSerializer(serializers.ModelSerializer):
+    tenant = TenantNestedSerializer()
 
 
     class Meta:
     class Meta:
         model = VRF
         model = VRF
-        fields = ['id', 'name', 'rd', 'enforce_unique', 'description']
+        fields = ['id', 'name', 'rd', 'tenant', 'enforce_unique', 'description']
 
 
 
 
 class VRFNestedSerializer(VRFSerializer):
 class VRFNestedSerializer(VRFSerializer):
@@ -21,6 +23,15 @@ class VRFNestedSerializer(VRFSerializer):
         fields = ['id', 'name', 'rd']
         fields = ['id', 'name', 'rd']
 
 
 
 
+class VRFTenantSerializer(VRFSerializer):
+    """
+    Include tenant serializer. Useful for determining tenant inheritance for Prefixes and IPAddresses.
+    """
+
+    class Meta(VRFSerializer.Meta):
+        fields = ['id', 'name', 'rd', 'tenant']
+
+
 #
 #
 # Roles
 # Roles
 #
 #
@@ -98,11 +109,12 @@ class VLANGroupNestedSerializer(VLANGroupSerializer):
 class VLANSerializer(serializers.ModelSerializer):
 class VLANSerializer(serializers.ModelSerializer):
     site = SiteNestedSerializer()
     site = SiteNestedSerializer()
     group = VLANGroupNestedSerializer()
     group = VLANGroupNestedSerializer()
+    tenant = TenantNestedSerializer()
     role = RoleNestedSerializer()
     role = RoleNestedSerializer()
 
 
     class Meta:
     class Meta:
         model = VLAN
         model = VLAN
-        fields = ['id', 'site', 'group', 'vid', 'name', 'status', 'role', 'description', 'display_name']
+        fields = ['id', 'site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description', 'display_name']
 
 
 
 
 class VLANNestedSerializer(VLANSerializer):
 class VLANNestedSerializer(VLANSerializer):
@@ -117,13 +129,14 @@ class VLANNestedSerializer(VLANSerializer):
 
 
 class PrefixSerializer(serializers.ModelSerializer):
 class PrefixSerializer(serializers.ModelSerializer):
     site = SiteNestedSerializer()
     site = SiteNestedSerializer()
-    vrf = VRFNestedSerializer()
+    vrf = VRFTenantSerializer()
+    tenant = TenantNestedSerializer()
     vlan = VLANNestedSerializer()
     vlan = VLANNestedSerializer()
     role = RoleNestedSerializer()
     role = RoleNestedSerializer()
 
 
     class Meta:
     class Meta:
         model = Prefix
         model = Prefix
-        fields = ['id', 'family', 'prefix', 'site', 'vrf', 'vlan', 'status', 'role', 'description']
+        fields = ['id', 'family', 'prefix', 'site', 'vrf', 'tenant', 'vlan', 'status', 'role', 'description']
 
 
 
 
 class PrefixNestedSerializer(PrefixSerializer):
 class PrefixNestedSerializer(PrefixSerializer):
@@ -137,12 +150,13 @@ class PrefixNestedSerializer(PrefixSerializer):
 #
 #
 
 
 class IPAddressSerializer(serializers.ModelSerializer):
 class IPAddressSerializer(serializers.ModelSerializer):
-    vrf = VRFNestedSerializer()
+    vrf = VRFTenantSerializer()
+    tenant = TenantNestedSerializer()
     interface = InterfaceNestedSerializer()
     interface = InterfaceNestedSerializer()
 
 
     class Meta:
     class Meta:
         model = IPAddress
         model = IPAddress
-        fields = ['id', 'family', 'address', 'vrf', 'interface', 'description', 'nat_inside', 'nat_outside']
+        fields = ['id', 'family', 'address', 'vrf', 'tenant', 'interface', 'description', 'nat_inside', 'nat_outside']
 
 
 
 
 class IPAddressNestedSerializer(IPAddressSerializer):
 class IPAddressNestedSerializer(IPAddressSerializer):

+ 10 - 10
netbox/ipam/api/views.py

@@ -14,7 +14,7 @@ class VRFListView(generics.ListAPIView):
     """
     """
     List all VRFs
     List all VRFs
     """
     """
-    queryset = VRF.objects.all()
+    queryset = VRF.objects.select_related('tenant')
     serializer_class = serializers.VRFSerializer
     serializer_class = serializers.VRFSerializer
     filter_class = filters.VRFFilter
     filter_class = filters.VRFFilter
 
 
@@ -23,7 +23,7 @@ class VRFDetailView(generics.RetrieveAPIView):
     """
     """
     Retrieve a single VRF
     Retrieve a single VRF
     """
     """
-    queryset = VRF.objects.all()
+    queryset = VRF.objects.select_related('tenant')
     serializer_class = serializers.VRFSerializer
     serializer_class = serializers.VRFSerializer
 
 
 
 
@@ -96,7 +96,7 @@ class PrefixListView(generics.ListAPIView):
     """
     """
     List prefixes (filterable)
     List prefixes (filterable)
     """
     """
-    queryset = Prefix.objects.select_related('site', 'vrf', 'vlan', 'role')
+    queryset = Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role')
     serializer_class = serializers.PrefixSerializer
     serializer_class = serializers.PrefixSerializer
     filter_class = filters.PrefixFilter
     filter_class = filters.PrefixFilter
 
 
@@ -105,7 +105,7 @@ class PrefixDetailView(generics.RetrieveAPIView):
     """
     """
     Retrieve a single prefix
     Retrieve a single prefix
     """
     """
-    queryset = Prefix.objects.select_related('site', 'vrf', 'vlan', 'role')
+    queryset = Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role')
     serializer_class = serializers.PrefixSerializer
     serializer_class = serializers.PrefixSerializer
 
 
 
 
@@ -117,7 +117,7 @@ class IPAddressListView(generics.ListAPIView):
     """
     """
     List IP addresses (filterable)
     List IP addresses (filterable)
     """
     """
-    queryset = IPAddress.objects.select_related('vrf', 'interface__device', 'nat_inside')\
+    queryset = IPAddress.objects.select_related('vrf__tenant', 'tenant', 'interface__device', 'nat_inside')\
         .prefetch_related('nat_outside')
         .prefetch_related('nat_outside')
     serializer_class = serializers.IPAddressSerializer
     serializer_class = serializers.IPAddressSerializer
     filter_class = filters.IPAddressFilter
     filter_class = filters.IPAddressFilter
@@ -127,7 +127,7 @@ class IPAddressDetailView(generics.RetrieveAPIView):
     """
     """
     Retrieve a single IP address
     Retrieve a single IP address
     """
     """
-    queryset = IPAddress.objects.select_related('vrf', 'interface__device', 'nat_inside')\
+    queryset = IPAddress.objects.select_related('vrf__tenant', 'tenant', 'interface__device', 'nat_inside')\
         .prefetch_related('nat_outside')
         .prefetch_related('nat_outside')
     serializer_class = serializers.IPAddressSerializer
     serializer_class = serializers.IPAddressSerializer
 
 
@@ -140,7 +140,7 @@ class VLANGroupListView(generics.ListAPIView):
     """
     """
     List all VLAN groups
     List all VLAN groups
     """
     """
-    queryset = VLANGroup.objects.all()
+    queryset = VLANGroup.objects.select_related('site')
     serializer_class = serializers.VLANGroupSerializer
     serializer_class = serializers.VLANGroupSerializer
     filter_class = filters.VLANGroupFilter
     filter_class = filters.VLANGroupFilter
 
 
@@ -149,7 +149,7 @@ class VLANGroupDetailView(generics.RetrieveAPIView):
     """
     """
     Retrieve a single VLAN group
     Retrieve a single VLAN group
     """
     """
-    queryset = VLANGroup.objects.all()
+    queryset = VLANGroup.objects.select_related('site')
     serializer_class = serializers.VLANGroupSerializer
     serializer_class = serializers.VLANGroupSerializer
 
 
 
 
@@ -161,7 +161,7 @@ class VLANListView(generics.ListAPIView):
     """
     """
     List VLANs (filterable)
     List VLANs (filterable)
     """
     """
-    queryset = VLAN.objects.select_related('site', 'role')
+    queryset = VLAN.objects.select_related('site', 'group', 'tenant', 'role')
     serializer_class = serializers.VLANSerializer
     serializer_class = serializers.VLANSerializer
     filter_class = filters.VLANFilter
     filter_class = filters.VLANFilter
 
 
@@ -170,5 +170,5 @@ class VLANDetailView(generics.RetrieveAPIView):
     """
     """
     Retrieve a single VLAN
     Retrieve a single VLAN
     """
     """
-    queryset = VLAN.objects.select_related('site', 'role')
+    queryset = VLAN.objects.select_related('site', 'group', 'tenant', 'role')
     serializer_class = serializers.VLANSerializer
     serializer_class = serializers.VLANSerializer

+ 123 - 8
netbox/ipam/filters.py

@@ -2,17 +2,42 @@ import django_filters
 from netaddr import IPNetwork
 from netaddr import IPNetwork
 from netaddr.core import AddrFormatError
 from netaddr.core import AddrFormatError
 
 
+from django.db.models import Q
+
 from dcim.models import Site, Device, Interface
 from dcim.models import Site, Device, Interface
+from tenancy.models import Tenant
 
 
 from .models import RIR, Aggregate, VRF, Prefix, IPAddress, VLAN, VLANGroup, Role
 from .models import RIR, Aggregate, VRF, Prefix, IPAddress, VLAN, VLANGroup, Role
 
 
 
 
 class VRFFilter(django_filters.FilterSet):
 class VRFFilter(django_filters.FilterSet):
+    q = django_filters.MethodFilter(
+        action='search',
+        label='Search',
+    )
     name = django_filters.CharFilter(
     name = django_filters.CharFilter(
         name='name',
         name='name',
         lookup_type='icontains',
         lookup_type='icontains',
         label='Name',
         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)',
+    )
+
+    def search(self, queryset, value):
+        return queryset.filter(
+            Q(name__icontains=value) |
+            Q(rd__icontains=value) |
+            Q(description__icontains=value)
+        )
 
 
     class Meta:
     class Meta:
         model = VRF
         model = VRF
@@ -20,6 +45,10 @@ class VRFFilter(django_filters.FilterSet):
 
 
 
 
 class AggregateFilter(django_filters.FilterSet):
 class AggregateFilter(django_filters.FilterSet):
+    q = django_filters.MethodFilter(
+        action='search',
+        label='Search',
+    )
     rir_id = django_filters.ModelMultipleChoiceFilter(
     rir_id = django_filters.ModelMultipleChoiceFilter(
         name='rir',
         name='rir',
         queryset=RIR.objects.all(),
         queryset=RIR.objects.all(),
@@ -36,6 +65,15 @@ class AggregateFilter(django_filters.FilterSet):
         model = Aggregate
         model = Aggregate
         fields = ['family', 'rir_id', 'rir', 'date_added']
         fields = ['family', 'rir_id', 'rir', 'date_added']
 
 
+    def search(self, queryset, value):
+        qs_filter = Q(description__icontains=value)
+        try:
+            prefix = str(IPNetwork(value.strip()).cidr)
+            qs_filter |= Q(prefix__net_contains_or_equals=prefix)
+        except AddrFormatError:
+            pass
+        return queryset.filter(qs_filter)
+
 
 
 class PrefixFilter(django_filters.FilterSet):
 class PrefixFilter(django_filters.FilterSet):
     q = django_filters.MethodFilter(
     q = django_filters.MethodFilter(
@@ -55,6 +93,14 @@ class PrefixFilter(django_filters.FilterSet):
         action='_vrf',
         action='_vrf',
         label='VRF',
         label='VRF',
     )
     )
+    tenant_id = django_filters.MethodFilter(
+        action='_tenant_id',
+        label='Tenant (ID)',
+    )
+    tenant = django_filters.MethodFilter(
+        action='_tenant',
+        label='Tenant',
+    )
     site_id = django_filters.ModelMultipleChoiceFilter(
     site_id = django_filters.ModelMultipleChoiceFilter(
         name='site',
         name='site',
         queryset=Site.objects.all(),
         queryset=Site.objects.all(),
@@ -92,12 +138,13 @@ class PrefixFilter(django_filters.FilterSet):
         fields = ['family', 'site_id', 'site', 'vrf', 'vrf_id', 'vlan_id', 'vlan_vid', 'status', 'role_id', 'role']
         fields = ['family', 'site_id', 'site', 'vrf', 'vrf_id', 'vlan_id', 'vlan_vid', 'status', 'role_id', 'role']
 
 
     def search(self, queryset, value):
     def search(self, queryset, value):
-        value = value.strip()
+        qs_filter = Q(description__icontains=value)
         try:
         try:
-            query = str(IPNetwork(value).cidr)
-            return queryset.filter(prefix__net_contains_or_equals=query)
+            prefix = str(IPNetwork(value.strip()).cidr)
+            qs_filter |= Q(prefix__net_contains_or_equals=prefix)
         except AddrFormatError:
         except AddrFormatError:
-            return queryset.none()
+            pass
+        return queryset.filter(qs_filter)
 
 
     def search_by_parent(self, queryset, value):
     def search_by_parent(self, queryset, value):
         value = value.strip()
         value = value.strip()
@@ -120,6 +167,24 @@ class PrefixFilter(django_filters.FilterSet):
             return queryset.filter(vrf__isnull=True)
             return queryset.filter(vrf__isnull=True)
         return queryset.filter(vrf__pk=value)
         return queryset.filter(vrf__pk=value)
 
 
+    def _tenant(self, queryset, value):
+        if str(value) == '':
+            return queryset
+        return queryset.filter(
+            Q(tenant__slug=value) |
+            Q(tenant__isnull=True, vrf__tenant__slug=value)
+        )
+
+    def _tenant_id(self, queryset, value):
+        try:
+            value = int(value)
+        except ValueError:
+            return queryset.none()
+        return queryset.filter(
+            Q(tenant__pk=value) |
+            Q(tenant__isnull=True, vrf__tenant__pk=value)
+        )
+
 
 
 class IPAddressFilter(django_filters.FilterSet):
 class IPAddressFilter(django_filters.FilterSet):
     q = django_filters.MethodFilter(
     q = django_filters.MethodFilter(
@@ -135,6 +200,14 @@ class IPAddressFilter(django_filters.FilterSet):
         action='_vrf',
         action='_vrf',
         label='VRF',
         label='VRF',
     )
     )
+    tenant_id = django_filters.MethodFilter(
+        action='_tenant_id',
+        label='Tenant (ID)',
+    )
+    tenant = django_filters.MethodFilter(
+        action='_tenant',
+        label='Tenant',
+    )
     device_id = django_filters.ModelMultipleChoiceFilter(
     device_id = django_filters.ModelMultipleChoiceFilter(
         name='interface__device',
         name='interface__device',
         queryset=Device.objects.all(),
         queryset=Device.objects.all(),
@@ -157,12 +230,13 @@ class IPAddressFilter(django_filters.FilterSet):
         fields = ['q', 'family', 'vrf_id', 'vrf', 'device_id', 'device', 'interface_id']
         fields = ['q', 'family', 'vrf_id', 'vrf', 'device_id', 'device', 'interface_id']
 
 
     def search(self, queryset, value):
     def search(self, queryset, value):
-        value = value.strip()
+        qs_filter = Q(description__icontains=value)
         try:
         try:
-            query = str(IPNetwork(value))
-            return queryset.filter(address__net_host=query)
+            ipaddress = str(IPNetwork(value.strip()))
+            qs_filter |= Q(address__net_host=ipaddress)
         except AddrFormatError:
         except AddrFormatError:
-            return queryset.none()
+            pass
+        return queryset.filter(qs_filter)
 
 
     def _vrf(self, queryset, value):
     def _vrf(self, queryset, value):
         if str(value) == '':
         if str(value) == '':
@@ -175,6 +249,24 @@ class IPAddressFilter(django_filters.FilterSet):
             return queryset.filter(vrf__isnull=True)
             return queryset.filter(vrf__isnull=True)
         return queryset.filter(vrf__pk=value)
         return queryset.filter(vrf__pk=value)
 
 
+    def _tenant(self, queryset, value):
+        if str(value) == '':
+            return queryset
+        return queryset.filter(
+            Q(tenant__slug=value) |
+            Q(tenant__isnull=True, vrf__tenant__slug=value)
+        )
+
+    def _tenant_id(self, queryset, value):
+        try:
+            value = int(value)
+        except ValueError:
+            return queryset.none()
+        return queryset.filter(
+            Q(tenant__pk=value) |
+            Q(tenant__isnull=True, vrf__tenant__pk=value)
+        )
+
 
 
 class VLANGroupFilter(django_filters.FilterSet):
 class VLANGroupFilter(django_filters.FilterSet):
     site_id = django_filters.ModelMultipleChoiceFilter(
     site_id = django_filters.ModelMultipleChoiceFilter(
@@ -195,6 +287,10 @@ class VLANGroupFilter(django_filters.FilterSet):
 
 
 
 
 class VLANFilter(django_filters.FilterSet):
 class VLANFilter(django_filters.FilterSet):
+    q = django_filters.MethodFilter(
+        action='search',
+        label='Search',
+    )
     site_id = django_filters.ModelMultipleChoiceFilter(
     site_id = django_filters.ModelMultipleChoiceFilter(
         name='site',
         name='site',
         queryset=Site.objects.all(),
         queryset=Site.objects.all(),
@@ -226,6 +322,17 @@ class VLANFilter(django_filters.FilterSet):
         name='vid',
         name='vid',
         label='VLAN number (1-4095)',
         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(
     role_id = django_filters.ModelMultipleChoiceFilter(
         name='role',
         name='role',
         queryset=Role.objects.all(),
         queryset=Role.objects.all(),
@@ -241,3 +348,11 @@ class VLANFilter(django_filters.FilterSet):
     class Meta:
     class Meta:
         model = VLAN
         model = VLAN
         fields = ['site_id', 'site', 'vid', 'name', 'status', 'role_id', 'role']
         fields = ['site_id', 'site', 'vid', 'name', 'status', 'role_id', 'role']
+
+    def search(self, queryset, value):
+        qs_filter = Q(name__icontains=value) | Q(description__icontains=value)
+        try:
+            qs_filter |= Q(vid=int(value))
+        except ValueError:
+            pass
+        return queryset.filter(qs_filter)

+ 125 - 0
netbox/ipam/fixtures/initial_data.json

@@ -0,0 +1,125 @@
+[
+{
+    "model": "ipam.aggregate",
+    "pk": 1,
+    "fields": {
+        "created": "2016-08-01",
+        "last_updated": "2016-08-01T15:22:20.938Z",
+        "family": 4,
+        "prefix": "10.0.0.0/8",
+        "rir": 6,
+        "date_added": null,
+        "description": "Private IPv4 space"
+    }
+},
+{
+    "model": "ipam.aggregate",
+    "pk": 2,
+    "fields": {
+        "created": "2016-08-01",
+        "last_updated": "2016-08-01T15:22:32.679Z",
+        "family": 4,
+        "prefix": "172.16.0.0/12",
+        "rir": 6,
+        "date_added": null,
+        "description": "Private IPv4 space"
+    }
+},
+{
+    "model": "ipam.aggregate",
+    "pk": 3,
+    "fields": {
+        "created": "2016-08-01",
+        "last_updated": "2016-08-01T15:22:42.289Z",
+        "family": 4,
+        "prefix": "192.168.0.0/16",
+        "rir": 6,
+        "date_added": null,
+        "description": "Private IPv4 space"
+    }
+},
+{
+    "model": "ipam.rir",
+    "pk": 1,
+    "fields": {
+        "name": "ARIN",
+        "slug": "arin"
+    }
+},
+{
+    "model": "ipam.rir",
+    "pk": 2,
+    "fields": {
+        "name": "RIPE",
+        "slug": "ripe"
+    }
+},
+{
+    "model": "ipam.rir",
+    "pk": 3,
+    "fields": {
+        "name": "APNIC",
+        "slug": "apnic"
+    }
+},
+{
+    "model": "ipam.rir",
+    "pk": 4,
+    "fields": {
+        "name": "LACNIC",
+        "slug": "lacnic"
+    }
+},
+{
+    "model": "ipam.rir",
+    "pk": 5,
+    "fields": {
+        "name": "AFRINIC",
+        "slug": "afrinic"
+    }
+},
+{
+    "model": "ipam.rir",
+    "pk": 6,
+    "fields": {
+        "name": "RFC 1918",
+        "slug": "rfc-1918"
+    }
+},
+{
+    "model": "ipam.role",
+    "pk": 1,
+    "fields": {
+        "name": "Production",
+        "slug": "production",
+        "weight": 1000
+    }
+},
+{
+    "model": "ipam.role",
+    "pk": 2,
+    "fields": {
+        "name": "Development",
+        "slug": "development",
+        "weight": 1000
+    }
+},
+{
+    "model": "ipam.role",
+    "pk": 3,
+    "fields": {
+        "name": "Management",
+        "slug": "management",
+        "weight": 1000
+    }
+},
+{
+    "model": "ipam.role",
+    "pk": 4,
+    "fields": {
+        "name": "Backup",
+        "slug": "backup",
+        "weight": 1000
+    }
+}
+]

+ 70 - 25
netbox/ipam/forms.py

@@ -4,6 +4,8 @@ from django import forms
 from django.db.models import Count
 from django.db.models import Count
 
 
 from dcim.models import Site, Device, Interface
 from dcim.models import Site, Device, Interface
+from tenancy.forms import bulkedit_tenant_choices
+from tenancy.models import Tenant
 from utilities.forms import BootstrapMixin, APISelect, Livesearch, CSVDataField, BulkImportForm, SlugField
 from utilities.forms import BootstrapMixin, APISelect, Livesearch, CSVDataField, BulkImportForm, SlugField
 
 
 from .models import (
 from .models import (
@@ -15,6 +17,18 @@ FORM_PREFIX_STATUS_CHOICES = (('', '---------'),) + PREFIX_STATUS_CHOICES
 FORM_VLAN_STATUS_CHOICES = (('', '---------'),) + VLAN_STATUS_CHOICES
 FORM_VLAN_STATUS_CHOICES = (('', '---------'),) + VLAN_STATUS_CHOICES
 
 
 
 
+def bulkedit_vrf_choices():
+    """
+    Include an option to assign the object to the global table.
+    """
+    choices = [
+        (None, '---------'),
+        (0, 'Global'),
+    ]
+    choices += [(v.pk, v.name) for v in VRF.objects.all()]
+    return choices
+
+
 #
 #
 # VRFs
 # VRFs
 #
 #
@@ -23,7 +37,7 @@ class VRFForm(forms.ModelForm, BootstrapMixin):
 
 
     class Meta:
     class Meta:
         model = VRF
         model = VRF
-        fields = ['name', 'rd', 'enforce_unique', 'description']
+        fields = ['name', 'rd', 'tenant', 'enforce_unique', 'description']
         labels = {
         labels = {
             'rd': "RD",
             'rd': "RD",
         }
         }
@@ -33,10 +47,12 @@ class VRFForm(forms.ModelForm, BootstrapMixin):
 
 
 
 
 class VRFFromCSVForm(forms.ModelForm):
 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:
     class Meta:
         model = VRF
         model = VRF
-        fields = ['name', 'rd', 'enforce_unique', 'description']
+        fields = ['name', 'rd', 'tenant', 'enforce_unique', 'description']
 
 
 
 
 class VRFImportForm(BulkImportForm, BootstrapMixin):
 class VRFImportForm(BulkImportForm, BootstrapMixin):
@@ -45,9 +61,20 @@ class VRFImportForm(BulkImportForm, BootstrapMixin):
 
 
 class VRFBulkEditForm(forms.Form, BootstrapMixin):
 class VRFBulkEditForm(forms.Form, BootstrapMixin):
     pk = forms.ModelMultipleChoiceField(queryset=VRF.objects.all(), widget=forms.MultipleHiddenInput)
     pk = forms.ModelMultipleChoiceField(queryset=VRF.objects.all(), widget=forms.MultipleHiddenInput)
+    tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant')
     description = forms.CharField(max_length=100, 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
 # RIRs
 #
 #
@@ -131,7 +158,7 @@ class PrefixForm(forms.ModelForm, BootstrapMixin):
 
 
     class Meta:
     class Meta:
         model = Prefix
         model = Prefix
-        fields = ['prefix', 'vrf', 'site', 'vlan', 'status', 'role', 'description']
+        fields = ['prefix', 'vrf', 'tenant', 'site', 'vlan', 'status', 'role', 'description']
         help_texts = {
         help_texts = {
             'prefix': "IPv4 or IPv6 network",
             'prefix': "IPv4 or IPv6 network",
             'vrf': "VRF (if applicable)",
             'vrf': "VRF (if applicable)",
@@ -172,6 +199,8 @@ class PrefixForm(forms.ModelForm, BootstrapMixin):
 class PrefixFromCSVForm(forms.ModelForm):
 class PrefixFromCSVForm(forms.ModelForm):
     vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, to_field_name='rd',
     vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, to_field_name='rd',
                                  error_messages={'invalid_choice': 'VRF not found.'})
                                  error_messages={'invalid_choice': 'VRF not found.'})
+    tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
+                                    error_messages={'invalid_choice': 'Tenant not found.'})
     site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False, to_field_name='name',
     site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False, to_field_name='name',
                                   error_messages={'invalid_choice': 'Site not found.'})
                                   error_messages={'invalid_choice': 'Site not found.'})
     vlan_group_name = forms.CharField(required=False)
     vlan_group_name = forms.CharField(required=False)
@@ -182,7 +211,8 @@ class PrefixFromCSVForm(forms.ModelForm):
 
 
     class Meta:
     class Meta:
         model = Prefix
         model = Prefix
-        fields = ['prefix', 'vrf', 'site', 'vlan_group_name', 'vlan_vid', 'status_name', 'role', 'description']
+        fields = ['prefix', 'vrf', 'tenant', 'site', 'vlan_group_name', 'vlan_vid', 'status_name', 'role',
+                  'description']
 
 
     def clean(self):
     def clean(self):
 
 
@@ -228,18 +258,16 @@ class PrefixImportForm(BulkImportForm, BootstrapMixin):
 class PrefixBulkEditForm(forms.Form, BootstrapMixin):
 class PrefixBulkEditForm(forms.Form, BootstrapMixin):
     pk = forms.ModelMultipleChoiceField(queryset=Prefix.objects.all(), widget=forms.MultipleHiddenInput)
     pk = forms.ModelMultipleChoiceField(queryset=Prefix.objects.all(), widget=forms.MultipleHiddenInput)
     site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False)
     site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False)
-    vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, label='VRF',
-                                 help_text="Select the VRF to assign, or check below to remove VRF assignment")
-    vrf_global = forms.BooleanField(required=False, label='Set VRF to global')
+    vrf = forms.TypedChoiceField(choices=bulkedit_vrf_choices, coerce=int, required=False, label='VRF')
+    tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant')
     status = forms.ChoiceField(choices=FORM_PREFIX_STATUS_CHOICES, required=False)
     status = forms.ChoiceField(choices=FORM_PREFIX_STATUS_CHOICES, required=False)
     role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False)
     role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False)
     description = forms.CharField(max_length=100, required=False)
     description = forms.CharField(max_length=100, required=False)
 
 
 
 
 def prefix_vrf_choices():
 def prefix_vrf_choices():
-    vrf_choices = [('', 'All'), (0, 'Global')]
-    vrf_choices += [(v.pk, v.name) for v in VRF.objects.all()]
-    return vrf_choices
+    vrf_choices = VRF.objects.annotate(prefix_count=Count('prefixes'))
+    return [(v.pk, u'{} ({})'.format(v.name, v.prefix_count)) for v in vrf_choices]
 
 
 
 
 def prefix_site_choices():
 def prefix_site_choices():
@@ -261,12 +289,16 @@ def prefix_role_choices():
 
 
 class PrefixFilterForm(forms.Form, BootstrapMixin):
 class PrefixFilterForm(forms.Form, BootstrapMixin):
     parent = forms.CharField(required=False, label='Search Within')
     parent = forms.CharField(required=False, label='Search Within')
-    vrf = forms.ChoiceField(required=False, choices=prefix_vrf_choices, label='VRF')
-    status = forms.MultipleChoiceField(required=False, choices=prefix_status_choices)
+    vrf = forms.MultipleChoiceField(required=False, choices=prefix_vrf_choices, label='VRF',
+                                    widget=forms.SelectMultiple(attrs={'size': 6}))
+    tenant = forms.ModelMultipleChoiceField(queryset=Tenant.objects.all(), required=False,
+                                            widget=forms.SelectMultiple(attrs={'size': 6}))
+    status = forms.MultipleChoiceField(required=False, choices=prefix_status_choices,
+                                       widget=forms.SelectMultiple(attrs={'size': 6}))
     site = forms.MultipleChoiceField(required=False, choices=prefix_site_choices,
     site = forms.MultipleChoiceField(required=False, choices=prefix_site_choices,
-                                     widget=forms.SelectMultiple(attrs={'size': 8}))
+                                     widget=forms.SelectMultiple(attrs={'size': 6}))
     role = forms.MultipleChoiceField(required=False, choices=prefix_role_choices,
     role = forms.MultipleChoiceField(required=False, choices=prefix_role_choices,
-                                     widget=forms.SelectMultiple(attrs={'size': 8}))
+                                     widget=forms.SelectMultiple(attrs={'size': 6}))
     expand = forms.BooleanField(required=False, label='Expand prefix hierarchy')
     expand = forms.BooleanField(required=False, label='Expand prefix hierarchy')
 
 
 
 
@@ -289,7 +321,7 @@ class IPAddressForm(forms.ModelForm, BootstrapMixin):
 
 
     class Meta:
     class Meta:
         model = IPAddress
         model = IPAddress
-        fields = ['address', 'vrf', 'nat_device', 'nat_inside', 'description']
+        fields = ['address', 'vrf', 'tenant', 'nat_device', 'nat_inside', 'description']
         help_texts = {
         help_texts = {
             'address': "IPv4 or IPv6 address and mask",
             'address': "IPv4 or IPv6 address and mask",
             'vrf': "VRF (if applicable)",
             'vrf': "VRF (if applicable)",
@@ -338,6 +370,8 @@ class IPAddressForm(forms.ModelForm, BootstrapMixin):
 class IPAddressFromCSVForm(forms.ModelForm):
 class IPAddressFromCSVForm(forms.ModelForm):
     vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, to_field_name='rd',
     vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, to_field_name='rd',
                                  error_messages={'invalid_choice': 'VRF not found.'})
                                  error_messages={'invalid_choice': 'VRF not found.'})
+    tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
+                                    error_messages={'invalid_choice': 'Tenant not found.'})
     device = forms.ModelChoiceField(queryset=Device.objects.all(), required=False, to_field_name='name',
     device = forms.ModelChoiceField(queryset=Device.objects.all(), required=False, to_field_name='name',
                                     error_messages={'invalid_choice': 'Device not found.'})
                                     error_messages={'invalid_choice': 'Device not found.'})
     interface_name = forms.CharField(required=False)
     interface_name = forms.CharField(required=False)
@@ -345,7 +379,7 @@ class IPAddressFromCSVForm(forms.ModelForm):
 
 
     class Meta:
     class Meta:
         model = IPAddress
         model = IPAddress
-        fields = ['address', 'vrf', 'device', 'interface_name', 'is_primary', 'description']
+        fields = ['address', 'vrf', 'tenant', 'device', 'interface_name', 'is_primary', 'description']
 
 
     def clean(self):
     def clean(self):
 
 
@@ -390,9 +424,8 @@ class IPAddressImportForm(BulkImportForm, BootstrapMixin):
 
 
 class IPAddressBulkEditForm(forms.Form, BootstrapMixin):
 class IPAddressBulkEditForm(forms.Form, BootstrapMixin):
     pk = forms.ModelMultipleChoiceField(queryset=IPAddress.objects.all(), widget=forms.MultipleHiddenInput)
     pk = forms.ModelMultipleChoiceField(queryset=IPAddress.objects.all(), widget=forms.MultipleHiddenInput)
-    vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, label='VRF',
-                                 help_text="Select the VRF to assign, or check below to remove VRF assignment")
-    vrf_global = forms.BooleanField(required=False, label='Set VRF to global')
+    vrf = forms.TypedChoiceField(choices=bulkedit_vrf_choices, coerce=int, required=False, label='VRF')
+    tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant')
     description = forms.CharField(max_length=100, required=False)
     description = forms.CharField(max_length=100, required=False)
 
 
 
 
@@ -401,14 +434,16 @@ def ipaddress_family_choices():
 
 
 
 
 def ipaddress_vrf_choices():
 def ipaddress_vrf_choices():
-    vrf_choices = [('', 'All'), (0, 'Global')]
-    vrf_choices += [(v.pk, v.name) for v in VRF.objects.all()]
-    return vrf_choices
+    vrf_choices = VRF.objects.annotate(ipaddress_count=Count('ip_addresses'))
+    return [(v.pk, u'{} ({})'.format(v.name, v.ipaddress_count)) for v in vrf_choices]
 
 
 
 
 class IPAddressFilterForm(forms.Form, BootstrapMixin):
 class IPAddressFilterForm(forms.Form, BootstrapMixin):
     family = forms.ChoiceField(required=False, choices=ipaddress_family_choices, label='Address Family')
     family = forms.ChoiceField(required=False, choices=ipaddress_family_choices, label='Address Family')
-    vrf = forms.ChoiceField(required=False, choices=ipaddress_vrf_choices, label='VRF')
+    vrf = forms.MultipleChoiceField(required=False, choices=ipaddress_vrf_choices, label='VRF',
+                                    widget=forms.SelectMultiple(attrs={'size': 6}))
+    tenant = forms.ModelMultipleChoiceField(queryset=Tenant.objects.all(), required=False,
+                                            widget=forms.SelectMultiple(attrs={'size': 6}))
 
 
 
 
 #
 #
@@ -444,7 +479,7 @@ class VLANForm(forms.ModelForm, BootstrapMixin):
 
 
     class Meta:
     class Meta:
         model = VLAN
         model = VLAN
-        fields = ['site', 'group', 'vid', 'name', 'description', 'status', 'role']
+        fields = ['site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description']
         help_texts = {
         help_texts = {
             'site': "The site at which this VLAN exists",
             'site': "The site at which this VLAN exists",
             'group': "VLAN group (optional)",
             'group': "VLAN group (optional)",
@@ -475,13 +510,15 @@ class VLANFromCSVForm(forms.ModelForm):
                                   error_messages={'invalid_choice': 'Device not found.'})
                                   error_messages={'invalid_choice': 'Device not found.'})
     group = forms.ModelChoiceField(queryset=VLANGroup.objects.all(), required=False, to_field_name='name',
     group = forms.ModelChoiceField(queryset=VLANGroup.objects.all(), required=False, to_field_name='name',
                                    error_messages={'invalid_choice': 'VLAN group not found.'})
                                    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])
     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',
     role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False, to_field_name='name',
                                   error_messages={'invalid_choice': 'Invalid role.'})
                                   error_messages={'invalid_choice': 'Invalid role.'})
 
 
     class Meta:
     class Meta:
         model = VLAN
         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):
     def save(self, *args, **kwargs):
         m = super(VLANFromCSVForm, self).save(commit=False)
         m = super(VLANFromCSVForm, self).save(commit=False)
@@ -500,6 +537,7 @@ class VLANBulkEditForm(forms.Form, BootstrapMixin):
     pk = forms.ModelMultipleChoiceField(queryset=VLAN.objects.all(), widget=forms.MultipleHiddenInput)
     pk = forms.ModelMultipleChoiceField(queryset=VLAN.objects.all(), widget=forms.MultipleHiddenInput)
     site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False)
     site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False)
     group = forms.ModelChoiceField(queryset=VLANGroup.objects.all(), required=False)
     group = forms.ModelChoiceField(queryset=VLANGroup.objects.all(), required=False)
+    tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant')
     status = forms.ChoiceField(choices=FORM_VLAN_STATUS_CHOICES, required=False)
     status = forms.ChoiceField(choices=FORM_VLAN_STATUS_CHOICES, required=False)
     role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False)
     role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False)
     description = forms.CharField(max_length=100, required=False)
     description = forms.CharField(max_length=100, required=False)
@@ -515,6 +553,11 @@ def vlan_group_choices():
     return [(g.pk, u'{} ({})'.format(g, g.vlan_count)) for g in 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():
 def vlan_status_choices():
     status_counts = {}
     status_counts = {}
     for status in VLAN.objects.values('status').annotate(count=Count('status')).order_by('status'):
     for status in VLAN.objects.values('status').annotate(count=Count('status')).order_by('status'):
@@ -532,6 +575,8 @@ class VLANFilterForm(forms.Form, BootstrapMixin):
                                      widget=forms.SelectMultiple(attrs={'size': 8}))
                                      widget=forms.SelectMultiple(attrs={'size': 8}))
     group_id = forms.MultipleChoiceField(required=False, choices=vlan_group_choices, label='VLAN Group',
     group_id = forms.MultipleChoiceField(required=False, choices=vlan_group_choices, label='VLAN Group',
                                          widget=forms.SelectMultiple(attrs={'size': 8}))
                                          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)
     status = forms.MultipleChoiceField(required=False, choices=vlan_status_choices)
     role = forms.MultipleChoiceField(required=False, choices=vlan_role_choices,
     role = forms.MultipleChoiceField(required=False, choices=vlan_role_choices,
                                      widget=forms.SelectMultiple(attrs={'size': 8}))
                                      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'),
+        ),
+    ]

+ 27 - 0
netbox/ipam/migrations/0007_prefix_ipaddress_add_tenant.py

@@ -0,0 +1,27 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.8 on 2016-07-28 15:32
+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', '0006_vrf_vlan_add_tenant'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='ipaddress',
+            name='tenant',
+            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='ip_addresses', to='tenancy.Tenant'),
+        ),
+        migrations.AddField(
+            model_name='prefix',
+            name='tenant',
+            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='prefixes', to='tenancy.Tenant'),
+        ),
+    ]

+ 10 - 2
netbox/ipam/models.py

@@ -7,6 +7,7 @@ from django.core.validators import MaxValueValidator, MinValueValidator
 from django.db import models
 from django.db import models
 
 
 from dcim.models import Interface
 from dcim.models import Interface
+from tenancy.models import Tenant
 from utilities.models import CreatedUpdatedModel
 from utilities.models import CreatedUpdatedModel
 
 
 from .fields import IPNetworkField, IPAddressField
 from .fields import IPNetworkField, IPAddressField
@@ -46,6 +47,7 @@ class VRF(CreatedUpdatedModel):
     """
     """
     name = models.CharField(max_length=50)
     name = models.CharField(max_length=50)
     rd = models.CharField(max_length=21, unique=True, verbose_name='Route distinguisher')
     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',
     enforce_unique = models.BooleanField(default=True, verbose_name='Enforce unique space',
                                          help_text="Prevent duplicate prefixes/IP addresses within this VRF")
                                          help_text="Prevent duplicate prefixes/IP addresses within this VRF")
     description = models.CharField(max_length=100, blank=True)
     description = models.CharField(max_length=100, blank=True)
@@ -65,6 +67,8 @@ class VRF(CreatedUpdatedModel):
         return ','.join([
         return ','.join([
             self.name,
             self.name,
             self.rd,
             self.rd,
+            self.tenant.name if self.tenant else '',
+            'True' if self.enforce_unique else '',
             self.description,
             self.description,
         ])
         ])
 
 
@@ -229,6 +233,7 @@ class Prefix(CreatedUpdatedModel):
     site = models.ForeignKey('dcim.Site', related_name='prefixes', on_delete=models.PROTECT, blank=True, null=True)
     site = models.ForeignKey('dcim.Site', related_name='prefixes', on_delete=models.PROTECT, blank=True, null=True)
     vrf = models.ForeignKey('VRF', related_name='prefixes', on_delete=models.PROTECT, blank=True, null=True,
     vrf = models.ForeignKey('VRF', related_name='prefixes', on_delete=models.PROTECT, blank=True, null=True,
                             verbose_name='VRF')
                             verbose_name='VRF')
+    tenant = models.ForeignKey(Tenant, related_name='prefixes', blank=True, null=True, on_delete=models.PROTECT)
     vlan = models.ForeignKey('VLAN', related_name='prefixes', on_delete=models.PROTECT, blank=True, null=True,
     vlan = models.ForeignKey('VLAN', related_name='prefixes', on_delete=models.PROTECT, blank=True, null=True,
                              verbose_name='VLAN')
                              verbose_name='VLAN')
     status = models.PositiveSmallIntegerField('Status', choices=PREFIX_STATUS_CHOICES, default=1)
     status = models.PositiveSmallIntegerField('Status', choices=PREFIX_STATUS_CHOICES, default=1)
@@ -291,7 +296,7 @@ class Prefix(CreatedUpdatedModel):
 
 
 class IPAddress(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
     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.
     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.
     Interfaces can have zero or more IPAddresses assigned to them.
@@ -304,6 +309,7 @@ class IPAddress(CreatedUpdatedModel):
     address = IPAddressField()
     address = IPAddressField()
     vrf = models.ForeignKey('VRF', related_name='ip_addresses', on_delete=models.PROTECT, blank=True, null=True,
     vrf = models.ForeignKey('VRF', related_name='ip_addresses', on_delete=models.PROTECT, blank=True, null=True,
                             verbose_name='VRF')
                             verbose_name='VRF')
+    tenant = models.ForeignKey(Tenant, related_name='ip_addresses', blank=True, null=True, on_delete=models.PROTECT)
     interface = models.ForeignKey(Interface, related_name='ip_addresses', on_delete=models.CASCADE, blank=True,
     interface = models.ForeignKey(Interface, related_name='ip_addresses', on_delete=models.CASCADE, blank=True,
                                   null=True)
                                   null=True)
     nat_inside = models.OneToOneField('self', related_name='nat_outside', on_delete=models.SET_NULL, blank=True,
     nat_inside = models.OneToOneField('self', related_name='nat_outside', on_delete=models.SET_NULL, blank=True,
@@ -407,9 +413,10 @@ class VLAN(CreatedUpdatedModel):
         MaxValueValidator(4094)
         MaxValueValidator(4094)
     ])
     ])
     name = models.CharField(max_length=64)
     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)
     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)
     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:
     class Meta:
         ordering = ['site', 'group', 'vid']
         ordering = ['site', 'group', 'vid']
@@ -438,6 +445,7 @@ class VLAN(CreatedUpdatedModel):
             self.group.name if self.group else '',
             self.group.name if self.group else '',
             str(self.vid),
             str(self.vid),
             self.name,
             self.name,
+            self.tenant.name if self.tenant else '',
             self.get_status_display(),
             self.get_status_display(),
             self.role.name if self.role else '',
             self.role.name if self.role else '',
             self.description,
             self.description,

+ 38 - 18
netbox/ipam/tables.py

@@ -6,8 +6,10 @@ from utilities.tables import BaseTable, ToggleColumn
 from .models import Aggregate, IPAddress, Prefix, RIR, Role, VLAN, VLANGroup, VRF
 from .models import Aggregate, IPAddress, Prefix, RIR, Role, VLAN, VLANGroup, VRF
 
 
 
 
-RIR_EDIT_LINK = """
-{% if perms.ipam.change_rir %}<a href="{% url 'ipam:rir_edit' slug=record.slug %}">Edit</a>{% endif %}
+RIR_ACTIONS = """
+{% if perms.ipam.change_rir %}
+    <a href="{% url 'ipam:rir_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
+{% endif %}
 """
 """
 
 
 UTILIZATION_GRAPH = """
 UTILIZATION_GRAPH = """
@@ -15,8 +17,10 @@ UTILIZATION_GRAPH = """
 {% utilization_graph record.get_utilization %}
 {% utilization_graph record.get_utilization %}
 """
 """
 
 
-ROLE_EDIT_LINK = """
-{% if perms.ipam.change_role %}<a href="{% url 'ipam:role_edit' slug=record.slug %}">Edit</a>{% endif %}
+ROLE_ACTIONS = """
+{% if perms.ipam.change_role %}
+    <a href="{% url 'ipam:role_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
+{% endif %}
 """
 """
 
 
 PREFIX_LINK = """
 PREFIX_LINK = """
@@ -43,9 +47,19 @@ STATUS_LABEL = """
 {% endif %}
 {% endif %}
 """
 """
 
 
-VLANGROUP_EDIT_LINK = """
+VLANGROUP_ACTIONS = """
 {% if perms.ipam.change_vlangroup %}
 {% if perms.ipam.change_vlangroup %}
-    <a href="{% url 'ipam:vlangroup_edit' pk=record.pk %}">Edit</a>
+    <a href="{% url 'ipam:vlangroup_edit' pk=record.pk %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
+{% endif %}
+"""
+
+TENANT_LINK = """
+{% if record.tenant %}
+    <a href="{% url 'tenancy:tenant' slug=record.tenant.slug %}">{{ record.tenant }}</a>
+{% elif record.vrf.tenant %}
+    <a href="{% url 'tenancy:tenant' slug=record.vrf.tenant.slug %}">{{ record.vrf.tenant }}</a>*
+{% else %}
+    &mdash;
 {% endif %}
 {% endif %}
 """
 """
 
 
@@ -58,11 +72,12 @@ class VRFTable(BaseTable):
     pk = ToggleColumn()
     pk = ToggleColumn()
     name = tables.LinkColumn('ipam:vrf', args=[Accessor('pk')], verbose_name='Name')
     name = tables.LinkColumn('ipam:vrf', args=[Accessor('pk')], verbose_name='Name')
     rd = tables.Column(verbose_name='RD')
     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')
     description = tables.Column(orderable=False, verbose_name='Description')
 
 
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):
         model = VRF
         model = VRF
-        fields = ('pk', 'name', 'rd', 'description')
+        fields = ('pk', 'name', 'rd', 'tenant', 'description')
 
 
 
 
 #
 #
@@ -74,11 +89,11 @@ class RIRTable(BaseTable):
     name = tables.LinkColumn(verbose_name='Name')
     name = tables.LinkColumn(verbose_name='Name')
     aggregate_count = tables.Column(verbose_name='Aggregates')
     aggregate_count = tables.Column(verbose_name='Aggregates')
     slug = tables.Column(verbose_name='Slug')
     slug = tables.Column(verbose_name='Slug')
-    edit = tables.TemplateColumn(template_code=RIR_EDIT_LINK, verbose_name='')
+    actions = tables.TemplateColumn(template_code=RIR_ACTIONS, attrs={'td': {'class': 'text-right'}}, verbose_name='')
 
 
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):
         model = RIR
         model = RIR
-        fields = ('pk', 'name', 'aggregate_count', 'slug', 'edit')
+        fields = ('pk', 'name', 'aggregate_count', 'slug', 'actions')
 
 
 
 
 #
 #
@@ -109,11 +124,11 @@ class RoleTable(BaseTable):
     prefix_count = tables.Column(accessor=Accessor('count_prefixes'), orderable=False, verbose_name='Prefixes')
     prefix_count = tables.Column(accessor=Accessor('count_prefixes'), orderable=False, verbose_name='Prefixes')
     vlan_count = tables.Column(accessor=Accessor('count_vlans'), orderable=False, verbose_name='VLANs')
     vlan_count = tables.Column(accessor=Accessor('count_vlans'), orderable=False, verbose_name='VLANs')
     slug = tables.Column(verbose_name='Slug')
     slug = tables.Column(verbose_name='Slug')
-    edit = tables.TemplateColumn(template_code=ROLE_EDIT_LINK, verbose_name='')
+    actions = tables.TemplateColumn(template_code=ROLE_ACTIONS, attrs={'td': {'class': 'text-right'}}, verbose_name='')
 
 
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):
         model = Role
         model = Role
-        fields = ('pk', 'name', 'prefix_count', 'vlan_count', 'slug', 'edit')
+        fields = ('pk', 'name', 'prefix_count', 'vlan_count', 'slug', 'actions')
 
 
 
 
 #
 #
@@ -124,14 +139,15 @@ class PrefixTable(BaseTable):
     pk = ToggleColumn()
     pk = ToggleColumn()
     status = tables.TemplateColumn(STATUS_LABEL, verbose_name='Status')
     status = tables.TemplateColumn(STATUS_LABEL, verbose_name='Status')
     prefix = tables.TemplateColumn(PREFIX_LINK, verbose_name='Prefix')
     prefix = tables.TemplateColumn(PREFIX_LINK, verbose_name='Prefix')
-    vrf = tables.Column(orderable=False, default='Global', verbose_name='VRF')
+    vrf = tables.LinkColumn('ipam:vrf', args=[Accessor('vrf.pk')], default='Global', verbose_name='VRF')
+    tenant = tables.TemplateColumn(TENANT_LINK, verbose_name='Tenant')
     site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
     site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
     role = tables.Column(verbose_name='Role')
     role = tables.Column(verbose_name='Role')
     description = tables.Column(orderable=False, verbose_name='Description')
     description = tables.Column(orderable=False, verbose_name='Description')
 
 
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):
         model = Prefix
         model = Prefix
-        fields = ('pk', 'prefix', 'status', 'vrf', 'site', 'role', 'description')
+        fields = ('pk', 'prefix', 'status', 'vrf', 'tenant', 'site', 'role', 'description')
 
 
 
 
 class PrefixBriefTable(BaseTable):
 class PrefixBriefTable(BaseTable):
@@ -143,6 +159,7 @@ class PrefixBriefTable(BaseTable):
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):
         model = Prefix
         model = Prefix
         fields = ('prefix', 'status', 'site', 'role')
         fields = ('prefix', 'status', 'site', 'role')
+        orderable = False
 
 
 
 
 #
 #
@@ -152,7 +169,8 @@ class PrefixBriefTable(BaseTable):
 class IPAddressTable(BaseTable):
 class IPAddressTable(BaseTable):
     pk = ToggleColumn()
     pk = ToggleColumn()
     address = tables.LinkColumn('ipam:ipaddress', args=[Accessor('pk')], verbose_name='IP Address')
     address = tables.LinkColumn('ipam:ipaddress', args=[Accessor('pk')], verbose_name='IP Address')
-    vrf = tables.Column(orderable=False, default='Global', verbose_name='VRF')
+    vrf = tables.LinkColumn('ipam:vrf', args=[Accessor('vrf.pk')], default='Global', verbose_name='VRF')
+    tenant = tables.TemplateColumn(TENANT_LINK, verbose_name='Tenant')
     device = tables.LinkColumn('dcim:device', args=[Accessor('interface.device.pk')], orderable=False,
     device = tables.LinkColumn('dcim:device', args=[Accessor('interface.device.pk')], orderable=False,
                                verbose_name='Device')
                                verbose_name='Device')
     interface = tables.Column(orderable=False, verbose_name='Interface')
     interface = tables.Column(orderable=False, verbose_name='Interface')
@@ -160,7 +178,7 @@ class IPAddressTable(BaseTable):
 
 
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):
         model = IPAddress
         model = IPAddress
-        fields = ('pk', 'address', 'vrf', 'device', 'interface', 'description')
+        fields = ('pk', 'address', 'vrf', 'tenant', 'device', 'interface', 'description')
 
 
 
 
 class IPAddressBriefTable(BaseTable):
 class IPAddressBriefTable(BaseTable):
@@ -186,11 +204,12 @@ class VLANGroupTable(BaseTable):
     site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
     site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
     vlan_count = tables.Column(verbose_name='VLANs')
     vlan_count = tables.Column(verbose_name='VLANs')
     slug = tables.Column(verbose_name='Slug')
     slug = tables.Column(verbose_name='Slug')
-    edit = tables.TemplateColumn(template_code=VLANGROUP_EDIT_LINK, verbose_name='')
+    actions = tables.TemplateColumn(template_code=VLANGROUP_ACTIONS, attrs={'td': {'class': 'text-right'}},
+                                    verbose_name='')
 
 
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):
         model = VLANGroup
         model = VLANGroup
-        fields = ('pk', 'name', 'site', 'vlan_count', 'slug', 'edit')
+        fields = ('pk', 'name', 'site', 'vlan_count', 'slug', 'actions')
 
 
 
 
 #
 #
@@ -203,9 +222,10 @@ class VLANTable(BaseTable):
     site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
     site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
     group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group')
     group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group')
     name = tables.Column(verbose_name='Name')
     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')
     status = tables.TemplateColumn(STATUS_LABEL, verbose_name='Status')
     role = tables.Column(verbose_name='Role')
     role = tables.Column(verbose_name='Role')
 
 
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):
         model = VLAN
         model = VLAN
-        fields = ('pk', 'vid', 'site', 'group', 'name', 'status', 'role')
+        fields = ('pk', 'vid', 'site', 'group', 'name', 'tenant', 'status', 'role')

+ 30 - 16
netbox/ipam/views.py

@@ -36,8 +36,9 @@ def add_available_prefixes(parent, prefix_list):
 #
 #
 
 
 class VRFListView(ObjectListView):
 class VRFListView(ObjectListView):
-    queryset = VRF.objects.all()
+    queryset = VRF.objects.select_related('tenant')
     filter = filters.VRFFilter
     filter = filters.VRFFilter
+    filter_form = forms.VRFFilterForm
     table = tables.VRFTable
     table = tables.VRFTable
     edit_permissions = ['ipam.change_vrf', 'ipam.delete_vrf']
     edit_permissions = ['ipam.change_vrf', 'ipam.delete_vrf']
     template_name = 'ipam/vrf_list.html'
     template_name = 'ipam/vrf_list.html'
@@ -47,10 +48,11 @@ def vrf(request, pk):
 
 
     vrf = get_object_or_404(VRF.objects.all(), pk=pk)
     vrf = get_object_or_404(VRF.objects.all(), pk=pk)
     prefixes = Prefix.objects.filter(vrf=vrf)
     prefixes = Prefix.objects.filter(vrf=vrf)
+    prefix_table = tables.PrefixBriefTable(prefixes)
 
 
     return render(request, 'ipam/vrf.html', {
     return render(request, 'ipam/vrf.html', {
         'vrf': vrf,
         'vrf': vrf,
-        'prefixes': prefixes,
+        'prefix_table': prefix_table,
     })
     })
 
 
 
 
@@ -85,6 +87,10 @@ class VRFBulkEditView(PermissionRequiredMixin, BulkEditView):
     def update_objects(self, pk_list, form):
     def update_objects(self, pk_list, form):
 
 
         fields_to_update = {}
         fields_to_update = {}
+        if form.cleaned_data['tenant'] == 0:
+            fields_to_update['tenant'] = None
+        elif form.cleaned_data['tenant']:
+            fields_to_update['tenant'] = form.cleaned_data['tenant']
         for field in ['description']:
         for field in ['description']:
             if form.cleaned_data[field]:
             if form.cleaned_data[field]:
                 fields_to_update[field] = form.cleaned_data[field]
                 fields_to_update[field] = form.cleaned_data[field]
@@ -145,7 +151,7 @@ class AggregateListView(ObjectListView):
             if a.prefix.version == 4:
             if a.prefix.version == 4:
                 ipv4_total += a.prefix.size
                 ipv4_total += a.prefix.size
             elif a.prefix.version == 6:
             elif a.prefix.version == 6:
-                ipv6_total += a.prefix.size / 2**64
+                ipv6_total += a.prefix.size / 2 ** 64
 
 
         return {
         return {
             'ipv4_total': ipv4_total,
             'ipv4_total': ipv4_total,
@@ -248,7 +254,7 @@ class RoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 #
 #
 
 
 class PrefixListView(ObjectListView):
 class PrefixListView(ObjectListView):
-    queryset = Prefix.objects.select_related('site', 'role')
+    queryset = Prefix.objects.select_related('site', 'vrf__tenant', 'role')
     filter = filters.PrefixFilter
     filter = filters.PrefixFilter
     filter_form = forms.PrefixFilterForm
     filter_form = forms.PrefixFilterForm
     table = tables.PrefixTable
     table = tables.PrefixTable
@@ -271,7 +277,8 @@ def prefix(request, pk):
         aggregate = None
         aggregate = None
 
 
     # Count child IP addresses
     # Count child IP addresses
-    ipaddress_count = IPAddress.objects.filter(address__net_contained_or_equal=str(prefix.prefix)).count()
+    ipaddress_count = IPAddress.objects.filter(vrf=prefix.vrf, address__net_contained_or_equal=str(prefix.prefix))\
+        .count()
 
 
     # Parent prefixes table
     # Parent prefixes table
     parent_prefixes = Prefix.objects.filter(vrf=prefix.vrf, prefix__net_contains=str(prefix.prefix))\
     parent_prefixes = Prefix.objects.filter(vrf=prefix.vrf, prefix__net_contains=str(prefix.prefix))\
@@ -336,10 +343,11 @@ class PrefixBulkEditView(PermissionRequiredMixin, BulkEditView):
     def update_objects(self, pk_list, form):
     def update_objects(self, pk_list, form):
 
 
         fields_to_update = {}
         fields_to_update = {}
-        if form.cleaned_data['vrf']:
-            fields_to_update['vrf'] = form.cleaned_data['vrf']
-        elif form.cleaned_data['vrf_global']:
-            fields_to_update['vrf'] = None
+        for field in ['vrf', 'tenant']:
+            if form.cleaned_data[field] == 0:
+                fields_to_update[field] = None
+            elif form.cleaned_data[field]:
+                fields_to_update[field] = form.cleaned_data[field]
         for field in ['site', 'status', 'role', 'description']:
         for field in ['site', 'status', 'role', 'description']:
             if form.cleaned_data[field]:
             if form.cleaned_data[field]:
                 fields_to_update[field] = form.cleaned_data[field]
                 fields_to_update[field] = form.cleaned_data[field]
@@ -358,7 +366,7 @@ def prefix_ipaddresses(request, pk):
     prefix = get_object_or_404(Prefix.objects.all(), pk=pk)
     prefix = get_object_or_404(Prefix.objects.all(), pk=pk)
 
 
     # Find all IPAddresses belonging to this Prefix
     # Find all IPAddresses belonging to this Prefix
-    ipaddresses = IPAddress.objects.filter(address__net_contained_or_equal=str(prefix.prefix))\
+    ipaddresses = IPAddress.objects.filter(vrf=prefix.vrf, address__net_contained_or_equal=str(prefix.prefix))\
         .select_related('vrf', 'interface__device', 'primary_ip4_for', 'primary_ip6_for')
         .select_related('vrf', 'interface__device', 'primary_ip4_for', 'primary_ip6_for')
 
 
     ip_table = tables.IPAddressTable(ipaddresses)
     ip_table = tables.IPAddressTable(ipaddresses)
@@ -378,7 +386,7 @@ def prefix_ipaddresses(request, pk):
 #
 #
 
 
 class IPAddressListView(ObjectListView):
 class IPAddressListView(ObjectListView):
-    queryset = IPAddress.objects.select_related('vrf', 'interface__device', 'primary_ip4_for', 'primary_ip6_for')
+    queryset = IPAddress.objects.select_related('vrf__tenant', 'interface__device')
     filter = filters.IPAddressFilter
     filter = filters.IPAddressFilter
     filter_form = forms.IPAddressFilterForm
     filter_form = forms.IPAddressFilterForm
     table = tables.IPAddressTable
     table = tables.IPAddressTable
@@ -460,10 +468,11 @@ class IPAddressBulkEditView(PermissionRequiredMixin, BulkEditView):
     def update_objects(self, pk_list, form):
     def update_objects(self, pk_list, form):
 
 
         fields_to_update = {}
         fields_to_update = {}
-        if form.cleaned_data['vrf']:
-            fields_to_update['vrf'] = form.cleaned_data['vrf']
-        elif form.cleaned_data['vrf_global']:
-            fields_to_update['vrf'] = None
+        for field in ['vrf', 'tenant']:
+            if form.cleaned_data[field] == 0:
+                fields_to_update[field] = None
+            elif form.cleaned_data[field]:
+                fields_to_update[field] = form.cleaned_data[field]
         for field in ['description']:
         for field in ['description']:
             if form.cleaned_data[field]:
             if form.cleaned_data[field]:
                 fields_to_update[field] = form.cleaned_data[field]
                 fields_to_update[field] = form.cleaned_data[field]
@@ -520,10 +529,11 @@ def vlan(request, pk):
 
 
     vlan = get_object_or_404(VLAN.objects.select_related('site', 'role'), pk=pk)
     vlan = get_object_or_404(VLAN.objects.select_related('site', 'role'), pk=pk)
     prefixes = Prefix.objects.filter(vlan=vlan)
     prefixes = Prefix.objects.filter(vlan=vlan)
+    prefix_table = tables.PrefixBriefTable(prefixes)
 
 
     return render(request, 'ipam/vlan.html', {
     return render(request, 'ipam/vlan.html', {
         'vlan': vlan,
         'vlan': vlan,
-        'prefixes': prefixes,
+        'prefix_table': prefix_table,
     })
     })
 
 
 
 
@@ -558,6 +568,10 @@ class VLANBulkEditView(PermissionRequiredMixin, BulkEditView):
     def update_objects(self, pk_list, form):
     def update_objects(self, pk_list, form):
 
 
         fields_to_update = {}
         fields_to_update = {}
+        if form.cleaned_data['tenant'] == 0:
+            fields_to_update['tenant'] = None
+        elif form.cleaned_data['tenant']:
+            fields_to_update['tenant'] = form.cleaned_data['tenant']
         for field in ['site', 'group', 'status', 'role', 'description']:
         for field in ['site', 'group', 'status', 'role', 'description']:
             if form.cleaned_data[field]:
             if form.cleaned_data[field]:
                 fields_to_update[field] = form.cleaned_data[field]
                 fields_to_update[field] = form.cleaned_data[field]

+ 2 - 1
netbox/netbox/settings.py

@@ -12,7 +12,7 @@ except ImportError:
                                "the documentation.")
                                "the documentation.")
 
 
 
 
-VERSION = '1.3.2'
+VERSION = '1.4.0'
 
 
 # Import local configuration
 # Import local configuration
 for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']:
 for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']:
@@ -108,6 +108,7 @@ INSTALLED_APPS = (
     'ipam',
     'ipam',
     'extras',
     'extras',
     'secrets',
     'secrets',
+    'tenancy',
     'users',
     'users',
     'utilities',
     'utilities',
 )
 )

+ 2 - 0
netbox/netbox/urls.py

@@ -22,6 +22,7 @@ urlpatterns = [
     url(r'^dcim/', include('dcim.urls', namespace='dcim')),
     url(r'^dcim/', include('dcim.urls', namespace='dcim')),
     url(r'^ipam/', include('ipam.urls', namespace='ipam')),
     url(r'^ipam/', include('ipam.urls', namespace='ipam')),
     url(r'^secrets/', include('secrets.urls', namespace='secrets')),
     url(r'^secrets/', include('secrets.urls', namespace='secrets')),
+    url(r'^tenancy/', include('tenancy.urls', namespace='tenancy')),
     url(r'^profile/', include('users.urls', namespace='users')),
     url(r'^profile/', include('users.urls', namespace='users')),
 
 
     # API
     # API
@@ -29,6 +30,7 @@ urlpatterns = [
     url(r'^api/dcim/', include('dcim.api.urls', namespace='dcim-api')),
     url(r'^api/dcim/', include('dcim.api.urls', namespace='dcim-api')),
     url(r'^api/ipam/', include('ipam.api.urls', namespace='ipam-api')),
     url(r'^api/ipam/', include('ipam.api.urls', namespace='ipam-api')),
     url(r'^api/secrets/', include('secrets.api.urls', namespace='secrets-api')),
     url(r'^api/secrets/', include('secrets.api.urls', namespace='secrets-api')),
+    url(r'^api/tenancy/', include('tenancy.api.urls', namespace='tenancy-api')),
     url(r'^api/docs/', include('rest_framework_swagger.urls')),
     url(r'^api/docs/', include('rest_framework_swagger.urls')),
     url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')),
     url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')),
 
 

+ 7 - 2
netbox/netbox/views.py

@@ -5,16 +5,20 @@ from django.shortcuts import render
 from circuits.models import Provider, Circuit
 from circuits.models import Provider, Circuit
 from dcim.models import Site, Rack, Device, ConsolePort, PowerPort, InterfaceConnection
 from dcim.models import Site, Rack, Device, ConsolePort, PowerPort, InterfaceConnection
 from extras.models import UserAction
 from extras.models import UserAction
-from ipam.models import Aggregate, Prefix, IPAddress, VLAN
+from ipam.models import Aggregate, Prefix, IPAddress, VLAN, VRF
 from secrets.models import Secret
 from secrets.models import Secret
+from tenancy.models import Tenant
 
 
 
 
 def home(request):
 def home(request):
 
 
     stats = {
     stats = {
 
 
-        # DCIM
+        # Organization
         'site_count': Site.objects.count(),
         'site_count': Site.objects.count(),
+        'tenant_count': Tenant.objects.count(),
+
+        # DCIM
         'rack_count': Rack.objects.count(),
         'rack_count': Rack.objects.count(),
         'device_count': Device.objects.count(),
         'device_count': Device.objects.count(),
         'interface_connections_count': InterfaceConnection.objects.count(),
         'interface_connections_count': InterfaceConnection.objects.count(),
@@ -22,6 +26,7 @@ def home(request):
         'power_connections_count': PowerPort.objects.filter(power_outlet__isnull=False).count(),
         'power_connections_count': PowerPort.objects.filter(power_outlet__isnull=False).count(),
 
 
         # IPAM
         # IPAM
+        'vrf_count': VRF.objects.count(),
         'aggregate_count': Aggregate.objects.count(),
         'aggregate_count': Aggregate.objects.count(),
         'prefix_count': Prefix.objects.count(),
         'prefix_count': Prefix.objects.count(),
         'ipaddress_count': IPAddress.objects.count(),
         'ipaddress_count': IPAddress.objects.count(),

+ 12 - 0
netbox/secrets/filters.py

@@ -1,10 +1,16 @@
 import django_filters
 import django_filters
 
 
+from django.db.models import Q
+
 from .models import Secret, SecretRole
 from .models import Secret, SecretRole
 from dcim.models import Device
 from dcim.models import Device
 
 
 
 
 class SecretFilter(django_filters.FilterSet):
 class SecretFilter(django_filters.FilterSet):
+    q = django_filters.MethodFilter(
+        action='search',
+        label='Search',
+    )
     role_id = django_filters.ModelMultipleChoiceFilter(
     role_id = django_filters.ModelMultipleChoiceFilter(
         name='role',
         name='role',
         queryset=SecretRole.objects.all(),
         queryset=SecretRole.objects.all(),
@@ -26,3 +32,9 @@ class SecretFilter(django_filters.FilterSet):
     class Meta:
     class Meta:
         model = Secret
         model = Secret
         fields = ['name', 'role_id', 'role', 'device']
         fields = ['name', 'role_id', 'role', 'device']
+
+    def search(self, queryset, value):
+        return queryset.filter(
+            Q(name__icontains=value) |
+            Q(device__name__icontains=value)
+        )

+ 42 - 0
netbox/secrets/fixtures/initial_data.json

@@ -0,0 +1,42 @@
+[
+{
+    "model": "secrets.secretrole",
+    "pk": 1,
+    "fields": {
+        "name": "Login Credentials",
+        "slug": "login-credentials",
+        "users": [],
+        "groups": []
+    }
+},
+{
+    "model": "secrets.secretrole",
+    "pk": 2,
+    "fields": {
+        "name": "RADIUS Key",
+        "slug": "radius-key",
+        "users": [],
+        "groups": []
+    }
+},
+{
+    "model": "secrets.secretrole",
+    "pk": 3,
+    "fields": {
+        "name": "SNMPv2 Community",
+        "slug": "snmpv2-community",
+        "users": [],
+        "groups": []
+    }
+},
+{
+    "model": "secrets.secretrole",
+    "pk": 4,
+    "fields": {
+        "name": "SNMPv3 Credentials",
+        "slug": "snmpv3-credentials",
+        "users": [],
+        "groups": []
+    }
+}
+]

+ 5 - 4
netbox/secrets/tables.py

@@ -6,9 +6,9 @@ from utilities.tables import BaseTable, ToggleColumn
 from .models import SecretRole, Secret
 from .models import SecretRole, Secret
 
 
 
 
-SECRETROLE_EDIT_LINK = """
+SECRETROLE_ACTIONS = """
 {% if perms.secrets.change_secretrole %}
 {% if perms.secrets.change_secretrole %}
-    <a href="{% url 'secrets:secretrole_edit' slug=record.slug %}">Edit</a>
+    <a href="{% url 'secrets:secretrole_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
 {% endif %}
 {% endif %}
 """
 """
 
 
@@ -22,11 +22,12 @@ class SecretRoleTable(BaseTable):
     name = tables.LinkColumn(verbose_name='Name')
     name = tables.LinkColumn(verbose_name='Name')
     secret_count = tables.Column(verbose_name='Secrets')
     secret_count = tables.Column(verbose_name='Secrets')
     slug = tables.Column(verbose_name='Slug')
     slug = tables.Column(verbose_name='Slug')
-    edit = tables.TemplateColumn(template_code=SECRETROLE_EDIT_LINK, verbose_name='')
+    actions = tables.TemplateColumn(template_code=SECRETROLE_ACTIONS, attrs={'td': {'class': 'text-right'}},
+                                    verbose_name='')
 
 
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):
         model = SecretRole
         model = SecretRole
-        fields = ('pk', 'name', 'secret_count', 'slug', 'edit')
+        fields = ('pk', 'name', 'secret_count', 'slug', 'actions')
 
 
 
 
 #
 #

+ 1 - 1
netbox/templates/500.html

@@ -13,7 +13,7 @@
             <div class="panel panel-danger" style="margin-top: 200px">
             <div class="panel panel-danger" style="margin-top: 200px">
                 <div class="panel-heading">
                 <div class="panel-heading">
                     <strong>
                     <strong>
-                        <i class="glyphicon glyphicon-warning-sign"></i>
+                        <i class="fa fa-warning"></i>
                         Server Error
                         Server Error
                     </strong>
                     </strong>
                 </div>
                 </div>

+ 79 - 70
netbox/templates/_base.html

@@ -24,173 +24,182 @@
             <div id="navbar" class="navbar-collapse collapse">
             <div id="navbar" class="navbar-collapse collapse">
                 {% if request.user.is_authenticated or not settings.LOGIN_REQUIRED %}
                 {% if request.user.is_authenticated or not settings.LOGIN_REQUIRED %}
                 <ul class="nav navbar-nav">
                 <ul class="nav navbar-nav">
-                    <li class="dropdown{% if request.path|startswith:'/dcim/sites/' %} active{% endif %}">
-                        {% if perms.dcim.add_site %}
-                            <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Sites <span class="caret"></span></a>
-                            <ul class="dropdown-menu">
-                                <li><a href="{% url 'dcim:site_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> Sites</a></li>
-                                <li><a href="{% url 'dcim:site_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a Site</a></li>
-                                <li><a href="{% url 'dcim:site_import' %}"><i class="glyphicon glyphicon-import" aria-hidden="true"></i> Import Sites</a></li>
-                            </ul>
-                        {% else %}
-                            <a href="{% url 'dcim:site_list' %}">Sites</a>
-                        {% endif %}
+                    <li class="dropdown{% if request.path|startswith:'/dcim/sites/' or 'tenancy' in request.path %} active{% endif %}">
+                        <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Organization <span class="caret"></span></a>
+                        <ul class="dropdown-menu">
+                            <li><a href="{% url 'dcim:site_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Sites</a></li>
+                            {% if perms.dcim.add_site %}
+                                <li><a href="{% url 'dcim:site_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Site</a></li>
+                                <li><a href="{% url 'dcim:site_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Sites</a></li>
+                            {% endif %}
+                            <li class="divider"></li>
+                            <li><a href="{% url 'tenancy:tenant_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Tenants</a></li>
+                            {% if perms.tenancy.add_tenant %}
+                                <li><a href="{% url 'tenancy:tenant_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Tenant</a></li>
+                                <li><a href="{% url 'tenancy:tenant_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Tenants</a></li>
+                            {% endif %}
+                            <li class="divider"></li>
+                            <li><a href="{% url 'tenancy:tenantgroup_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Tenant Groups</a></li>
+                            {% if perms.tenancy.add_tenantgroup %}
+                                <li><a href="{% url 'tenancy:tenantgroup_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Tenant Group</a></li>
+                            {% endif %}
+                        </ul>
                     </li>
                     </li>
                     <li class="dropdown{% if request.path|startswith:'/dcim/rack' %} active{% endif %}">
                     <li class="dropdown{% if request.path|startswith:'/dcim/rack' %} active{% endif %}">
                         <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Racks <span class="caret"></span></a>
                         <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Racks <span class="caret"></span></a>
                         <ul class="dropdown-menu">
                         <ul class="dropdown-menu">
-                            <li><a href="{% url 'dcim:rack_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> Racks</a></li>
+                            <li><a href="{% url 'dcim:rack_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Racks</a></li>
                             {% if perms.dcim.add_rack %}
                             {% if perms.dcim.add_rack %}
-                                <li><a href="{% url 'dcim:rack_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a Rack</a></li>
-                                <li><a href="{% url 'dcim:rack_import' %}"><i class="glyphicon glyphicon-import" aria-hidden="true"></i> Import Racks</a></li>
+                                <li><a href="{% url 'dcim:rack_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Rack</a></li>
+                                <li><a href="{% url 'dcim:rack_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Racks</a></li>
                             {% endif %}
                             {% endif %}
                             <li class="divider"></li>
                             <li class="divider"></li>
-                            <li><a href="{% url 'dcim:rackgroup_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> Rack Groups</a></li>
+                            <li><a href="{% url 'dcim:rackgroup_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Rack Groups</a></li>
                             {% if perms.dcim.add_rackgroup %}
                             {% if perms.dcim.add_rackgroup %}
-                                <li><a href="{% url 'dcim:rackgroup_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a Rack Group</a></li>
+                                <li><a href="{% url 'dcim:rackgroup_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Rack Group</a></li>
                             {% endif %}
                             {% endif %}
                         </ul>
                         </ul>
                     </li>
                     </li>
                     <li class="dropdown{% if request.path|startswith:'/dcim/device' or request.path|startswith:'/dcim/manufacturers/' or request.path|startswith:'/dcim/platforms/' %} active{% endif %}">
                     <li class="dropdown{% if request.path|startswith:'/dcim/device' or request.path|startswith:'/dcim/manufacturers/' or request.path|startswith:'/dcim/platforms/' %} active{% endif %}">
                         <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Devices <span class="caret"></span></a>
                         <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Devices <span class="caret"></span></a>
                         <ul class="dropdown-menu">
                         <ul class="dropdown-menu">
-                            <li><a href="{% url 'dcim:device_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> Devices</a></li>
+                            <li><a href="{% url 'dcim:device_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Devices</a></li>
                             {% if perms.dcim.add_device %}
                             {% if perms.dcim.add_device %}
-                                <li><a href="{% url 'dcim:device_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a Device</a></li>
-                                <li><a href="{% url 'dcim:device_import' %}"><i class="glyphicon glyphicon-import" aria-hidden="true"></i> Import Devices</a></li>
+                                <li><a href="{% url 'dcim:device_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Device</a></li>
+                                <li><a href="{% url 'dcim:device_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Devices</a></li>
                             {% endif %}
                             {% endif %}
                             {% if perms.ipam.add_device or perms.ipam.add_devicetype %}
                             {% if perms.ipam.add_device or perms.ipam.add_devicetype %}
                                 <li class="divider"></li>
                                 <li class="divider"></li>
                             {% endif %}
                             {% endif %}
-                            <li><a href="{% url 'dcim:devicetype_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> Device Types</a></li>
+                            <li><a href="{% url 'dcim:devicetype_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Device Types</a></li>
                             {% if perms.dcim.add_devicetype %}
                             {% if perms.dcim.add_devicetype %}
-                                <li><a href="{% url 'dcim:devicetype_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a Device Type</a></li>
+                                <li><a href="{% url 'dcim:devicetype_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Device Type</a></li>
                             {% endif %}
                             {% endif %}
                             <li class="divider"></li>
                             <li class="divider"></li>
-                            <li><a href="{% url 'dcim:devicerole_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> Device Roles</a></li>
+                            <li><a href="{% url 'dcim:devicerole_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Device Roles</a></li>
                             {% if perms.dcim.add_devicerole %}
                             {% if perms.dcim.add_devicerole %}
-                                <li><a href="{% url 'dcim:devicerole_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a Device Role</a></li>
+                                <li><a href="{% url 'dcim:devicerole_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Device Role</a></li>
                             {% endif %}
                             {% endif %}
                             {% if perms.dcim.add_devicerole or perms.dcim.add_manufacturer %}
                             {% if perms.dcim.add_devicerole or perms.dcim.add_manufacturer %}
                                 <li class="divider"></li>
                                 <li class="divider"></li>
                             {% endif %}
                             {% endif %}
-                            <li><a href="{% url 'dcim:manufacturer_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> Manufacturers</a></li>
+                            <li><a href="{% url 'dcim:manufacturer_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Manufacturers</a></li>
                             {% if perms.dcim.add_manufacturer %}
                             {% if perms.dcim.add_manufacturer %}
-                                <li><a href="{% url 'dcim:manufacturer_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a Manufacturer</a></li>
+                                <li><a href="{% url 'dcim:manufacturer_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Manufacturer</a></li>
                             {% endif %}
                             {% endif %}
                             {% if perms.dcim.add_manufacturer or perms.dcim.add_platform %}
                             {% if perms.dcim.add_manufacturer or perms.dcim.add_platform %}
                                 <li class="divider"></li>
                                 <li class="divider"></li>
                             {% endif %}
                             {% endif %}
-                            <li><a href="{% url 'dcim:platform_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> Platforms</a></li>
+                            <li><a href="{% url 'dcim:platform_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Platforms</a></li>
                             {% if perms.dcim.add_platform %}
                             {% if perms.dcim.add_platform %}
-                                <li><a href="{% url 'dcim:platform_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a Platform</a></li>
+                                <li><a href="{% url 'dcim:platform_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Platform</a></li>
                             {% endif %}
                             {% endif %}
                         </ul>
                         </ul>
                     </li>
                     </li>
                     <li class="dropdown{% if request.path|startswith:'/dcim/console-connections/' or request.path|startswith:'/dcim/power-connections/' or request.path|startswith:'/dcim/interface-connections/' %} active{% endif %}">
                     <li class="dropdown{% if request.path|startswith:'/dcim/console-connections/' or request.path|startswith:'/dcim/power-connections/' or request.path|startswith:'/dcim/interface-connections/' %} active{% endif %}">
                         <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Connections <span class="caret"></span></a>
                         <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Connections <span class="caret"></span></a>
                         <ul class="dropdown-menu">
                         <ul class="dropdown-menu">
-                            <li><a href="{% url 'dcim:console_connections_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> Console Connections</a></li>
+                            <li><a href="{% url 'dcim:console_connections_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Console Connections</a></li>
                             {% if perms.dcim.change_consoleport %}
                             {% if perms.dcim.change_consoleport %}
-                                <li><a href="{% url 'dcim:console_connections_import' %}"><i class="glyphicon glyphicon-import" aria-hidden="true"></i> Import Console Connections</a></li>
+                                <li><a href="{% url 'dcim:console_connections_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Console Connections</a></li>
                             {% endif %}
                             {% endif %}
                             {% if perms.ipam.change_consoleport or perms.ipam.change_powerport %}
                             {% if perms.ipam.change_consoleport or perms.ipam.change_powerport %}
                                 <li class="divider"></li>
                                 <li class="divider"></li>
                             {% endif %}
                             {% endif %}
-                            <li><a href="{% url 'dcim:power_connections_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> Power Connections</a></li>
+                            <li><a href="{% url 'dcim:power_connections_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Power Connections</a></li>
                             {% if perms.dcim.change_powerport %}
                             {% if perms.dcim.change_powerport %}
-                                <li><a href="{% url 'dcim:power_connections_import' %}"><i class="glyphicon glyphicon-import" aria-hidden="true"></i> Import Power Connections</a></li>
+                                <li><a href="{% url 'dcim:power_connections_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Power Connections</a></li>
                             {% endif %}
                             {% endif %}
                             {% if perms.ipam.change_powerport or perms.ipam.add_interfaceconnection %}
                             {% if perms.ipam.change_powerport or perms.ipam.add_interfaceconnection %}
                                 <li class="divider"></li>
                                 <li class="divider"></li>
                             {% endif %}
                             {% endif %}
-                            <li><a href="{% url 'dcim:interface_connections_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> Interface Connections</a></li>
+                            <li><a href="{% url 'dcim:interface_connections_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Interface Connections</a></li>
                             {% if perms.dcim.add_interfaceconnection %}
                             {% if perms.dcim.add_interfaceconnection %}
-                                <li><a href="{% url 'dcim:interface_connections_import' %}"><i class="glyphicon glyphicon-import" aria-hidden="true"></i> Import Interface Connections</a></li>
+                                <li><a href="{% url 'dcim:interface_connections_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Interface Connections</a></li>
                             {% endif %}
                             {% endif %}
                         </ul>
                         </ul>
                     </li>
                     </li>
                     <li class="dropdown{% if request.path|startswith:'/ipam/' and not request.path|startswith:'/ipam/vlan' %} 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>
                         <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">
                         <ul class="dropdown-menu">
-                            <li><a href="{% url 'ipam:ipaddress_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> IP Addresses</a></li>
+                            <li><a href="{% url 'ipam:ipaddress_list' %}"><i class="fa fa-search" aria-hidden="true"></i> IP Addresses</a></li>
                             {% if perms.ipam.add_ipaddress %}
                             {% if perms.ipam.add_ipaddress %}
-                                <li><a href="{% url 'ipam:ipaddress_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add an IP</a></li>
-                                <li><a href="{% url 'ipam:ipaddress_import' %}"><i class="glyphicon glyphicon-import" aria-hidden="true"></i> Import IPs</a></li>
+                                <li><a href="{% url 'ipam:ipaddress_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add an IP</a></li>
+                                <li><a href="{% url 'ipam:ipaddress_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import IPs</a></li>
                             {% endif %}
                             {% endif %}
                             {% if perms.ipam.add_ipaddress or perms.ipam.add_prefix %}
                             {% if perms.ipam.add_ipaddress or perms.ipam.add_prefix %}
                                 <li class="divider"></li>
                                 <li class="divider"></li>
                             {% endif %}
                             {% endif %}
-                            <li><a href="{% url 'ipam:prefix_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> Prefixes</a></li>
+                            <li><a href="{% url 'ipam:prefix_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Prefixes</a></li>
                             {% if perms.ipam.add_prefix %}
                             {% if perms.ipam.add_prefix %}
-                                <li><a href="{% url 'ipam:prefix_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a Prefix</a></li>
-                                <li><a href="{% url 'ipam:prefix_import' %}"><i class="glyphicon glyphicon-import" aria-hidden="true"></i> Import Prefixes</a></li>
+                                <li><a href="{% url 'ipam:prefix_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Prefix</a></li>
+                                <li><a href="{% url 'ipam:prefix_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Prefixes</a></li>
                             {% endif %}
                             {% endif %}
                             {% if perms.ipam.add_prefix or perms.ipam.add_aggregate %}
                             {% if perms.ipam.add_prefix or perms.ipam.add_aggregate %}
                                 <li class="divider"></li>
                                 <li class="divider"></li>
                             {% endif %}
                             {% endif %}
-                            <li><a href="{% url 'ipam:aggregate_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> Aggregates</a></li>
+                            <li><a href="{% url 'ipam:aggregate_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Aggregates</a></li>
                             {% if perms.ipam.add_aggregate %}
                             {% if perms.ipam.add_aggregate %}
-                                <li><a href="{% url 'ipam:aggregate_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add an Aggregate</a></li>
-                                <li><a href="{% url 'ipam:aggregate_import' %}"><i class="glyphicon glyphicon-import" aria-hidden="true"></i> Import Aggregates</a></li>
+                                <li><a href="{% url 'ipam:aggregate_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add an Aggregate</a></li>
+                                <li><a href="{% url 'ipam:aggregate_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Aggregates</a></li>
                             {% endif %}
                             {% endif %}
                             {% if perms.ipam.add_aggregate or perms.ipam.add_vrf %}
                             {% if perms.ipam.add_aggregate or perms.ipam.add_vrf %}
                                 <li class="divider"></li>
                                 <li class="divider"></li>
                             {% endif %}
                             {% endif %}
-                            <li><a href="{% url 'ipam:vrf_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> VRFs</a></li>
+                            <li><a href="{% url 'ipam:vrf_list' %}"><i class="fa fa-search" aria-hidden="true"></i> VRFs</a></li>
                             {% if perms.ipam.add_vrf %}
                             {% if perms.ipam.add_vrf %}
-                                <li><a href="{% url 'ipam:vrf_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a VRF</a></li>
-                                <li><a href="{% url 'ipam:vrf_import' %}"><i class="glyphicon glyphicon-import" aria-hidden="true"></i> Import VRFs</a></li>
+                                <li><a href="{% url 'ipam:vrf_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a VRF</a></li>
+                                <li><a href="{% url 'ipam:vrf_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import VRFs</a></li>
                             {% endif %}
                             {% endif %}
                             <li class="divider"></li>
                             <li class="divider"></li>
-                            <li><a href="{% url 'ipam:rir_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> RIRs</a></li>
+                            <li><a href="{% url 'ipam:rir_list' %}"><i class="fa fa-search" aria-hidden="true"></i> RIRs</a></li>
                             {% if perms.ipam.add_rir %}
                             {% if perms.ipam.add_rir %}
-                                <li><a href="{% url 'ipam:rir_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a RIR</a></li>
+                                <li><a href="{% url 'ipam:rir_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a RIR</a></li>
                             {% endif %}
                             {% endif %}
                             {% if perms.ipam.add_rir or perms.ipam.add_role %}
                             {% if perms.ipam.add_rir or perms.ipam.add_role %}
                                 <li class="divider"></li>
                                 <li class="divider"></li>
                             {% endif %}
                             {% endif %}
-                            <li><a href="{% url 'ipam:role_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> Prefix/VLAN Roles</a></li>
+                            <li><a href="{% url 'ipam:role_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Prefix/VLAN Roles</a></li>
                             {% if perms.ipam.add_role %}
                             {% if perms.ipam.add_role %}
-                                <li><a href="{% url 'ipam:role_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a Role</a></li>
+                                <li><a href="{% url 'ipam:role_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Role</a></li>
                             {% endif %}
                             {% endif %}
                         </ul>
                         </ul>
                     </li>
                     </li>
                     <li class="dropdown{% if request.path|startswith:'/ipam/vlan' %} active{% endif %}">
                     <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>
                         <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">
                         <ul class="dropdown-menu">
-                            <li><a href="{% url 'ipam:vlan_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> VLANs</a></li>
+                            <li><a href="{% url 'ipam:vlan_list' %}"><i class="fa fa-search" aria-hidden="true"></i> VLANs</a></li>
                             {% if perms.ipam.add_vlan %}
                             {% 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>
+                                <li><a href="{% url 'ipam:vlan_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a VLAN</a></li>
+                                <li><a href="{% url 'ipam:vlan_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import VLANs</a></li>
                             {% endif %}
                             {% endif %}
                             <li class="divider"></li>
                             <li class="divider"></li>
-                            <li><a href="{% url 'ipam:vlangroup_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> VLAN Groups</a></li>
+                            <li><a href="{% url 'ipam:vlangroup_list' %}"><i class="fa fa-search" aria-hidden="true"></i> VLAN Groups</a></li>
                             {% if perms.ipam.add_vlangroup %}
                             {% 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>
+                                <li><a href="{% url 'ipam:vlangroup_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a VLAN Group</a></li>
                             {% endif %}
                             {% endif %}
                         </ul>
                         </ul>
                     </li>
                     </li>
                     <li class="dropdown{% if request.path|startswith:'/circuits/' %} active{% endif %}">
                     <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>
                         <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Circuits <span class="caret"></span></a>
                         <ul class="dropdown-menu">
                         <ul class="dropdown-menu">
-                            <li><a href="{% url 'circuits:provider_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> Providers</a></li>
+                            <li><a href="{% url 'circuits:provider_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Providers</a></li>
                             {% if perms.circuits.add_provider %}
                             {% if perms.circuits.add_provider %}
-                                <li><a href="{% url 'circuits:provider_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a Provider</a></li>
-                                <li><a href="{% url 'circuits:provider_import' %}"><i class="glyphicon glyphicon-import" aria-hidden="true"></i> Import Providers</a></li>
+                                <li><a href="{% url 'circuits:provider_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Provider</a></li>
+                                <li><a href="{% url 'circuits:provider_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Providers</a></li>
                             {% endif %}
                             {% endif %}
                             {% if perms.circuits.add_circuit or perms.circuits.add_provider %}
                             {% if perms.circuits.add_circuit or perms.circuits.add_provider %}
                                 <li class="divider"></li>
                                 <li class="divider"></li>
                             {% endif %}
                             {% endif %}
-                            <li><a href="{% url 'circuits:circuit_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> Circuits</a></li>
+                            <li><a href="{% url 'circuits:circuit_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Circuits</a></li>
                             {% if perms.circuits.add_circuit %}
                             {% if perms.circuits.add_circuit %}
-                                <li><a href="{% url 'circuits:circuit_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a Circuit</a></li>
-                                <li><a href="{% url 'circuits:circuit_import' %}"><i class="glyphicon glyphicon-import" aria-hidden="true"></i> Import Circuits</a></li>
+                                <li><a href="{% url 'circuits:circuit_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Circuit</a></li>
+                                <li><a href="{% url 'circuits:circuit_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Circuits</a></li>
                             {% endif %}
                             {% endif %}
                             <li class="divider"></li>
                             <li class="divider"></li>
-                            <li><a href="{% url 'circuits:circuittype_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> Circuit Types</a></li>
+                            <li><a href="{% url 'circuits:circuittype_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Circuit Types</a></li>
                             {% if perms.circuits.add_circuittype %}
                             {% if perms.circuits.add_circuittype %}
-                                <li><a href="{% url 'circuits:circuittype_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a Circuit Type</a></li>
+                                <li><a href="{% url 'circuits:circuittype_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Circuit Type</a></li>
                             {% endif %}
                             {% endif %}
                         </ul>
                         </ul>
                     </li>
                     </li>
@@ -198,14 +207,14 @@
                         <li class="dropdown{% if request.path|startswith:'/secrets/' %} active{% endif %}">
                         <li class="dropdown{% if request.path|startswith:'/secrets/' %} active{% endif %}">
                             <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Secrets <span class="caret"></span></a>
                             <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Secrets <span class="caret"></span></a>
                             <ul class="dropdown-menu">
                             <ul class="dropdown-menu">
-                                <li><a href="{% url 'secrets:secret_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> Secrets</a></li>
+                                <li><a href="{% url 'secrets:secret_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Secrets</a></li>
                                 {% if perms.secrets.add_secret %}
                                 {% if perms.secrets.add_secret %}
-                                    <li><a href="{% url 'secrets:secret_import' %}"><i class="glyphicon glyphicon-import" aria-hidden="true"></i> Import Secrets</a></li>
+                                    <li><a href="{% url 'secrets:secret_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Secrets</a></li>
                                 {% endif %}
                                 {% endif %}
                                 <li class="divider"></li>
                                 <li class="divider"></li>
-                                <li><a href="{% url 'secrets:secretrole_list' %}"><i class="glyphicon glyphicon-search" aria-hidden="true"></i> Secret Roles</a></li>
+                                <li><a href="{% url 'secrets:secretrole_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Secret Roles</a></li>
                                 {% if perms.secrets.add_secretrole %}
                                 {% if perms.secrets.add_secretrole %}
-                                    <li><a href="{% url 'secrets:secretrole_add' %}"><i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add a Secret Role</a></li>
+                                    <li><a href="{% url 'secrets:secretrole_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Secret Role</a></li>
                                 {% endif %}
                                 {% endif %}
                             </ul>
                             </ul>
                         </li>
                         </li>
@@ -215,12 +224,12 @@
                 <ul class="nav navbar-nav navbar-right">
                 <ul class="nav navbar-nav navbar-right">
                     {% if request.user.is_authenticated %}
                     {% if request.user.is_authenticated %}
                         {% if request.user.is_staff %}
                         {% if request.user.is_staff %}
-                            <li><a href="{% url 'admin:index' %}"><i class="glyphicon glyphicon-cog" aria-hidden="true"></i> Admin</a></li>
+                            <li><a href="{% url 'admin:index' %}"><i class="fa fa-cogs" aria-hidden="true"></i> Admin</a></li>
                         {% endif %}
                         {% endif %}
-                        <li><a href="{% url 'users:profile' %}"><i class="glyphicon glyphicon-user" aria-hidden="true"></i> Profile</a></li>
-                        <li><a href="{% url 'logout' %}"><i class="glyphicon glyphicon-log-out" aria-hidden="true"></i> Log out</a></li>
+                        <li><a href="{% url 'users:profile' %}"><i class="fa fa-user" aria-hidden="true"></i> Profile</a></li>
+                        <li><a href="{% url 'logout' %}"><i class="fa fa-sign-out" aria-hidden="true"></i> Log out</a></li>
                     {% else %}
                     {% else %}
-                        <li><a href="{% url 'login' %}?next={{ request.path }}"><i class="glyphicon glyphicon-log-in" aria-hidden="true"></i> Log in</a></li>
+                        <li><a href="{% url 'login' %}?next={{ request.path }}"><i class="fa fa-sign-in" aria-hidden="true"></i> Log in</a></li>
                     {% endif %}
                     {% endif %}
                 </ul>
                 </ul>
             </div>
             </div>

+ 80 - 29
netbox/templates/circuits/circuit.html

@@ -1,24 +1,24 @@
 {% extends '_base.html' %}
 {% extends '_base.html' %}
 {% load helpers %}
 {% load helpers %}
 
 
-{% block title %}{{ circuit.provider }} Circuit {{ circuit.cid }}{% endblock %}
+{% block title %}{{ circuit.provider }} - {{ circuit.cid }}{% endblock %}
 
 
 {% block content %}
 {% block content %}
 <div class="row">
 <div class="row">
     <div class="col-md-9">
     <div class="col-md-9">
         <ol class="breadcrumb">
         <ol class="breadcrumb">
-            <li><a href="{% url 'dcim:site' slug=circuit.site.slug %}">{{ circuit.site }}</a></li>
-            <li><a href="{% url 'circuits:circuit_list' %}?site={{ circuit.site.slug }}">Circuits</a></li>
-            <li>{{ circuit }}</li>
+            <li><a href="{% url 'circuits:circuit_list' %}">Circuits</a></li>
+            <li><a href="{% url 'circuits:circuit_list' %}?provider={{ circuit.provider.slug }}">{{ circuit.provider }}</a></li>
+            <li>{{ circuit.cid }}</li>
         </ol>
         </ol>
     </div>
     </div>
     <div class="col-md-3">
     <div class="col-md-3">
         <form action="{% url 'circuits:circuit_list' %}" method="get">
         <form action="{% url 'circuits:circuit_list' %}" method="get">
             <div class="input-group">
             <div class="input-group">
-                <input type="text" name="q" class="form-control" placeholder="Circuit ID" />
+                <input type="text" name="q" class="form-control" />
                 <span class="input-group-btn">
                 <span class="input-group-btn">
                     <button type="submit" class="btn btn-primary">
                     <button type="submit" class="btn btn-primary">
-                        <span class="glyphicon glyphicon-search" aria-hidden="true"></span>
+                        <span class="fa fa-search" aria-hidden="true"></span>
                     </button>
                     </button>
                 </span>
                 </span>
             </div>
             </div>
@@ -28,18 +28,18 @@
 <div class="pull-right">
 <div class="pull-right">
     {% if perms.circuits.change_circuit %}
     {% if perms.circuits.change_circuit %}
 		<a href="{% url 'circuits:circuit_edit' pk=circuit.pk %}" class="btn btn-warning">
 		<a href="{% url 'circuits:circuit_edit' pk=circuit.pk %}" class="btn btn-warning">
-			<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span>
+			<span class="fa fa-pencil" aria-hidden="true"></span>
 			Edit this circuit
 			Edit this circuit
 		</a>
 		</a>
     {% endif %}
     {% endif %}
     {% if perms.circuits.delete_circuit %}
     {% if perms.circuits.delete_circuit %}
 		<a href="{% url 'circuits:circuit_delete' pk=circuit.pk %}" class="btn btn-danger">
 		<a href="{% url 'circuits:circuit_delete' pk=circuit.pk %}" class="btn btn-danger">
-			<span class="glyphicon glyphicon-trash" aria-hidden="true"></span>
+			<span class="fa fa-trash" aria-hidden="true"></span>
 			Delete this circuit
 			Delete this circuit
 		</a>
 		</a>
     {% endif %}
     {% endif %}
 </div>
 </div>
-<h1>{{ circuit.provider }} Circuit {{ circuit.cid }}</h1>
+<h1>{{ circuit.provider }} - {{ circuit.cid }}</h1>
 <div class="row">
 <div class="row">
 	<div class="col-md-6">
 	<div class="col-md-6">
         <div class="panel panel-default">
         <div class="panel panel-default">
@@ -58,40 +58,48 @@
                     <td>{{ circuit.cid }}</td>
                     <td>{{ circuit.cid }}</td>
                 </tr>
                 </tr>
                 <tr>
                 <tr>
-                    <td>Site</td>
-                    <td>
-                        <a href="{% url 'dcim:site' slug=circuit.site.slug %}">{{ circuit.site }}</a>
-                    </td>
+                    <td>Type</td>
+                    <td><a href="{{ circuit.type.get_absolute_url }}">{{ circuit.type }}</a></td>
                 </tr>
                 </tr>
                 <tr>
                 <tr>
-                    <td>Termination</td>
+                    <td>Tenant</td>
                     <td>
                     <td>
-                        {% if circuit.interface %}
-                            <span><a href="{% url 'dcim:device' pk=circuit.interface.device.pk %}">{{ circuit.interface.device }}</a> {{ circuit.interface }}</span>
+                        {% if circuit.tenant %}
+                            <a href="{{ circuit.tenant.get_absolute_url }}">{{ circuit.tenant }}</a>
                         {% else %}
                         {% else %}
-                            <span class="text-muted">Not defined</span>
+                            <span class="text-muted">None</span>
                         {% endif %}
                         {% endif %}
                     </td>
                     </td>
                 </tr>
                 </tr>
                 <tr>
                 <tr>
                     <td>Install Date</td>
                     <td>Install Date</td>
-                    <td>{{ circuit.install_date }}</td>
+                    <td>
+                        {% if circuit.install_date %}
+                            {{ circuit.install_date }}
+                        {% else %}
+                            <span class="text-muted">N/A</span>
+                        {% endif %}
+                    </td>
                 </tr>
                 </tr>
                 <tr>
                 <tr>
                     <td>Port Speed</td>
                     <td>Port Speed</td>
-                    <td>{{ circuit.port_speed_human }}</td>
+                    <td>
+                        {% if circuit.port_speed %}
+                            {{ circuit.port_speed_human }}
+                        {% else %}
+                            <span class="text-muted">N/A</span>
+                        {% endif %}
+                    </td>
                 </tr>
                 </tr>
                 <tr>
                 <tr>
                     <td>Commit Rate</td>
                     <td>Commit Rate</td>
-                    <td>{% if circuit.commit_rate %}{{ circuit.commit_rate_human }}{% else %}<span class="text-muted">N/A</span>{% endif %}</td>
-                </tr>
-                <tr>
-                    <td>Cross-Connect</td>
-                    <td>{{ circuit.xconnect_id }}</td>
-                </tr>
-                <tr>
-                    <td>Patch Panel/Port</td>
-                    <td>{{ circuit.pp_info }}</td>
+                    <td>
+                        {% if circuit.commit_speed %}
+                            {{ circuit.commit_speed_human }}
+                        {% else %}
+                            <span class="text-muted">N/A</span>
+                        {% endif %}
+                    </td>
                 </tr>
                 </tr>
                 <tr>
                 <tr>
                     <td>Created</td>
                     <td>Created</td>
@@ -105,12 +113,55 @@
         </div>
         </div>
 	</div>
 	</div>
 	<div class="col-md-6">
 	<div class="col-md-6">
+        <div class="panel panel-default">
+            <div class="panel-heading">
+                <strong>Termination</strong>
+            </div>
+            <table class="table table-hover panel-body">
+                <tr>
+                    <td>Site</td>
+                    <td>
+                        <a href="{% url 'dcim:site' slug=circuit.site.slug %}">{{ circuit.site }}</a>
+                    </td>
+                </tr>
+                <tr>
+                    <td>Termination</td>
+                    <td>
+                        {% if circuit.interface %}
+                            <span><a href="{% url 'dcim:device' pk=circuit.interface.device.pk %}">{{ circuit.interface.device }}</a> {{ circuit.interface }}</span>
+                        {% else %}
+                            <span class="text-muted">Not defined</span>
+                        {% endif %}
+                    </td>
+                </tr>
+                <tr>
+                    <td>Cross-Connect</td>
+                    <td>
+                        {% if circuit.xconnect_id %}
+                            {{ circuit.xconnect_id }}
+                        {% else %}
+                            <span class="text-muted">N/A</span>
+                        {% endif %}
+                    </td>
+                </tr>
+                <tr>
+                    <td>Patch Panel/Port</td>
+                    <td>
+                        {% if circuit.pp_info %}
+                            {{ circuit.pp_info }}
+                        {% else %}
+                            <span class="text-muted">N/A</span>
+                        {% endif %}
+                    </td>
+                </tr>
+            </table>
+        </div>
         <div class="panel panel-default">
         <div class="panel panel-default">
             <div class="panel-heading">
             <div class="panel-heading">
                 <strong>Comments</strong>
                 <strong>Comments</strong>
             </div>
             </div>
             <div class="panel-body">
             <div class="panel-body">
-                {% if circuit.comments  %}
+                {% if circuit.comments %}
                     {{ circuit.comments|gfm }}
                     {{ circuit.comments|gfm }}
                 {% else %}
                 {% else %}
                     <span class="text-muted">None</span>
                     <span class="text-muted">None</span>

+ 8 - 2
netbox/templates/circuits/circuit_edit.html

@@ -9,13 +9,19 @@
             {% render_field form.provider %}
             {% render_field form.provider %}
             {% render_field form.cid %}
             {% render_field form.cid %}
             {% render_field form.type %}
             {% render_field form.type %}
+            {% render_field form.tenant %}
             {% render_field form.install_date %}
             {% render_field form.install_date %}
-            {% render_field form.port_speed %}
-            {% render_field form.commit_rate %}
             {% render_field form.xconnect_id %}
             {% render_field form.xconnect_id %}
             {% render_field form.pp_info %}
             {% render_field form.pp_info %}
         </div>
         </div>
     </div>
     </div>
+    <div class="panel panel-default">
+        <div class="panel-heading"><strong>Bandwidth</strong></div>
+        <div class="panel-body">
+            {% render_field form.port_speed %}
+            {% render_field form.commit_rate %}
+        </div>
+    </div>
     <div class="panel panel-default">
     <div class="panel panel-default">
         <div class="panel-heading"><strong>Termination</strong></div>
         <div class="panel-heading"><strong>Termination</strong></div>
         <div class="panel-body">
         <div class="panel-body">

+ 6 - 1
netbox/templates/circuits/circuit_import.html

@@ -43,6 +43,11 @@
 					<td>Circuit type</td>
 					<td>Circuit type</td>
 					<td>Transit</td>
 					<td>Transit</td>
 				</tr>
 				</tr>
+				<tr>
+					<td>Tenant</td>
+					<td>Name of tenant (optional)</td>
+					<td>Strickland Propane</td>
+				</tr>
 				<tr>
 				<tr>
 					<td>Site</td>
 					<td>Site</td>
 					<td>Site name</td>
 					<td>Site name</td>
@@ -76,7 +81,7 @@
 			</tbody>
 			</tbody>
 		</table>
 		</table>
 		<h4>Example</h4>
 		<h4>Example</h4>
-		<pre>IC-603122,TeliaSonera,Transit,ASH-4,2016-02-23,10000,2000,937649,PP8371 ports 13/14</pre>
+		<pre>IC-603122,TeliaSonera,Transit,Strickland Propane,ASH-4,2016-02-23,10000,2000,937649,PP8371 ports 13/14</pre>
 	</div>
 	</div>
 </div>
 </div>
 {% endblock %}
 {% endblock %}

+ 2 - 18
netbox/templates/circuits/circuit_list.html

@@ -7,7 +7,7 @@
 <div class="pull-right">
 <div class="pull-right">
     {% if perms.circuits.add_circuit %}
     {% if perms.circuits.add_circuit %}
 		<a href="{% url 'circuits:circuit_add' %}" class="btn btn-primary">
 		<a href="{% url 'circuits:circuit_add' %}" class="btn btn-primary">
-			<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
+			<span class="fa fa-plus" aria-hidden="true"></span>
 			Add a circuit
 			Add a circuit
 		</a>
 		</a>
     {% endif %}
     {% endif %}
@@ -19,23 +19,7 @@
         {% include 'utilities/obj_table.html' with bulk_edit_url='circuits:circuit_bulk_edit' bulk_delete_url='circuits:circuit_bulk_delete' %}
         {% include 'utilities/obj_table.html' with bulk_edit_url='circuits:circuit_bulk_edit' bulk_delete_url='circuits:circuit_bulk_delete' %}
     </div>
     </div>
     <div class="col-md-3">
     <div class="col-md-3">
-		<div class="panel panel-default">
-			<div class="panel-heading">
-				<strong>Search</strong>
-			</div>
-			<div class="panel-body">
-				<form action="{% url 'circuits:circuit_list' %}" method="get">
-					<div class="input-group">
-						<input type="text" name="q" class="form-control" placeholder="Name" {% if request.GET.q %}value="{{ request.GET.q }}" {% endif %}/>
-						<span class="input-group-btn">
-							<button type="submit" class="btn btn-primary">
-								<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
-							</button>
-						</span>
-					</div>
-				</form>
-			</div>
-		</div>
+		{% include 'inc/search_panel.html' %}
 		{% include 'inc/filter_panel.html' %}
 		{% include 'inc/filter_panel.html' %}
     </div>
     </div>
 </div>
 </div>

+ 1 - 1
netbox/templates/circuits/circuittype_list.html

@@ -7,7 +7,7 @@
 <div class="pull-right">
 <div class="pull-right">
     {% if perms.circuits.add_circuittype %}
     {% if perms.circuits.add_circuittype %}
         <a href="{% url 'circuits:circuittype_add' %}" class="btn btn-primary">
         <a href="{% url 'circuits:circuittype_add' %}" class="btn btn-primary">
-            <span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
+            <span class="fa fa-plus" aria-hidden="true"></span>
             Add a circuit type
             Add a circuit type
         </a>
         </a>
     {% endif %}
     {% endif %}

+ 55 - 13
netbox/templates/circuits/provider.html

@@ -6,27 +6,41 @@
 
 
 {% block content %}
 {% block content %}
 <div class="row">
 <div class="row">
-    <div class="col-md-12">
+    <div class="col-md-9">
         <ol class="breadcrumb">
         <ol class="breadcrumb">
             <li><a href="{% url 'circuits:provider_list' %}">Providers</a></li>
             <li><a href="{% url 'circuits:provider_list' %}">Providers</a></li>
             <li>{{ provider }}</li>
             <li>{{ provider }}</li>
         </ol>
         </ol>
     </div>
     </div>
+    <div class="col-md-3">
+        <form action="{% url 'circuits:provider_list' %}" method="get">
+            <div class="input-group">
+                <input type="text" name="q" class="form-control" />
+                <span class="input-group-btn">
+                    <button type="submit" class="btn btn-primary">
+                        <span class="fa fa-search" aria-hidden="true"></span>
+                    </button>
+                </span>
+            </div>
+        </form>
+    </div>
 </div>
 </div>
 <div class="pull-right">
 <div class="pull-right">
-    <button type="button" class="btn btn-primary" data-toggle="modal" data-target="#graphs_modal" data-obj="{{ provider.name }}" data-url="{% url 'circuits-api:provider_graphs' pk=provider.pk %}" title="Show graphs">
-        <i class="glyphicon glyphicon-signal" aria-hidden="true"></i>
-        Graphs
-    </button>
+    {% if show_graphs %}
+        <button type="button" class="btn btn-primary" data-toggle="modal" data-target="#graphs_modal" data-obj="{{ provider.name }}" data-url="{% url 'circuits-api:provider_graphs' pk=provider.pk %}" title="Show graphs">
+            <i class="fa fa-signal" aria-hidden="true"></i>
+            Graphs
+        </button>
+    {% endif %}
     {% if perms.circuits.change_provider %}
     {% if perms.circuits.change_provider %}
 		<a href="{% url 'circuits:provider_edit' slug=provider.slug %}" class="btn btn-warning">
 		<a href="{% url 'circuits:provider_edit' slug=provider.slug %}" class="btn btn-warning">
-			<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span>
+			<span class="fa fa-pencil" aria-hidden="true"></span>
 			Edit this provider
 			Edit this provider
 		</a>
 		</a>
     {% endif %}
     {% endif %}
     {% if perms.circuits.delete_provider %}
     {% if perms.circuits.delete_provider %}
 		<a href="{% url 'circuits:provider_delete' slug=provider.slug %}" class="btn btn-danger">
 		<a href="{% url 'circuits:provider_delete' slug=provider.slug %}" class="btn btn-danger">
-			<span class="glyphicon glyphicon-trash" aria-hidden="true"></span>
+			<span class="fa fa-trash" aria-hidden="true"></span>
 			Delete this provider
 			Delete this provider
 		</a>
 		</a>
     {% endif %}
     {% endif %}
@@ -41,25 +55,53 @@
             <table class="table table-hover panel-body">
             <table class="table table-hover panel-body">
                 <tr>
                 <tr>
                     <td>ASN</td>
                     <td>ASN</td>
-                    <td>{{ provider.asn }}</td>
+                    <td>
+                        {% if provider.asn %}
+                            {{ provider.asn }}
+                        {% else %}
+                            <span class="text-muted">N/A</span>
+                        {% endif %}
+                    </td>
                 </tr>
                 </tr>
                 <tr>
                 <tr>
                     <td>Account</td>
                     <td>Account</td>
-                    <td>{{ provider.account }}</td>
+                    <td>
+                        {% if provider.account %}
+                            {{ provider.account }}
+                        {% else %}
+                            <span class="text-muted">N/A</span>
+                        {% endif %}
+                    </td>
                 </tr>
                 </tr>
                 <tr>
                 <tr>
                     <td>Customer Portal</td>
                     <td>Customer Portal</td>
                     <td>
                     <td>
-                        <a href="{{ provider.portal_url }}">{{ provider.portal_url }}</a>
+                        {% if provider.portal_url %}
+                            <a href="{{ provider.portal_url }}">{{ provider.portal_url }}</a>
+                        {% else %}
+                            <span class="text-muted">N/A</span>
+                        {% endif %}
                     </td>
                     </td>
                 </tr>
                 </tr>
                 <tr>
                 <tr>
                     <td>NOC Contact</td>
                     <td>NOC Contact</td>
-                    <td>{{ provider.noc_contact|linebreaksbr }}</td>
+                    <td>
+                        {% if provider.noc_contact %}
+                            {{ provider.noc_contact|linebreaksbr }}
+                        {% else %}
+                            <span class="text-muted">N/A</span>
+                        {% endif %}
+                    </td>
                 </tr>
                 </tr>
                 <tr>
                 <tr>
                     <td>Admin Contact</td>
                     <td>Admin Contact</td>
-                    <td>{{ provider.admin_contact|linebreaksbr }}</td>
+                    <td>
+                        {% if provider.admin_contact %}
+                            {{ provider.admin_contact|linebreaksbr }}
+                        {% else %}
+                            <span class="text-muted">N/A</span>
+                        {% endif %}
+                    </td>
                 </tr>
                 </tr>
                 <tr>
                 <tr>
                     <td>Created</td>
                     <td>Created</td>
@@ -76,7 +118,7 @@
                 <strong>Comments</strong>
                 <strong>Comments</strong>
             </div>
             </div>
             <div class="panel-body">
             <div class="panel-body">
-                {% if provider.comments  %}
+                {% if provider.comments %}
                     {{ provider.comments|gfm }}
                     {{ provider.comments|gfm }}
                 {% else %}
                 {% else %}
                     <span class="text-muted">None</span>
                     <span class="text-muted">None</span>

+ 2 - 18
netbox/templates/circuits/provider_list.html

@@ -6,7 +6,7 @@
 <div class="pull-right">
 <div class="pull-right">
     {% if perms.circuits.add_provider %}
     {% if perms.circuits.add_provider %}
 		<a href="{% url 'circuits:provider_add' %}" class="btn btn-primary">
 		<a href="{% url 'circuits:provider_add' %}" class="btn btn-primary">
-			<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
+			<span class="fa fa-plus" aria-hidden="true"></span>
 			Add a provider
 			Add a provider
 		</a>
 		</a>
     {% endif %}
     {% endif %}
@@ -18,23 +18,7 @@
         {% include 'utilities/obj_table.html' with bulk_edit_url='circuits:provider_bulk_edit' bulk_delete_url='circuits:provider_bulk_delete' %}
         {% include 'utilities/obj_table.html' with bulk_edit_url='circuits:provider_bulk_edit' bulk_delete_url='circuits:provider_bulk_delete' %}
     </div>
     </div>
     <div class="col-md-3">
     <div class="col-md-3">
-		<div class="panel panel-default">
-			<div class="panel-heading">
-				<strong>Search</strong>
-			</div>
-			<div class="panel-body">
-				<form action="{% url 'circuits:provider_list' %}" method="get">
-					<div class="input-group">
-						<input type="text" name="q" class="form-control" placeholder="Name" {% if request.GET.q %}value="{{ request.GET.q }}" {% endif %}/>
-						<span class="input-group-btn">
-							<button type="submit" class="btn btn-primary">
-								<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
-							</button>
-						</span>
-					</div>
-				</form>
-			</div>
-		</div>
+		{% include 'inc/search_panel.html' %}
 		{% include 'inc/filter_panel.html' %}
 		{% include 'inc/filter_panel.html' %}
     </div>
     </div>
 </div>
 </div>

+ 1 - 1
netbox/templates/dcim/console_connections_list.html

@@ -7,7 +7,7 @@
 <div class="pull-right">
 <div class="pull-right">
     {% if perms.dcim.change_consoleport %}
     {% if perms.dcim.change_consoleport %}
         <a href="{% url 'dcim:console_connections_import' %}" class="btn btn-info">
         <a href="{% url 'dcim:console_connections_import' %}" class="btn btn-info">
-            <span class="glyphicon glyphicon-import" aria-hidden="true"></span>
+            <span class="fa fa-download" aria-hidden="true"></span>
             Import connections
             Import connections
         </a>
         </a>
     {% endif %}
     {% endif %}

+ 15 - 5
netbox/templates/dcim/device.html

@@ -14,6 +14,16 @@
                 <strong>Device</strong>
                 <strong>Device</strong>
             </div>
             </div>
             <table class="table table-hover panel-body">
             <table class="table table-hover panel-body">
+                <tr>
+                    <td>Tenant</td>
+                    <td>
+                        {% if device.tenant %}
+                            <a href="{{ device.tenant.get_absolute_url }}">{{ device.tenant }}</a>
+                        {% else %}
+                            <span class="text-muted">None</span>
+                        {% endif %}
+                    </td>
+                </tr>
                 <tr>
                 <tr>
                     <td>Site</td>
                     <td>Site</td>
                     <td>
                     <td>
@@ -55,7 +65,7 @@
                         {% if device.serial %}
                         {% if device.serial %}
                             <span>{{ device.serial }}</span>
                             <span>{{ device.serial }}</span>
                         {% else %}
                         {% else %}
-                            <span class="text-muted">Not defined</span>
+                            <span class="text-muted">N/A</span>
                         {% endif %}
                         {% endif %}
                     </td>
                     </td>
                 </tr>
                 </tr>
@@ -86,7 +96,7 @@
                         {% if device.platform %}
                         {% if device.platform %}
                             <span>{{ device.platform }}</span>
                             <span>{{ device.platform }}</span>
                         {% else %}
                         {% else %}
-                            <span class="text-warning">Not assigned</span>
+                            <span class="text-muted">None</span>
                         {% endif %}
                         {% endif %}
                     </td>
                     </td>
                 </tr>
                 </tr>
@@ -111,7 +121,7 @@
                                 <span>(NAT: {{ device.primary_ip4.nat_outside.address.ip }})</span>
                                 <span>(NAT: {{ device.primary_ip4.nat_outside.address.ip }})</span>
                             {% endif %}
                             {% endif %}
                         {% else %}
                         {% else %}
-                            <span class="text-muted">Not defined</span>
+                            <span class="text-muted">N/A</span>
                         {% endif %}
                         {% endif %}
                     </td>
                     </td>
                 </tr>
                 </tr>
@@ -126,7 +136,7 @@
                                 <span>(NAT: {{ device.primary_ip6.nat_outside.address.ip }})</span>
                                 <span>(NAT: {{ device.primary_ip6.nat_outside.address.ip }})</span>
                             {% endif %}
                             {% endif %}
                         {% else %}
                         {% else %}
-                            <span class="text-muted">Not defined</span>
+                            <span class="text-muted">N/A</span>
                         {% endif %}
                         {% endif %}
                     </td>
                     </td>
                 </tr>
                 </tr>
@@ -257,7 +267,7 @@
                 <strong>Comments</strong>
                 <strong>Comments</strong>
             </div>
             </div>
             <div class="panel-body">
             <div class="panel-body">
-                {% if device.comments  %}
+                {% if device.comments %}
                     {{ device.comments|gfm }}
                     {{ device.comments|gfm }}
                 {% else %}
                 {% else %}
                     <span class="text-muted">None</span>
                     <span class="text-muted">None</span>

+ 1 - 0
netbox/templates/dcim/device_bulk_edit.html

@@ -9,6 +9,7 @@
             <td><a href="{% url 'dcim:device' pk=device.pk %}">{{ device }}</a></td>
             <td><a href="{% url 'dcim:device' pk=device.pk %}">{{ device }}</a></td>
             <td>{{ device.device_type }}</td>
             <td>{{ device.device_type }}</td>
             <td>{{ device.device_role }}</td>
             <td>{{ device.device_role }}</td>
+            <td>{{ device.tenant }}</td>
             <td>{{ device.serial }}</td>
             <td>{{ device.serial }}</td>
         </tr>
         </tr>
     {% endfor %}
     {% endfor %}

+ 1 - 0
netbox/templates/dcim/device_edit.html

@@ -7,6 +7,7 @@
         <div class="panel-body">
         <div class="panel-body">
             {% render_field form.name %}
             {% render_field form.name %}
             {% render_field form.device_role %}
             {% render_field form.device_role %}
+            {% render_field form.tenant %}
         </div>
         </div>
     </div>
     </div>
     <div class="panel panel-default">
     <div class="panel panel-default">

+ 6 - 1
netbox/templates/dcim/device_import.html

@@ -36,6 +36,11 @@
 					<td>Functional role of device</td>
 					<td>Functional role of device</td>
 					<td>ToR Switch</td>
 					<td>ToR Switch</td>
 				</tr>
 				</tr>
+				<tr>
+					<td>Tenant</td>
+					<td>Name of tenant (optional)</td>
+					<td>Pied Piper</td>
+				</tr>
 				<tr>
 				<tr>
 					<td>Device manufacturer</td>
 					<td>Device manufacturer</td>
 					<td>Hardware manufacturer</td>
 					<td>Hardware manufacturer</td>
@@ -79,7 +84,7 @@
 			</tbody>
 			</tbody>
 		</table>
 		</table>
 		<h4>Example</h4>
 		<h4>Example</h4>
-		<pre>rack101_sw1,ToR Switch,Juniper,EX4300-48T,Juniper Junos,CAB00577291,Ashburn-VA,R101,21,Rear</pre>
+		<pre>rack101_sw1,ToR Switch,Pied Piper,Juniper,EX4300-48T,Juniper Junos,CAB00577291,Ashburn-VA,R101,21,Rear</pre>
 	</div>
 	</div>
 </div>
 </div>
 {% endblock %}
 {% endblock %}

+ 1 - 1
netbox/templates/dcim/device_inventory.html

@@ -107,7 +107,7 @@
         </div>
         </div>
         {% if perms.dcim.add_module %}
         {% if perms.dcim.add_module %}
             <a href="{% url 'dcim:module_add' pk=device.pk %}" class="btn btn-success">
             <a href="{% url 'dcim:module_add' pk=device.pk %}" class="btn btn-success">
-                <span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
+                <span class="fa fa-plus" aria-hidden="true"></span>
                 Add a Module
                 Add a Module
             </a>
             </a>
         {% endif %}
         {% endif %}

+ 3 - 20
netbox/templates/dcim/device_list.html

@@ -7,11 +7,11 @@
 <div class="pull-right">
 <div class="pull-right">
     {% if perms.dcim.add_device %}
     {% if perms.dcim.add_device %}
         <a href="{% url 'dcim:device_add' %}" class="btn btn-primary">
         <a href="{% url 'dcim:device_add' %}" class="btn btn-primary">
-            <span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
+            <span class="fa fa-plus" aria-hidden="true"></span>
             Add a device
             Add a device
         </a>
         </a>
         <a href="{% url 'dcim:device_import' %}" class="btn btn-info">
         <a href="{% url 'dcim:device_import' %}" class="btn btn-info">
-            <span class="glyphicon glyphicon-import" aria-hidden="true"></span>
+            <span class="fa fa-download" aria-hidden="true"></span>
             Import devices
             Import devices
         </a>
         </a>
     {% endif %}
     {% endif %}
@@ -23,24 +23,7 @@
         {% include 'dcim/inc/device_table.html' with bulk_edit_url='dcim:device_bulk_edit' bulk_delete_url='dcim:device_bulk_delete' %}
         {% include 'dcim/inc/device_table.html' with bulk_edit_url='dcim:device_bulk_edit' bulk_delete_url='dcim:device_bulk_delete' %}
     </div>
     </div>
     <div class="col-md-3">
     <div class="col-md-3">
-		<div class="panel panel-default">
-			<div class="panel-heading">
-				<span class="glyphicon glyphicon-search" aria-hidden="true"></span> 
-				<strong>Search</strong>
-			</div>
-			<div class="panel-body">
-				<form action="{% url 'dcim:device_list' %}" method="get">
-					<div class="input-group">
-						<input type="text" name="q" class="form-control" placeholder="Name or serial" {% if request.GET.q %}value="{{ request.GET.q }}" {% endif %}/>
-						<span class="input-group-btn">
-							<button type="submit" class="btn btn-primary">
-								<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
-							</button>
-						</span>
-					</div>
-				</form>
-			</div>
-		</div>
+		{% include 'inc/search_panel.html' %}
 		{% include 'inc/filter_panel.html' %}
 		{% include 'inc/filter_panel.html' %}
     </div>
     </div>
 </div>
 </div>

+ 1 - 1
netbox/templates/dcim/devicerole_list.html

@@ -7,7 +7,7 @@
 <div class="pull-right">
 <div class="pull-right">
     {% if perms.dcim.add_devicerole %}
     {% if perms.dcim.add_devicerole %}
         <a href="{% url 'dcim:devicerole_add' %}" class="btn btn-primary">
         <a href="{% url 'dcim:devicerole_add' %}" class="btn btn-primary">
-            <span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
+            <span class="fa fa-plus" aria-hidden="true"></span>
             Add a device role
             Add a device role
         </a>
         </a>
     {% endif %}
     {% endif %}

+ 2 - 2
netbox/templates/dcim/devicetype.html

@@ -19,13 +19,13 @@
     <div class="pull-right">
     <div class="pull-right">
       {% if perms.dcim.change_devicetype %}
       {% if perms.dcim.change_devicetype %}
             <a href="{% url 'dcim:devicetype_edit' pk=devicetype.pk %}" class="btn btn-warning">
             <a href="{% url 'dcim:devicetype_edit' pk=devicetype.pk %}" class="btn btn-warning">
-              <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span>
+              <span class="fa fa-pencil" aria-hidden="true"></span>
               Edit this device type
               Edit this device type
             </a>
             </a>
       {% endif %}
       {% endif %}
       {% if perms.dcim.delete_devicetype %}
       {% if perms.dcim.delete_devicetype %}
           <a href="{% url 'dcim:devicetype_delete' pk=devicetype.pk %}" class="btn btn-danger">
           <a href="{% url 'dcim:devicetype_delete' pk=devicetype.pk %}" class="btn btn-danger">
-          	<span class="glyphicon glyphicon-trash" aria-hidden="true"></span>
+          	<span class="fa fa-trash" aria-hidden="true"></span>
           	Delete this device type
           	Delete this device type
           </a>
           </a>
       {% endif %}
       {% endif %}

+ 1 - 1
netbox/templates/dcim/devicetype_list.html

@@ -7,7 +7,7 @@
 <div class="pull-right">
 <div class="pull-right">
     {% if perms.dcim.add_devicetype %}
     {% if perms.dcim.add_devicetype %}
         <a href="{% url 'dcim:devicetype_add' %}" class="btn btn-primary">
         <a href="{% url 'dcim:devicetype_add' %}" class="btn btn-primary">
-            <span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
+            <span class="fa fa-plus" aria-hidden="true"></span>
             Add a device type
             Add a device type
         </a>
         </a>
     {% endif %}
     {% endif %}

+ 2 - 2
netbox/templates/dcim/inc/_device_header.html

@@ -16,10 +16,10 @@
     <div class="col-md-3">
     <div class="col-md-3">
         <form action="{% url 'dcim:device_list' %}" method="get">
         <form action="{% url 'dcim:device_list' %}" method="get">
             <div class="input-group">
             <div class="input-group">
-                <input type="text" name="q" class="form-control" placeholder="Device name or serial" />
+                <input type="text" name="q" class="form-control" placeholder="Search devices" />
                 <span class="input-group-btn">
                 <span class="input-group-btn">
                     <button type="submit" class="btn btn-primary">
                     <button type="submit" class="btn btn-primary">
-                        <span class="glyphicon glyphicon-search" aria-hidden="true"></span>
+                        <span class="fa fa-search" aria-hidden="true"></span>
                     </button>
                     </button>
                 </span>
                 </span>
             </div>
             </div>

+ 6 - 4
netbox/templates/dcim/inc/_interface.html

@@ -34,10 +34,12 @@
         </td>
         </td>
     {% endif %}
     {% endif %}
     <td class="text-right">
     <td class="text-right">
-        {% if iface.circuit or iface.connection %}
-            <button type="button" class="btn btn-primary btn-xs" data-toggle="modal" data-target="#graphs_modal" data-obj="{{ device.name }} - {{ iface.name }}" data-url="{% url 'dcim-api:interface_graphs' pk=iface.pk %}" title="Show graphs">
-                <i class="glyphicon glyphicon-signal" aria-hidden="true"></i>
-            </button>
+        {% if show_graphs %}
+            {% if iface.circuit or iface.connection %}
+                <button type="button" class="btn btn-primary btn-xs" data-toggle="modal" data-target="#graphs_modal" data-obj="{{ device.name }} - {{ iface.name }}" data-url="{% url 'dcim-api:interface_graphs' pk=iface.pk %}" title="Show graphs">
+                    <i class="glyphicon glyphicon-signal" aria-hidden="true"></i>
+                </button>
+            {% endif %}
         {% endif %}
         {% endif %}
         {% if perms.dcim.change_interface %}
         {% if perms.dcim.change_interface %}
             {% if iface.is_physical %}
             {% if iface.is_physical %}

+ 1 - 1
netbox/templates/dcim/interface_connections_list.html

@@ -7,7 +7,7 @@
 <div class="pull-right">
 <div class="pull-right">
     {% if perms.dcim.add_interfaceconnection %}
     {% if perms.dcim.add_interfaceconnection %}
         <a href="{% url 'dcim:interface_connections_import' %}" class="btn btn-info">
         <a href="{% url 'dcim:interface_connections_import' %}" class="btn btn-info">
-            <span class="glyphicon glyphicon-import" aria-hidden="true"></span>
+            <span class="fa fa-download" aria-hidden="true"></span>
             Import connections
             Import connections
         </a>
         </a>
     {% endif %}
     {% endif %}

+ 1 - 1
netbox/templates/dcim/manufacturer_list.html

@@ -7,7 +7,7 @@
 <div class="pull-right">
 <div class="pull-right">
     {% if perms.dcim.add_manufacturer %}
     {% if perms.dcim.add_manufacturer %}
         <a href="{% url 'dcim:manufacturer_add' %}" class="btn btn-primary">
         <a href="{% url 'dcim:manufacturer_add' %}" class="btn btn-primary">
-            <span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
+            <span class="fa fa-plus" aria-hidden="true"></span>
             Add a manufacturer
             Add a manufacturer
         </a>
         </a>
     {% endif %}
     {% endif %}

+ 1 - 1
netbox/templates/dcim/platform_list.html

@@ -7,7 +7,7 @@
 <div class="pull-right">
 <div class="pull-right">
     {% if perms.dcim.add_platform %}
     {% if perms.dcim.add_platform %}
         <a href="{% url 'dcim:platform_add' %}" class="btn btn-primary">
         <a href="{% url 'dcim:platform_add' %}" class="btn btn-primary">
-            <span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
+            <span class="fa fa-plus" aria-hidden="true"></span>
             Add a platform
             Add a platform
         </a>
         </a>
     {% endif %}
     {% endif %}

+ 1 - 1
netbox/templates/dcim/power_connections_list.html

@@ -7,7 +7,7 @@
 <div class="pull-right">
 <div class="pull-right">
     {% if perms.dcim.change_powerport %}
     {% if perms.dcim.change_powerport %}
         <a href="{% url 'dcim:power_connections_import' %}" class="btn btn-info">
         <a href="{% url 'dcim:power_connections_import' %}" class="btn btn-info">
-            <span class="glyphicon glyphicon-import" aria-hidden="true"></span>
+            <span class="fa fa-download" aria-hidden="true"></span>
             Import connections
             Import connections
         </a>
         </a>
     {% endif %}
     {% endif %}

+ 19 - 9
netbox/templates/dcim/rack.html

@@ -8,18 +8,18 @@
 <div class="row">
 <div class="row">
     <div class="col-md-9">
     <div class="col-md-9">
         <ol class="breadcrumb">
         <ol class="breadcrumb">
-            <li><a href="{% url 'dcim:site' slug=rack.site.slug %}">{{ rack.site }}</a></li>
-            <li><a href="{% url 'dcim:rack_list' %}?site={{ rack.site.slug }}">Racks</a></li>
+            <li><a href="{% url 'dcim:rack_list' %}">Racks</a></li>
+            <li><a href="{% url 'dcim:rack_list' %}?site={{ rack.site.slug }}">{{ rack.site }}</a></li>
             <li>{{ rack }}</li>
             <li>{{ rack }}</li>
         </ol>
         </ol>
     </div>
     </div>
     <div class="col-md-3">
     <div class="col-md-3">
         <form action="{% url 'dcim:rack_list' %}" method="get">
         <form action="{% url 'dcim:rack_list' %}" method="get">
             <div class="input-group">
             <div class="input-group">
-                <input type="text" name="q" class="form-control" placeholder="Rack name or ID" />
+                <input type="text" name="q" class="form-control" placeholder="Search racks" />
                 <span class="input-group-btn">
                 <span class="input-group-btn">
                     <button type="submit" class="btn btn-primary">
                     <button type="submit" class="btn btn-primary">
-                        <span class="glyphicon glyphicon-search" aria-hidden="true"></span>
+                        <span class="fa fa-search" aria-hidden="true"></span>
                     </button>
                     </button>
                 </span>
                 </span>
             </div>
             </div>
@@ -29,25 +29,25 @@
 <div class="pull-right">
 <div class="pull-right">
     {% if prev_rack %}
     {% if prev_rack %}
         <a href="{% url 'dcim:rack' pk=prev_rack.pk %}" class="btn btn-primary">
         <a href="{% url 'dcim:rack' pk=prev_rack.pk %}" class="btn btn-primary">
-            <span class="glyphicon glyphicon-chevron-left" aria-hidden="true"></span>
+            <span class="fa fa-chevron-left" aria-hidden="true"></span>
             Previous Rack
             Previous Rack
         </a>
         </a>
     {% endif %}
     {% endif %}
     {% if next_rack %}
     {% if next_rack %}
         <a href="{% url 'dcim:rack' pk=next_rack.pk %}" class="btn btn-primary">
         <a href="{% url 'dcim:rack' pk=next_rack.pk %}" class="btn btn-primary">
-            <span class="glyphicon glyphicon-chevron-right" aria-hidden="true"></span>
+            <span class="fa fa-chevron-right" aria-hidden="true"></span>
             Next Rack
             Next Rack
         </a>
         </a>
     {% endif %}
     {% endif %}
     {% if perms.dcim.change_rack %}
     {% if perms.dcim.change_rack %}
 		<a href="{% url 'dcim:rack_edit' pk=rack.pk %}" class="btn btn-warning">
 		<a href="{% url 'dcim:rack_edit' pk=rack.pk %}" class="btn btn-warning">
-			<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span>
+			<span class="fa fa-pencil" aria-hidden="true"></span>
 			Edit this rack
 			Edit this rack
 		</a>
 		</a>
     {% endif %}
     {% endif %}
     {% if perms.dcim.delete_rack %}
     {% if perms.dcim.delete_rack %}
 		<a href="{% url 'dcim:rack_delete' pk=rack.pk %}" class="btn btn-danger">
 		<a href="{% url 'dcim:rack_delete' pk=rack.pk %}" class="btn btn-danger">
-			<span class="glyphicon glyphicon-trash" aria-hidden="true"></span>
+			<span class="fa fa-trash" aria-hidden="true"></span>
 			Delete this rack
 			Delete this rack
 		</a>
 		</a>
     {% endif %}
     {% endif %}
@@ -81,6 +81,16 @@
                     <td>
                     <td>
                         {% if rack.facility_id %}
                         {% if rack.facility_id %}
                             <span>{{ rack.facility_id }}</span>
                             <span>{{ rack.facility_id }}</span>
+                        {% else %}
+                            <span class="text-muted">N/A</span>
+                        {% endif %}
+                    </td>
+                </tr>
+                <tr>
+                    <td>Tenant</td>
+                    <td>
+                        {% if rack.tenant %}
+                            <a href="{{ rack.tenant.get_absolute_url }}">{{ rack.tenant }}</a>
                         {% else %}
                         {% else %}
                             <span class="text-muted">None</span>
                             <span class="text-muted">None</span>
                         {% endif %}
                         {% endif %}
@@ -146,7 +156,7 @@
                 <strong>Comments</strong>
                 <strong>Comments</strong>
             </div>
             </div>
             <div class="panel-body">
             <div class="panel-body">
-                {% if rack.comments  %}
+                {% if rack.comments %}
                     {{ rack.comments|gfm }}
                     {{ rack.comments|gfm }}
                 {% else %}
                 {% else %}
                     <span class="text-muted">None</span>
                     <span class="text-muted">None</span>

+ 1 - 0
netbox/templates/dcim/rack_bulk_edit.html

@@ -9,6 +9,7 @@
             <td><a href="{% url 'dcim:rack' pk=rack.pk %}">{{ rack }}</a></td>
             <td><a href="{% url 'dcim:rack' pk=rack.pk %}">{{ rack }}</a></td>
             <td>{{ rack.facility_id }}</td>
             <td>{{ rack.facility_id }}</td>
             <td>{{ rack.site }}</td>
             <td>{{ rack.site }}</td>
+            <td>{{ rack.tenant }}</td>
             <td>{{ rack.u_height }}</td>
             <td>{{ rack.u_height }}</td>
         </tr>
         </tr>
     {% endfor %}
     {% endfor %}

+ 1 - 0
netbox/templates/dcim/rack_edit.html

@@ -9,6 +9,7 @@
             {% render_field form.group %}
             {% render_field form.group %}
             {% render_field form.name %}
             {% render_field form.name %}
             {% render_field form.facility_id %}
             {% render_field form.facility_id %}
+            {% render_field form.tenant %}
             {% render_field form.u_height %}
             {% render_field form.u_height %}
         </div>
         </div>
     </div>
     </div>

+ 6 - 1
netbox/templates/dcim/rack_import.html

@@ -48,6 +48,11 @@
 					<td>Rack ID assigned by the facility (optional)</td>
 					<td>Rack ID assigned by the facility (optional)</td>
 					<td>J12.100</td>
 					<td>J12.100</td>
 				</tr>
 				</tr>
+				<tr>
+					<td>Tenant</td>
+					<td>Name of tenant (optional)</td>
+					<td>Pied Piper</td>
+				</tr>
 				<tr>
 				<tr>
 					<td>Height</td>
 					<td>Height</td>
 					<td>Height in rack units</td>
 					<td>Height in rack units</td>
@@ -56,7 +61,7 @@
 			</tbody>
 			</tbody>
 		</table>
 		</table>
 		<h4>Example</h4>
 		<h4>Example</h4>
-		<pre>DC-4,Cage 1400,R101,J12.100,42</pre>
+		<pre>DC-4,Cage 1400,R101,J12.100,Pied Piper,42</pre>
 	</div>
 	</div>
 </div>
 </div>
 {% endblock %}
 {% endblock %}

+ 3 - 20
netbox/templates/dcim/rack_list.html

@@ -7,11 +7,11 @@
 <div class="pull-right">
 <div class="pull-right">
     {% if perms.dcim.add_rack %}
     {% if perms.dcim.add_rack %}
         <a href="{% url 'dcim:rack_add' %}" class="btn btn-primary">
         <a href="{% url 'dcim:rack_add' %}" class="btn btn-primary">
-            <span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
+            <span class="fa fa-plus" aria-hidden="true"></span>
             Add a rack
             Add a rack
         </a>
         </a>
         <a href="{% url 'dcim:rack_import' %}" class="btn btn-info">
         <a href="{% url 'dcim:rack_import' %}" class="btn btn-info">
-            <span class="glyphicon glyphicon-import" aria-hidden="true"></span>
+            <span class="fa fa-download" aria-hidden="true"></span>
             Import racks
             Import racks
         </a>
         </a>
     {% endif %}
     {% endif %}
@@ -23,24 +23,7 @@
         {% include 'utilities/obj_table.html' with bulk_edit_url='dcim:rack_bulk_edit' bulk_delete_url='dcim:rack_bulk_delete' %}
         {% include 'utilities/obj_table.html' with bulk_edit_url='dcim:rack_bulk_edit' bulk_delete_url='dcim:rack_bulk_delete' %}
     </div>
     </div>
     <div class="col-md-3">
     <div class="col-md-3">
-		<div class="panel panel-default">
-			<div class="panel-heading">
-				<span class="glyphicon glyphicon-search" aria-hidden="true"></span> 
-				<strong>Search</strong>
-			</div>
-			<div class="panel-body">
-				<form action="{% url 'dcim:rack_list' %}" method="get">
-					<div class="input-group">
-						<input type="text" name="q" class="form-control" placeholder="Name" {% if request.GET.q %}value="{{ request.GET.q }}" {% endif %}/>
-						<span class="input-group-btn">
-							<button type="submit" class="btn btn-primary">
-								<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
-							</button>
-						</span>
-					</div>
-				</form>
-			</div>
-		</div>
+		{% include 'inc/search_panel.html' %}
 		{% include 'inc/filter_panel.html' %}
 		{% include 'inc/filter_panel.html' %}
     </div>
     </div>
 </div>
 </div>

+ 1 - 1
netbox/templates/dcim/rackgroup_list.html

@@ -7,7 +7,7 @@
 <div class="pull-right">
 <div class="pull-right">
     {% if perms.dcim.add_rackgroup %}
     {% if perms.dcim.add_rackgroup %}
         <a href="{% url 'dcim:rackgroup_add' %}" class="btn btn-primary">
         <a href="{% url 'dcim:rackgroup_add' %}" class="btn btn-primary">
-            <span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
+            <span class="fa fa-plus" aria-hidden="true"></span>
             Add a rack group
             Add a rack group
         </a>
         </a>
     {% endif %}
     {% endif %}

+ 63 - 47
netbox/templates/dcim/site.html

@@ -16,10 +16,10 @@
     <div class="col-md-3">
     <div class="col-md-3">
         <form action="{% url 'dcim:site_list' %}" method="get">
         <form action="{% url 'dcim:site_list' %}" method="get">
             <div class="input-group">
             <div class="input-group">
-                <input type="text" name="q" class="form-control" placeholder="Search" />
+                <input type="text" name="q" class="form-control" placeholder="Search sites" />
                 <span class="input-group-btn">
                 <span class="input-group-btn">
                     <button type="submit" class="btn btn-primary">
                     <button type="submit" class="btn btn-primary">
-                        <span class="glyphicon glyphicon-search" aria-hidden="true"></span>
+                        <span class="fa fa-search" aria-hidden="true"></span>
                     </button>
                     </button>
                 </span>
                 </span>
             </div>
             </div>
@@ -27,38 +27,62 @@
     </div>
     </div>
 </div>
 </div>
 <div class="pull-right">
 <div class="pull-right">
-    <button type="button" class="btn btn-primary" data-toggle="modal" data-target="#graphs_modal" data-obj="{{ site.name }}" data-url="{% url 'dcim-api:site_graphs' pk=site.pk %}" title="Show graphs">
-        <i class="glyphicon glyphicon-signal" aria-hidden="true"></i>
-        Graphs
-    </button>
+    {% if show_graphs %}
+        <button type="button" class="btn btn-primary" data-toggle="modal" data-target="#graphs_modal" data-obj="{{ site.name }}" data-url="{% url 'dcim-api:site_graphs' pk=site.pk %}" title="Show graphs">
+            <i class="fa fa-signal" aria-hidden="true"></i>
+            Graphs
+        </button>
+    {% endif %}
     {% if perms.dcim.change_site %}
     {% if perms.dcim.change_site %}
 		<a href="{% url 'dcim:site_edit' slug=site.slug %}" class="btn btn-warning">
 		<a href="{% url 'dcim:site_edit' slug=site.slug %}" class="btn btn-warning">
-			<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span>
+			<span class="fa fa-pencil" aria-hidden="true"></span>
 			Edit this site
 			Edit this site
 		</a>
 		</a>
     {% endif %}
     {% endif %}
     {% if perms.dcim.delete_site %}
     {% if perms.dcim.delete_site %}
 		<a href="{% url 'dcim:site_delete' slug=site.slug %}" class="btn btn-danger">
 		<a href="{% url 'dcim:site_delete' slug=site.slug %}" class="btn btn-danger">
-			<span class="glyphicon glyphicon-trash" aria-hidden="true"></span>
+			<span class="fa fa-trash" aria-hidden="true"></span>
 			Delete this site
 			Delete this site
 		</a>
 		</a>
     {% endif %}
     {% endif %}
 </div>
 </div>
 <h1>{{ site.name }}</h1>
 <h1>{{ site.name }}</h1>
 <div class="row">
 <div class="row">
-	<div class="col-md-6">
+	<div class="col-md-7">
         <div class="panel panel-default">
         <div class="panel panel-default">
             <div class="panel-heading">
             <div class="panel-heading">
                 <strong>Site</strong>
                 <strong>Site</strong>
             </div>
             </div>
             <table class="table table-hover panel-body">
             <table class="table table-hover panel-body">
+                <tr>
+                    <td>Tenant</td>
+                    <td>
+                        {% if site.tenant %}
+                            <a href="{{ site.tenant.get_absolute_url }}">{{ site.tenant }}</a>
+                        {% else %}
+                            <span class="text-muted">None</span>
+                        {% endif %}
+                    </td>
+                </tr>
                 <tr>
                 <tr>
                     <td>Facility</td>
                     <td>Facility</td>
-                    <td>{{ site.facility }}</td>
+                    <td>
+                        {% if site.facility %}
+                            {{ site.facility }}
+                        {% else %}
+                            <span class="text-muted">N/A</span>
+                        {% endif %}
+                    </td>
                 </tr>
                 </tr>
                 <tr>
                 <tr>
                     <td>AS Number</td>
                     <td>AS Number</td>
-                    <td>{{ site.asn }}</td>
+                    <td>
+                        {% if site.asn %}
+                            {{ site.asn }}
+                        {% else %}
+                            <span class="text-muted">N/A</span>
+                        {% endif %}
+                    </td>
                 </tr>
                 </tr>
                 <tr>
                 <tr>
                     <td>Physical Address</td>
                     <td>Physical Address</td>
@@ -69,8 +93,10 @@
                                     <i class="glyphicon glyphicon-map-marker"></i> Map it
                                     <i class="glyphicon glyphicon-map-marker"></i> Map it
                                 </a>
                                 </a>
                             </div>
                             </div>
+                            <span>{{ site.physical_address|linebreaksbr }}</span>
+                        {% else %}
+                            <span class="text-muted">N/A</span>
                         {% endif %}
                         {% endif %}
-                        <span>{{ site.physical_address|linebreaksbr }}</span>
                     </td>
                     </td>
                 </tr>
                 </tr>
                 <tr>
                 <tr>
@@ -79,7 +105,7 @@
                         {% if site.shipping_address %}
                         {% if site.shipping_address %}
                             <span>{{ site.shipping_address|linebreaksbr }}</span>
                             <span>{{ site.shipping_address|linebreaksbr }}</span>
                         {% else %}
                         {% else %}
-                            <span class="text-muted">See physical address</span>
+                            <span class="text-muted">N/A</span>
                         {% endif %}
                         {% endif %}
                     </td>
                     </td>
                 </tr>
                 </tr>
@@ -98,7 +124,7 @@
                 <strong>Comments</strong>
                 <strong>Comments</strong>
             </div>
             </div>
             <div class="panel-body">
             <div class="panel-body">
-                {% if site.comments  %}
+                {% if site.comments %}
                     {{ site.comments|gfm }}
                     {{ site.comments|gfm }}
                 {% else %}
                 {% else %}
                     <span class="text-muted">None</span>
                     <span class="text-muted">None</span>
@@ -106,43 +132,33 @@
             </div>
             </div>
         </div>
         </div>
     </div>
     </div>
-    <div class="col-md-6">
+    <div class="col-md-5">
         <div class="panel panel-default">
         <div class="panel panel-default">
             <div class="panel-heading">
             <div class="panel-heading">
                 <strong>Stats</strong>
                 <strong>Stats</strong>
             </div>
             </div>
-            <table class="table table-hover panel-body">
-                <tr>
-                    <td>Racks</td>
-                    <td>
-                        <a href="{% url 'dcim:rack_list' %}?site={{ site.slug }}">{{ stats.rack_count }}</a>
-                    </td>
-                </tr>
-                <tr>
-                    <td>Devices</td>
-                    <td>
-                        <a href="{% url 'dcim:device_list' %}?site={{ site.slug }}">{{ stats.device_count }}</a>
-                    </td>
-                </tr>
-                <tr>
-                    <td>Prefixes</td>
-                    <td>
-                        <a href="{% url 'ipam:prefix_list' %}?site={{ site.slug }}">{{ stats.prefix_count }}</a>
-                    </td>
-                </tr>
-                <tr>
-                    <td>VLANs</td>
-                    <td>
-                        <a href="{% url 'ipam:vlan_list' %}?site={{ site.slug }}">{{ stats.vlan_count }}</a>
-                    </td>
-                </tr>
-                <tr>
-                    <td>Circuits</td>
-                    <td>
-                        <a href="{% url 'circuits:circuit_list' %}?site={{ site.slug }}">{{ stats.circuit_count }}</a>
-                    </td>
-                </tr>
-            </table>
+            <div class="row panel-body">
+                <div class="col-md-4 text-center">
+                    <h2><a href="{% url 'dcim:rack_list' %}?site={{ site.slug }}" class="btn {% if stats.rack_count %}btn-primary{% else %}btn-default{% endif %} btn-lg">{{ stats.rack_count }}</a></h2>
+                    <p>Racks</p>
+                </div>
+                <div class="col-md-4 text-center">
+                    <h2><a href="{% url 'dcim:device_list' %}?site={{ site.slug }}" class="btn {% if stats.device_count %}btn-primary{% else %}btn-default{% endif %} btn-lg">{{ stats.device_count }}</a></h2>
+                    <p>Devices</p>
+                </div>
+                <div class="col-md-4 text-center">
+                    <h2><a href="{% url 'ipam:prefix_list' %}?site={{ site.slug }}" class="btn {% if stats.prefix_count %}btn-primary{% else %}btn-default{% endif %} btn-lg">{{ stats.prefix_count }}</a></h2>
+                    <p>Prefixes</p>
+                </div>
+                <div class="col-md-4 text-center">
+                    <h2><a href="{% url 'ipam:vlan_list' %}?site={{ site.slug }}" class="btn {% if stats.vlan_count %}btn-primary{% else %}btn-default{% endif %} btn-lg">{{ stats.vlan_count }}</a></h2>
+                    <p>VLANs</p>
+                </div>
+                <div class="col-md-4 text-center">
+                    <h2><a href="{% url 'circuits:circuit_list' %}?site={{ site.slug }}" class="btn {% if stats.circuit_count %}btn-primary{% else %}btn-default{% endif %} btn-lg">{{ stats.circuit_count }}</a></h2>
+                    <p>Circuits</p>
+                </div>
+            </div>
         </div>
         </div>
         <div class="panel panel-default">
         <div class="panel panel-default">
             <div class="panel-heading">
             <div class="panel-heading">

+ 13 - 0
netbox/templates/dcim/site_bulk_edit.html

@@ -0,0 +1,13 @@
+{% extends 'utilities/bulk_edit_form.html' %}
+{% load form_helpers %}
+
+{% block title %}Site Bulk Edit{% endblock %}
+
+{% block select_objects_table %}
+    {% for site in selected_objects %}
+        <tr>
+            <td><a href="{% url 'dcim:site' slug=site.slug %}">{{ site.slug }}</a></td>
+            <td>{{ site.tenant }}</td>
+        </tr>
+    {% endfor %}
+{% endblock %}

+ 1 - 0
netbox/templates/dcim/site_edit.html

@@ -7,6 +7,7 @@
         <div class="panel-body">
         <div class="panel-body">
             {% render_field form.name %}
             {% render_field form.name %}
             {% render_field form.slug %}
             {% render_field form.slug %}
+            {% render_field form.tenant %}
             {% render_field form.facility %}
             {% render_field form.facility %}
             {% render_field form.asn %}
             {% render_field form.asn %}
             {% render_field form.physical_address %}
             {% render_field form.physical_address %}

+ 6 - 1
netbox/templates/dcim/site_import.html

@@ -38,6 +38,11 @@
 					<td>URL-friendly name</td>
 					<td>URL-friendly name</td>
 					<td>ash4-south</td>
 					<td>ash4-south</td>
 				</tr>
 				</tr>
+				<tr>
+					<td>Tenant</td>
+					<td>Name of tenant (optional)</td>
+					<td>Pied Piper</td>
+				</tr>
 				<tr>
 				<tr>
 					<td>Facility</td>
 					<td>Facility</td>
 					<td>Name of the hosting facility (optional)</td>
 					<td>Name of the hosting facility (optional)</td>
@@ -51,7 +56,7 @@
 			</tbody>
 			</tbody>
 		</table>
 		</table>
 		<h4>Example</h4>
 		<h4>Example</h4>
-		<pre>ASH-4 South,ash4-south,Equinix DC6,65000</pre>
+		<pre>ASH-4 South,ash4-south,Pied Piper,Equinix DC6,65000</pre>
 	</div>
 	</div>
 </div>
 </div>
 {% endblock %}
 {% endblock %}

+ 8 - 21
netbox/templates/dcim/site_list.html

@@ -1,5 +1,4 @@
 {% extends '_base.html' %}
 {% extends '_base.html' %}
-{% load render_table from django_tables2 %}
 
 
 {% block title %}Sites{% endblock %}
 {% block title %}Sites{% endblock %}
 
 
@@ -7,36 +6,24 @@
 <div class="pull-right">
 <div class="pull-right">
     {% if perms.dcim.add_site %}
     {% if perms.dcim.add_site %}
 		<a href="{% url 'dcim:site_add' %}" class="btn btn-primary">
 		<a href="{% url 'dcim:site_add' %}" class="btn btn-primary">
-			<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
+			<span class="fa fa-plus" aria-hidden="true"></span>
 			Add a site
 			Add a site
 		</a>
 		</a>
+        <a href="{% url 'dcim:site_import' %}" class="btn btn-info">
+            <span class="fa fa-download" aria-hidden="true"></span>
+            Import sites
+        </a>
     {% endif %}
     {% endif %}
     {% include 'inc/export_button.html' with obj_type='sites' %}
     {% include 'inc/export_button.html' with obj_type='sites' %}
 </div>
 </div>
 <h1>Sites</h1>
 <h1>Sites</h1>
 <div class="row">
 <div class="row">
 	<div class="col-md-9">
 	<div class="col-md-9">
-        {% render_table table 'table.html' %}
+        {% include 'utilities/obj_table.html' with bulk_edit_url='dcim:site_bulk_edit' %}
     </div>
     </div>
     <div class="col-md-3">
     <div class="col-md-3">
-		<div class="panel panel-default">
-			<div class="panel-heading">
-				<span class="glyphicon glyphicon-search" aria-hidden="true"></span> 
-				<strong>Search</strong>
-			</div>
-			<div class="panel-body">
-				<form action="{% url 'dcim:site_list' %}" method="get">
-					<div class="input-group">
-						<input type="text" name="q" class="form-control" placeholder="Name" {% if request.GET.q %}value="{{ request.GET.q }}" {% endif %}/>
-						<span class="input-group-btn">
-							<button type="submit" class="btn btn-primary">
-								<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
-							</button>
-						</span>
-					</div>
-				</form>
-			</div>
-		</div>
+		{% include 'inc/search_panel.html' %}
+		{% include 'inc/filter_panel.html' %}
     </div>
     </div>
 </div>
 </div>
 {% endblock %}
 {% endblock %}

+ 39 - 22
netbox/templates/home.html

@@ -6,10 +6,10 @@
 	<div class="col-md-4">
 	<div class="col-md-4">
 		<form action="{% url 'dcim:device_list' %}" method="get">
 		<form action="{% url 'dcim:device_list' %}" method="get">
 			<div class="input-group input-group-lg">
 			<div class="input-group input-group-lg">
-				<input type="text" name="q" placeholder="Device name or serial" class="form-control" />
+				<input type="text" name="q" placeholder="Search devices" class="form-control" />
 				<span class="input-group-btn">
 				<span class="input-group-btn">
 					<button type="submit" class="btn btn-primary">
 					<button type="submit" class="btn btn-primary">
-						<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
+						<span class="fa fa-search" aria-hidden="true"></span>
 						Devices
 						Devices
 					</button>
 					</button>
 				</span>
 				</span>
@@ -20,11 +20,11 @@
 	<div class="col-md-4">
 	<div class="col-md-4">
 		<form action="{% url 'ipam:prefix_list' %}" method="get">
 		<form action="{% url 'ipam:prefix_list' %}" method="get">
 			<div class="input-group input-group-lg">
 			<div class="input-group input-group-lg">
-				<input type="text" name="q" placeholder="IP or network" class="form-control" />
+				<input type="text" name="q" placeholder="Search prefixes" class="form-control" />
 				<span class="input-group-btn">
 				<span class="input-group-btn">
 					<button type="submit" class="btn btn-primary">
 					<button type="submit" class="btn btn-primary">
-						<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
-						IP
+						<span class="fa fa-search" aria-hidden="true"></span>
+						Prefixes
 					</button>
 					</button>
 				</span>
 				</span>
 			</div>
 			</div>
@@ -34,10 +34,10 @@
 	<div class="col-md-4">
 	<div class="col-md-4">
 		<form action="{% url 'circuits:circuit_list' %}" method="get">
 		<form action="{% url 'circuits:circuit_list' %}" method="get">
 			<div class="input-group input-group-lg">
 			<div class="input-group input-group-lg">
-				<input type="text" name="q" placeholder="Circuit ID" class="form-control" />
+				<input type="text" name="q" placeholder="Search circuits" class="form-control" />
 				<span class="input-group-btn">
 				<span class="input-group-btn">
 					<button type="submit" class="btn btn-primary">
 					<button type="submit" class="btn btn-primary">
-						<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
+						<span class="fa fa-search" aria-hidden="true"></span>
 						Circuits
 						Circuits
 					</button>
 					</button>
 				</span>
 				</span>
@@ -50,7 +50,7 @@
     <div class="col-md-4">
     <div class="col-md-4">
         <div class="panel panel-default">
         <div class="panel panel-default">
             <div class="panel-heading">
             <div class="panel-heading">
-                <strong>DCIM</strong>
+                <strong>Organization</strong>
             </div>
             </div>
             <div class="list-group">
             <div class="list-group">
                 <div class="list-group-item">
                 <div class="list-group-item">
@@ -58,6 +58,18 @@
                     <h4 class="list-group-item-heading"><a href="{% url 'dcim:site_list' %}">Sites</a></h4>
                     <h4 class="list-group-item-heading"><a href="{% url 'dcim:site_list' %}">Sites</a></h4>
                     <p class="list-group-item-text text-muted">Geographic locations</p>
                     <p class="list-group-item-text text-muted">Geographic locations</p>
                 </div>
                 </div>
+                <div class="list-group-item">
+                    <span class="badge pull-right">{{ stats.tenant_count }}</span>
+                    <h4 class="list-group-item-heading"><a href="{% url 'tenancy:tenant_list' %}">Tenants</a></h4>
+                    <p class="list-group-item-text text-muted">Customers or departments</p>
+                </div>
+            </div>
+        </div>
+        <div class="panel panel-default">
+            <div class="panel-heading">
+                <strong>DCIM</strong>
+            </div>
+            <div class="list-group">
                 <div class="list-group-item">
                 <div class="list-group-item">
                     <span class="badge pull-right">{{ stats.rack_count }}</span>
                     <span class="badge pull-right">{{ stats.rack_count }}</span>
                     <h4 class="list-group-item-heading"><a href="{% url 'dcim:rack_list' %}">Racks</a></h4>
                     <h4 class="list-group-item-heading"><a href="{% url 'dcim:rack_list' %}">Racks</a></h4>
@@ -79,20 +91,6 @@
                 </div>
                 </div>
             </div>
             </div>
         </div>
         </div>
-        {% if perms.secrets %}
-            <div class="panel panel-default">
-                <div class="panel-heading">
-                    <strong>Secrets</strong>
-                </div>
-                <div class="list-group">
-                    <div class="list-group-item">
-                        <span class="badge pull-right">{{ stats.secret_count }}</span>
-                        <h4 class="list-group-item-heading"><a href="{% url 'secrets:secret_list' %}">Secrets</a></h4>
-                        <p class="list-group-item-text text-muted">Sensitive data (such as passwords) which has been stored securely</p>
-                    </div>
-                </div>
-            </div>
-        {% endif %}
     </div>
     </div>
     <div class="col-md-4">
     <div class="col-md-4">
         <div class="panel panel-default">
         <div class="panel panel-default">
@@ -100,6 +98,11 @@
                 <strong>IPAM</strong>
                 <strong>IPAM</strong>
             </div>
             </div>
             <div class="list-group">
             <div class="list-group">
+                <div class="list-group-item">
+                    <span class="badge pull-right">{{ stats.vrf_count }}</span>
+                    <h4 class="list-group-item-heading"><a href="{% url 'ipam:vrf_list' %}">VRFs</a></h4>
+                    <p class="list-group-item-text text-muted">Virtual routing and forwarding tables</p>
+                </div>
                 <div class="list-group-item">
                 <div class="list-group-item">
                     <span class="badge pull-right">{{ stats.aggregate_count }}</span>
                     <span class="badge pull-right">{{ stats.aggregate_count }}</span>
                     <h4 class="list-group-item-heading"><a href="{% url 'ipam:aggregate_list' %}">Aggregates</a></h4>
                     <h4 class="list-group-item-heading"><a href="{% url 'ipam:aggregate_list' %}">Aggregates</a></h4>
@@ -141,6 +144,20 @@
         </div>
         </div>
     </div>
     </div>
     <div class="col-md-4">
     <div class="col-md-4">
+        {% if perms.secrets %}
+            <div class="panel panel-default">
+                <div class="panel-heading">
+                    <strong>Secrets</strong>
+                </div>
+                <div class="list-group">
+                    <div class="list-group-item">
+                        <span class="badge pull-right">{{ stats.secret_count }}</span>
+                        <h4 class="list-group-item-heading"><a href="{% url 'secrets:secret_list' %}">Secrets</a></h4>
+                        <p class="list-group-item-text text-muted">Sensitive data (such as passwords) which has been stored securely</p>
+                    </div>
+                </div>
+            </div>
+        {% endif %}
         <div class="panel panel-default">
         <div class="panel panel-default">
             <div class="panel-heading">
             <div class="panel-heading">
                 <strong>Recent Activity</strong>
                 <strong>Recent Activity</strong>

+ 1 - 1
netbox/templates/import_success.html

@@ -7,7 +7,7 @@
 <h1>Import Completed</h1>
 <h1>Import Completed</h1>
 {% render_table table %}
 {% render_table table %}
 <a href="{{ request.path }}" class="btn btn-primary">
 <a href="{{ request.path }}" class="btn btn-primary">
-	<span class="glyphicon glyphicon-import" aria-hidden="true"></span>
+	<span class="fa fa-download" aria-hidden="true"></span>
 	Import more
 	Import more
 </a>
 </a>
 {% endblock %}
 {% endblock %}

+ 2 - 2
netbox/templates/inc/export_button.html

@@ -1,7 +1,7 @@
 {% if export_templates %}
 {% if export_templates %}
     <div class="btn-group">
     <div class="btn-group">
         <button type="button" class="btn btn-success dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
         <button type="button" class="btn btn-success dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
-            <span class="glyphicon glyphicon-export" aria-hidden="true"></span>
+            <span class="fa fa-upload" aria-hidden="true"></span>
             Export {{ obj_type }} <span class="caret"></span>
             Export {{ obj_type }} <span class="caret"></span>
         </button>
         </button>
         <ul class="dropdown-menu">
         <ul class="dropdown-menu">
@@ -14,7 +14,7 @@
     </div>
     </div>
 {% else %}
 {% else %}
     <a href="?{% if request.GET %}{{ request.GET.urlencode }}&{% endif %}export" class="btn btn-success">
     <a href="?{% if request.GET %}{{ request.GET.urlencode }}&{% endif %}export" class="btn btn-success">
-        <span class="glyphicon glyphicon-export" aria-hidden="true"></span>
+        <span class="fa fa-upload" aria-hidden="true"></span>
         Export {{ obj_type }}
         Export {{ obj_type }}
     </a>
     </a>
 {% endif %}
 {% endif %}

+ 2 - 2
netbox/templates/inc/filter_panel.html

@@ -2,7 +2,7 @@
 
 
 <div class="panel panel-default">
 <div class="panel panel-default">
     <div class="panel-heading">
     <div class="panel-heading">
-        <span class="glyphicon glyphicon-filter" aria-hidden="true"></span> 
+        <span class="fa fa-filter" aria-hidden="true"></span>
         <strong>Filter</strong>
         <strong>Filter</strong>
     </div>
     </div>
     <div class="panel-body">
     <div class="panel-body">
@@ -19,7 +19,7 @@
             {% endfor %}
             {% endfor %}
             <div class="text-right">
             <div class="text-right">
                 <button type="submit" class="btn btn-primary">
                 <button type="submit" class="btn btn-primary">
-                    <span class="glyphicon glyphicon-search" aria-hidden="true"></span> Apply filters
+                    <span class="fa fa-search" aria-hidden="true"></span> Apply filters
                 </button>
                 </button>
             </div>
             </div>
         </form>
         </form>

+ 18 - 0
netbox/templates/inc/search_panel.html

@@ -0,0 +1,18 @@
+<div class="panel panel-default">
+    <div class="panel-heading">
+        <span class="fa fa-search" aria-hidden="true"></span>
+        <strong>Search</strong>
+    </div>
+    <div class="panel-body">
+        <form action="." method="get">
+            <div class="input-group">
+                <input type="text" name="q" class="form-control" placeholder="Search" {% if request.GET.q %}value="{{ request.GET.q }}" {% endif %}/>
+                <span class="input-group-btn">
+                    <button type="submit" class="btn btn-primary">
+                        <span class="fa fa-search" aria-hidden="true"></span>
+                    </button>
+                </span>
+            </div>
+        </form>
+    </div>
+</div>

+ 18 - 6
netbox/templates/ipam/aggregate.html

@@ -5,24 +5,36 @@
 
 
 {% block content %}
 {% block content %}
 <div class="row">
 <div class="row">
-    <div class="col-md-12">
+    <div class="col-md-9">
         <ol class="breadcrumb">
         <ol class="breadcrumb">
             <li><a href="{% url 'ipam:aggregate_list' %}">Aggregates</a></li>
             <li><a href="{% url 'ipam:aggregate_list' %}">Aggregates</a></li>
             <li><a href="{% url 'ipam:aggregate_list' %}?rir={{ aggregate.rir.slug }}">{{ aggregate.rir }}</a></li>
             <li><a href="{% url 'ipam:aggregate_list' %}?rir={{ aggregate.rir.slug }}">{{ aggregate.rir }}</a></li>
             <li>{{ aggregate }}</li>
             <li>{{ aggregate }}</li>
         </ol>
         </ol>
     </div>
     </div>
+    <div class="col-md-3">
+        <form action="{% url 'ipam:aggregate_list' %}" method="get">
+            <div class="input-group">
+                <input type="text" name="q" class="form-control" placeholder="Search aggregates" />
+                <span class="input-group-btn">
+                    <button type="submit" class="btn btn-primary">
+                        <span class="fa fa-search" aria-hidden="true"></span>
+                    </button>
+                </span>
+            </div>
+        </form>
+    </div>
 </div>
 </div>
 <div class="pull-right">
 <div class="pull-right">
     {% if perms.ipam.change_aggregate %}
     {% if perms.ipam.change_aggregate %}
         <a href="{% url 'ipam:aggregate_edit' pk=aggregate.pk %}" class="btn btn-warning">
         <a href="{% url 'ipam:aggregate_edit' pk=aggregate.pk %}" class="btn btn-warning">
-            <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span>
+            <span class="fa fa-pencil" aria-hidden="true"></span>
             Edit this aggregate
             Edit this aggregate
         </a>
         </a>
     {% endif %}
     {% endif %}
     {% if perms.ipam.delete_aggregate %}
     {% if perms.ipam.delete_aggregate %}
         <a href="{% url 'ipam:aggregate_delete' pk=aggregate.pk %}" class="btn btn-danger">
         <a href="{% url 'ipam:aggregate_delete' pk=aggregate.pk %}" class="btn btn-danger">
-            <span class="glyphicon glyphicon-trash" aria-hidden="true"></span>
+            <span class="fa fa-trash" aria-hidden="true"></span>
             Delete this aggregate
             Delete this aggregate
         </a>
         </a>
     {% endif %}
     {% endif %}
@@ -51,17 +63,17 @@
                         {% if aggregate.date_added %}
                         {% if aggregate.date_added %}
                             <span>{{ aggregate.date_added }}</span>
                             <span>{{ aggregate.date_added }}</span>
                         {% else %}
                         {% else %}
-                            <span class="text-muted">Not defined</span>
+                            <span class="text-muted">N/A</span>
                         {% endif %}
                         {% endif %}
                     </td>
                     </td>
                 </tr>
                 </tr>
                 <tr>
                 <tr>
                     <td>Description</td>
                     <td>Description</td>
                     <td>
                     <td>
-                        {% if aggregate.description  %}
+                        {% if aggregate.description %}
                             <span>{{ aggregate.description }}</span>
                             <span>{{ aggregate.description }}</span>
                         {% else %}
                         {% else %}
-                            <span class="text-muted">None</span>
+                            <span class="text-muted">N/A</span>
                         {% endif %}
                         {% endif %}
                     </td>
                     </td>
                 </tr>
                 </tr>

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

@@ -8,7 +8,7 @@
 <div class="pull-right">
 <div class="pull-right">
     {% if perms.ipam.add_aggregate %}
     {% if perms.ipam.add_aggregate %}
 		<a href="{% url 'ipam:aggregate_add' %}" class="btn btn-primary">
 		<a href="{% url 'ipam:aggregate_add' %}" class="btn btn-primary">
-			<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
+			<span class="fa fa-plus" aria-hidden="true"></span>
 			Add an aggregate
 			Add an aggregate
 		</a>
 		</a>
     {% endif %}
     {% endif %}
@@ -22,6 +22,7 @@
         <p class="text-right">IPv6 total: <strong>{{ ipv6_total|intcomma }} /64s</strong></p>
         <p class="text-right">IPv6 total: <strong>{{ ipv6_total|intcomma }} /64s</strong></p>
 	</div>
 	</div>
 	<div class="col-md-3">
 	<div class="col-md-3">
+		{% include 'inc/search_panel.html' %}
 		{% include 'inc/filter_panel.html' %}
 		{% include 'inc/filter_panel.html' %}
 	</div>
 	</div>
 </div>
 </div>

+ 8 - 10
netbox/templates/ipam/inc/prefix_header.html

@@ -1,11 +1,9 @@
 <div class="row">
 <div class="row">
     <div class="col-md-9">
     <div class="col-md-9">
         <ol class="breadcrumb">
         <ol class="breadcrumb">
-            {% if prefix.site %}
-                <li><a href="{% url 'dcim:site' slug=prefix.site.slug %}">{{ prefix.site }}</a></li>
-                <li><a href="{% url 'ipam:prefix_list' %}?site={{ prefix.site.slug }}">Prefixes</a></li>
-            {% else %}
-                <li><a href="{% url 'ipam:prefix_list' %}">Prefixes</a></li>
+            <li><a href="{% url 'ipam:prefix_list' %}">Prefixes</a></li>
+            {% if prefix.vrf %}
+                <li><a href="{% url 'ipam:prefix_list' %}?vrf={{ prefix.vrf.pk }}">{{ prefix.vrf }}</a></li>
             {% endif %}
             {% endif %}
             <li>{{ prefix }}</li>
             <li>{{ prefix }}</li>
         </ol>
         </ol>
@@ -13,10 +11,10 @@
     <div class="col-md-3">
     <div class="col-md-3">
         <form action="{% url 'ipam:prefix_list' %}" method="get">
         <form action="{% url 'ipam:prefix_list' %}" method="get">
             <div class="input-group">
             <div class="input-group">
-                <input type="text" name="q" class="form-control" placeholder="Network or IP" />
+                <input type="text" name="q" class="form-control" placeholder="Search prefixes" />
                 <span class="input-group-btn">
                 <span class="input-group-btn">
                     <button type="submit" class="btn btn-primary">
                     <button type="submit" class="btn btn-primary">
-                        <span class="glyphicon glyphicon-search" aria-hidden="true"></span>
+                        <span class="fa fa-search" aria-hidden="true"></span>
                     </button>
                     </button>
                 </span>
                 </span>
             </div>
             </div>
@@ -26,19 +24,19 @@
 <div class="pull-right">
 <div class="pull-right">
     {% if perms.ipam.add_ipaddress %}
     {% if perms.ipam.add_ipaddress %}
 		<a href="{% url 'ipam:ipaddress_add' %}?address={{ prefix.prefix }}{% if prefix.vrf %}&vrf={{ prefix.vrf.pk }}{% endif %}" class="btn btn-success">
 		<a href="{% url 'ipam:ipaddress_add' %}?address={{ prefix.prefix }}{% if prefix.vrf %}&vrf={{ prefix.vrf.pk }}{% endif %}" class="btn btn-success">
-			<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
+			<span class="fa fa-plus" aria-hidden="true"></span>
 			Add an IP Address
 			Add an IP Address
 		</a>
 		</a>
     {% endif %}
     {% endif %}
     {% if perms.ipam.change_prefix %}
     {% if perms.ipam.change_prefix %}
 		<a href="{% url 'ipam:prefix_edit' pk=prefix.pk %}" class="btn btn-warning">
 		<a href="{% url 'ipam:prefix_edit' pk=prefix.pk %}" class="btn btn-warning">
-			<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span>
+			<span class="fa fa-pencil" aria-hidden="true"></span>
 			Edit this prefix
 			Edit this prefix
 		</a>
 		</a>
     {% endif %}
     {% endif %}
     {% if perms.ipam.delete_prefix %}
     {% if perms.ipam.delete_prefix %}
 		<a href="{% url 'ipam:prefix_delete' pk=prefix.pk %}" class="btn btn-danger">
 		<a href="{% url 'ipam:prefix_delete' pk=prefix.pk %}" class="btn btn-danger">
-			<span class="glyphicon glyphicon-trash" aria-hidden="true"></span>
+			<span class="fa fa-trash" aria-hidden="true"></span>
 			Delete this prefix
 			Delete this prefix
 		</a>
 		</a>
     {% endif %}
     {% endif %}

+ 24 - 12
netbox/templates/ipam/ipaddress.html

@@ -7,21 +7,20 @@
 <div class="row">
 <div class="row">
     <div class="col-md-9">
     <div class="col-md-9">
         <ol class="breadcrumb">
         <ol class="breadcrumb">
-            {% for p in parent_prefixes %}
-                <li><a href="{% url 'ipam:prefix' pk=p.pk %}">{{ p }}</a></li>
-            {% empty %}
-                <li><a href="{% url 'ipam:ipaddress_list' %}">IP Addresses</a></li>
-            {% endfor %}
-            <li>{{ ipaddress.address.ip }}</li>
+            <li><a href="{% url 'ipam:ipaddress_list' %}">IP Addresses</a></li>
+            {% if ipaddress.vrf %}
+                <li><a href="{% url 'ipam:ipaddress_list' %}?vrf={{ ipaddress.vrf.pk }}">{{ ipaddress.vrf }}</a></li>
+            {% endif %}
+            <li>{{ ipaddress }}</li>
         </ol>
         </ol>
     </div>
     </div>
     <div class="col-md-3">
     <div class="col-md-3">
         <form action="{% url 'ipam:ipaddress_list' %}" method="get">
         <form action="{% url 'ipam:ipaddress_list' %}" method="get">
             <div class="input-group">
             <div class="input-group">
-                <input type="text" name="q" class="form-control" placeholder="IP address" />
+                <input type="text" name="q" class="form-control" placeholder="Search IPs" />
                 <span class="input-group-btn">
                 <span class="input-group-btn">
                     <button type="submit" class="btn btn-primary">
                     <button type="submit" class="btn btn-primary">
-                        <span class="glyphicon glyphicon-search" aria-hidden="true"></span>
+                        <span class="fa fa-search" aria-hidden="true"></span>
                     </button>
                     </button>
                 </span>
                 </span>
             </div>
             </div>
@@ -31,13 +30,13 @@
 <div class="pull-right">
 <div class="pull-right">
     {% if perms.ipam.change_ipaddress %}
     {% if perms.ipam.change_ipaddress %}
         <a href="{% url 'ipam:ipaddress_edit' pk=ipaddress.pk %}" class="btn btn-warning">
         <a href="{% url 'ipam:ipaddress_edit' pk=ipaddress.pk %}" class="btn btn-warning">
-            <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span>
+            <span class="fa fa-pencil" aria-hidden="true"></span>
             Edit this IP
             Edit this IP
         </a>
         </a>
     {% endif %}
     {% endif %}
     {% if perms.ipam.delete_ipaddress %}
     {% if perms.ipam.delete_ipaddress %}
         <a href="{% url 'ipam:ipaddress_delete' pk=ipaddress.pk %}" class="btn btn-danger">
         <a href="{% url 'ipam:ipaddress_delete' pk=ipaddress.pk %}" class="btn btn-danger">
-            <span class="glyphicon glyphicon-trash" aria-hidden="true"></span>
+            <span class="fa fa-trash" aria-hidden="true"></span>
             Delete this IP
             Delete this IP
         </a>
         </a>
     {% endif %}
     {% endif %}
@@ -64,13 +63,26 @@
                         {% endif %}
                         {% endif %}
                     </td>
                     </td>
                 </tr>
                 </tr>
+                <tr>
+                    <td>Tenant</td>
+                    <td>
+                        {% if ipaddress.tenant %}
+                            <a href="{{ ipaddress.tenant.get_absolute_url }}">{{ ipaddress.tenant }}</a>
+                        {% elif ipaddress.vrf.tenant %}
+                            <a href="{{ ipaddress.vrf.tenant.get_absolute_url }}">{{ ipaddress.vrf.tenant }}</a>
+                            <label class="label label-info">Inherited</label>
+                        {% else %}
+                            <span class="text-muted">None</span>
+                        {% endif %}
+                    </td>
+                </tr>
                 <tr>
                 <tr>
                     <td>Description</td>
                     <td>Description</td>
                     <td>
                     <td>
-                        {% if ipaddress.description  %}
+                        {% if ipaddress.description %}
                             <span>{{ ipaddress.description }}</span>
                             <span>{{ ipaddress.description }}</span>
                         {% else %}
                         {% else %}
-                            <span class="text-muted">None</span>
+                            <span class="text-muted">N/A</span>
                         {% endif %}
                         {% endif %}
                     </td>
                     </td>
                 </tr>
                 </tr>

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

@@ -7,7 +7,8 @@
     {% for ipaddress in selected_objects %}
     {% for ipaddress in selected_objects %}
         <tr>
         <tr>
             <td><a href="{% url 'ipam:ipaddress' pk=ipaddress.pk %}">{{ ipaddress }}</a></td>
             <td><a href="{% url 'ipam:ipaddress' pk=ipaddress.pk %}">{{ ipaddress }}</a></td>
-            <td>{{ ipaddress.vrf }}</td>
+            <td>{{ ipaddress.vrf|default:"Global" }}</td>
+            <td>{{ ipaddress.tenant }}</td>
             <td>{{ ipaddress.interface.device }}</td>
             <td>{{ ipaddress.interface.device }}</td>
             <td>{{ ipaddress.interface }}</td>
             <td>{{ ipaddress.interface }}</td>
             <td>{{ ipaddress.description }}</td>
             <td>{{ ipaddress.description }}</td>

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

@@ -8,6 +8,7 @@
         <div class="panel-body">
         <div class="panel-body">
             {% render_field form.address %}
             {% render_field form.address %}
             {% render_field form.vrf %}
             {% render_field form.vrf %}
+            {% render_field form.tenant %}
             {% if obj %}
             {% if obj %}
                 <div class="form-group">
                 <div class="form-group">
                     <label class="col-md-3 control-label">Device</label>
                     <label class="col-md-3 control-label">Device</label>

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

@@ -38,6 +38,11 @@
 					<td>VRF route distinguisher (optional)</td>
 					<td>VRF route distinguisher (optional)</td>
 					<td>65000:123</td>
 					<td>65000:123</td>
 				</tr>
 				</tr>
+				<tr>
+					<td>Tenant</td>
+					<td>Name of tenant (optional)</td>
+					<td>ABC01</td>
+				</tr>
 				<tr>
 				<tr>
 					<td>Device</td>
 					<td>Device</td>
 					<td>Device name (optional)</td>
 					<td>Device name (optional)</td>
@@ -61,7 +66,7 @@
 			</tbody>
 			</tbody>
 		</table>
 		</table>
 		<h4>Example</h4>
 		<h4>Example</h4>
-		<pre>192.0.2.42/24,65000:123,switch12,ge-0/0/31,True,Management IP</pre>
+		<pre>192.0.2.42/24,65000:123,ABC01,switch12,ge-0/0/31,True,Management IP</pre>
 	</div>
 	</div>
 </div>
 </div>
 {% endblock %}
 {% endblock %}

+ 3 - 20
netbox/templates/ipam/ipaddress_list.html

@@ -8,11 +8,11 @@
 <div class="pull-right">
 <div class="pull-right">
     {% if perms.ipam.add_ipaddress %}
     {% if perms.ipam.add_ipaddress %}
 		<a href="{% url 'ipam:ipaddress_add' %}" class="btn btn-primary">
 		<a href="{% url 'ipam:ipaddress_add' %}" class="btn btn-primary">
-			<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
+			<span class="fa fa-plus" aria-hidden="true"></span>
 			Add an IP
 			Add an IP
 		</a>
 		</a>
 		<a href="{% url 'ipam:ipaddress_import' %}" class="btn btn-info">
 		<a href="{% url 'ipam:ipaddress_import' %}" class="btn btn-info">
-			<span class="glyphicon glyphicon-import" aria-hidden="true"></span>
+			<span class="fa fa-download" aria-hidden="true"></span>
 			Import IPs
 			Import IPs
 		</a>
 		</a>
 	{% endif %}
 	{% endif %}
@@ -24,24 +24,7 @@
         {% include 'utilities/obj_table.html' with bulk_edit_url='ipam:ipaddress_bulk_edit' bulk_delete_url='ipam:ipaddress_bulk_delete' %}
         {% include 'utilities/obj_table.html' with bulk_edit_url='ipam:ipaddress_bulk_edit' bulk_delete_url='ipam:ipaddress_bulk_delete' %}
 	</div>
 	</div>
 	<div class="col-md-3">
 	<div class="col-md-3">
-		<div class="panel panel-default">
-			<div class="panel-heading">
-				<span class="glyphicon glyphicon-search" aria-hidden="true"></span> 
-				<strong>Search</strong>
-			</div>
-			<div class="panel-body">
-				<form action="{% url 'ipam:ipaddress_list' %}" method="get">
-					<div class="input-group">
-						<input type="text" name="q" class="form-control" placeholder="IP address" {%  if request.GET.q %}value="{{ request.GET.q }}" {% endif %}/>
-						<span class="input-group-btn">
-							<button type="submit" class="btn btn-primary">
-								<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
-							</button>
-						</span>
-					</div>
-				</form>
-			</div>
-		</div>
+		{% include 'inc/search_panel.html' %}
 		{% include 'inc/filter_panel.html' %}
 		{% include 'inc/filter_panel.html' %}
 	</div>
 	</div>
 </div>
 </div>

+ 25 - 6
netbox/templates/ipam/prefix.html

@@ -26,6 +26,19 @@
                         {% endif %}
                         {% endif %}
                     </td>
                     </td>
                 </tr>
                 </tr>
+                <tr>
+                    <td>Tenant</td>
+                    <td>
+                        {% if prefix.tenant %}
+                            <a href="{{ prefix.tenant.get_absolute_url }}">{{ prefix.tenant }}</a>
+                        {% elif prefix.vrf.tenant %}
+                            <a href="{{ prefix.vrf.tenant.get_absolute_url }}">{{ prefix.vrf.tenant }}</a>
+                            <label class="label label-info">Inherited</label>
+                        {% else %}
+                            <span class="text-muted">None</span>
+                        {% endif %}
+                    </td>
+                </tr>
                 <tr>
                 <tr>
                     <td>Aggregate</td>
                     <td>Aggregate</td>
                     <td>
                     <td>
@@ -42,7 +55,7 @@
                         {% if prefix.site %}
                         {% if prefix.site %}
                             <a href="{% url 'dcim:site' slug=prefix.site.slug %}">{{ prefix.site }}</a>
                             <a href="{% url 'dcim:site' slug=prefix.site.slug %}">{{ prefix.site }}</a>
                         {% else %}
                         {% else %}
-                            <span class="text-muted">Not assigned</span>
+                            <span class="text-muted">None</span>
                         {% endif %}
                         {% endif %}
                     </td>
                     </td>
                 </tr>
                 </tr>
@@ -52,7 +65,7 @@
                         {% if prefix.vlan %}
                         {% if prefix.vlan %}
                             <a href="{% url 'ipam:vlan' pk=prefix.vlan.pk %}">{{ prefix.vlan.display_name }}</a>
                             <a href="{% url 'ipam:vlan' pk=prefix.vlan.pk %}">{{ prefix.vlan.display_name }}</a>
                         {% else %}
                         {% else %}
-                            <span class="text-muted">Not assigned</span>
+                            <span class="text-muted">None</span>
                         {% endif %}
                         {% endif %}
                     </td>
                     </td>
                 </tr>
                 </tr>
@@ -64,15 +77,21 @@
                 </tr>
                 </tr>
                 <tr>
                 <tr>
                     <td>Role</td>
                     <td>Role</td>
-                    <td>{{ prefix.role }}</td>
+                    <td>
+                        {% if prefix.role %}
+                            <span>{{ prefix.role }}</span>
+                        {% else %}
+                            <span class="text-muted">None</span>
+                        {% endif %}
+                    </td>
                 </tr>
                 </tr>
                 <tr>
                 <tr>
                     <td>Description</td>
                     <td>Description</td>
                     <td>
                     <td>
-                        {% if prefix.description  %}
+                        {% if prefix.description %}
                             <span>{{ prefix.description }}</span>
                             <span>{{ prefix.description }}</span>
                         {% else %}
                         {% else %}
-                            <span class="text-muted">None</span>
+                            <span class="text-muted">N/A</span>
                         {% endif %}
                         {% endif %}
                     </td>
                     </td>
                 </tr>
                 </tr>
@@ -108,7 +127,7 @@
             {% include 'utilities/obj_table.html' with table=child_prefix_table table_template='panel_table.html' heading='Child Prefixes' parent=prefix bulk_edit_url='ipam:prefix_bulk_edit' bulk_delete_url='ipam:prefix_bulk_delete' %}
             {% include 'utilities/obj_table.html' with table=child_prefix_table table_template='panel_table.html' heading='Child Prefixes' parent=prefix bulk_edit_url='ipam:prefix_bulk_edit' bulk_delete_url='ipam:prefix_bulk_delete' %}
         {% elif prefix.new_subnet %}
         {% elif prefix.new_subnet %}
             <a href="{% url 'ipam:prefix_add' %}?prefix={{ prefix.new_subnet }}{% if prefix.vrf %}&vrf={{ prefix.vrf.pk }}{% endif %}{% if prefix.site %}&site={{ prefix.site.pk }}{% endif %}" class="btn btn-success">
             <a href="{% url 'ipam:prefix_add' %}?prefix={{ prefix.new_subnet }}{% if prefix.vrf %}&vrf={{ prefix.vrf.pk }}{% endif %}{% if prefix.site %}&site={{ prefix.site.pk }}{% endif %}" class="btn btn-success">
-                <i class="glyphicon glyphicon-plus" aria-hidden="true"></i> Add Child Prefix
+                <i class="fa fa-plus" aria-hidden="true"></i> Add Child Prefix
             </a>
             </a>
         {% endif %}
         {% endif %}
     </div>
     </div>

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

@@ -8,6 +8,7 @@
         <tr>
         <tr>
             <td><a href="{% url 'ipam:prefix' pk=prefix.pk %}">{{ prefix }}</a></td>
             <td><a href="{% url 'ipam:prefix' pk=prefix.pk %}">{{ prefix }}</a></td>
             <td>{{ prefix.vrf|default:"Global" }}</td>
             <td>{{ prefix.vrf|default:"Global" }}</td>
+            <td>{{ prefix.tenant }}</td>
             <td>{{ prefix.site }}</td>
             <td>{{ prefix.site }}</td>
             <td>{{ prefix.status }}</td>
             <td>{{ prefix.status }}</td>
             <td>{{ prefix.role }}</td>
             <td>{{ prefix.role }}</td>

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

@@ -38,6 +38,11 @@
 					<td>VRF route distinguisher (optional)</td>
 					<td>VRF route distinguisher (optional)</td>
 					<td>65000:123</td>
 					<td>65000:123</td>
 				</tr>
 				</tr>
+				<tr>
+					<td>Tenant</td>
+					<td>Name of tenant (optional)</td>
+					<td>ABC01</td>
+				</tr>
 				<tr>
 				<tr>
 					<td>Site</td>
 					<td>Site</td>
 					<td>Name of assigned site (optional)</td>
 					<td>Name of assigned site (optional)</td>
@@ -71,7 +76,7 @@
 			</tbody>
 			</tbody>
 		</table>
 		</table>
 		<h4>Example</h4>
 		<h4>Example</h4>
-		<pre>192.168.42.0/24,65000:123,HQ,Customers,801,Active,Customer,7th floor WiFi</pre>
+		<pre>192.168.42.0/24,65000:123,ABC01,HQ,Customers,801,Active,Customer,7th floor WiFi</pre>
 	</div>
 	</div>
 </div>
 </div>
 {% endblock %}
 {% endblock %}

+ 3 - 20
netbox/templates/ipam/prefix_list.html

@@ -8,11 +8,11 @@
 <div class="pull-right">
 <div class="pull-right">
     {% if perms.ipam.add_prefix %}
     {% if perms.ipam.add_prefix %}
 		<a href="{% url 'ipam:prefix_add' %}" class="btn btn-primary">
 		<a href="{% url 'ipam:prefix_add' %}" class="btn btn-primary">
-			<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
+			<span class="fa fa-plus" aria-hidden="true"></span>
 			Add a prefix
 			Add a prefix
 		</a>
 		</a>
 		<a href="{% url 'ipam:prefix_import' %}" class="btn btn-info">
 		<a href="{% url 'ipam:prefix_import' %}" class="btn btn-info">
-			<span class="glyphicon glyphicon-import" aria-hidden="true"></span>
+			<span class="fa fa-download" aria-hidden="true"></span>
 			Import prefixes
 			Import prefixes
 		</a>
 		</a>
 	{% endif %}
 	{% endif %}
@@ -24,24 +24,7 @@
         {% include 'utilities/obj_table.html' with bulk_edit_url='ipam:prefix_bulk_edit' bulk_delete_url='ipam:prefix_bulk_delete' %}
         {% include 'utilities/obj_table.html' with bulk_edit_url='ipam:prefix_bulk_edit' bulk_delete_url='ipam:prefix_bulk_delete' %}
 	</div>
 	</div>
 	<div class="col-md-3">
 	<div class="col-md-3">
-		<div class="panel panel-default">
-			<div class="panel-heading">
-				<span class="glyphicon glyphicon-search" aria-hidden="true"></span> 
-				<strong>Search</strong>
-			</div>
-			<div class="panel-body">
-				<form action="{% url 'ipam:prefix_list' %}" method="get">
-					<div class="input-group">
-						<input type="text" name="q" class="form-control" placeholder="Network" {% if request.GET.q %}value="{{ request.GET.q }}" {% endif %}/>
-						<span class="input-group-btn">
-							<button type="submit" class="btn btn-primary">
-								<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
-							</button>
-						</span>
-					</div>
-				</form>
-			</div>
-		</div>
+		{% include 'inc/search_panel.html' %}
 		{% include 'inc/filter_panel.html' %}
 		{% include 'inc/filter_panel.html' %}
 	</div>
 	</div>
 </div>
 </div>

+ 1 - 1
netbox/templates/ipam/rir_list.html

@@ -7,7 +7,7 @@
 <div class="pull-right">
 <div class="pull-right">
     {% if perms.ipam.add_rir %}
     {% if perms.ipam.add_rir %}
         <a href="{% url 'ipam:rir_add' %}" class="btn btn-primary">
         <a href="{% url 'ipam:rir_add' %}" class="btn btn-primary">
-            <span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
+            <span class="fa fa-plus" aria-hidden="true"></span>
             Add a RIR
             Add a RIR
         </a>
         </a>
     {% endif %}
     {% endif %}

+ 1 - 1
netbox/templates/ipam/role_list.html

@@ -7,7 +7,7 @@
 <div class="pull-right">
 <div class="pull-right">
     {% if perms.dcim.add_devicerole %}
     {% if perms.dcim.add_devicerole %}
         <a href="{% url 'ipam:role_add' %}" class="btn btn-primary">
         <a href="{% url 'ipam:role_add' %}" class="btn btn-primary">
-            <span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
+            <span class="fa fa-plus" aria-hidden="true"></span>
             Add a role
             Add a role
         </a>
         </a>
     {% endif %}
     {% endif %}

+ 30 - 30
netbox/templates/ipam/vlan.html

@@ -7,18 +7,21 @@
 <div class="row">
 <div class="row">
     <div class="col-md-9">
     <div class="col-md-9">
         <ol class="breadcrumb">
         <ol class="breadcrumb">
-            <li><a href="{% url 'dcim:site' slug=vlan.site.slug %}">{{ vlan.site }}</a></li>
-            <li><a href="{% url 'ipam:vlan_list' %}?site={{ vlan.site.slug }}">VLANs</a></li>
+            <li><a href="{% url 'ipam:vlan_list' %}">VLANs</a></li>
+            <li><a href="{% url 'ipam:vlan_list' %}?site={{ vlan.site.slug }}">{{ vlan.site }}</a></li>
+            {% if vlan.group %}
+                <li><a href="{% url 'ipam:vlan_list' %}?site={{ vlan.site.slug }}&group={{ vlan.group.slug }}">{{ vlan.group.name }}</a></li>
+            {% endif %}
             <li>{{ vlan.name }} ({{ vlan.vid }})</li>
             <li>{{ vlan.name }} ({{ vlan.vid }})</li>
         </ol>
         </ol>
     </div>
     </div>
     <div class="col-md-3">
     <div class="col-md-3">
     <form action="{% url 'ipam:vlan_list' %}" method="get">
     <form action="{% url 'ipam:vlan_list' %}" method="get">
         <div class="input-group">
         <div class="input-group">
-            <input type="text" name="vid" class="form-control" placeholder="VLAN ID search" />
+            <input type="text" name="q" class="form-control" placeholder="Search VLANs" />
             <span class="input-group-btn">
             <span class="input-group-btn">
                 <button type="submit" class="btn btn-primary">
                 <button type="submit" class="btn btn-primary">
-                    <span class="glyphicon glyphicon-search" aria-hidden="true"></span>
+                    <span class="fa fa-search" aria-hidden="true"></span>
                 </button>
                 </button>
             </span>
             </span>
         </div>
         </div>
@@ -28,13 +31,13 @@
 <div class="pull-right">
 <div class="pull-right">
     {% if perms.ipam.change_vlan %}
     {% if perms.ipam.change_vlan %}
         <a href="{% url 'ipam:vlan_edit' pk=vlan.pk %}" class="btn btn-warning">
         <a href="{% url 'ipam:vlan_edit' pk=vlan.pk %}" class="btn btn-warning">
-            <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span>
+            <span class="fa fa-pencil" aria-hidden="true"></span>
             Edit this VLAN
             Edit this VLAN
         </a>
         </a>
     {% endif %}
     {% endif %}
     {% if perms.ipam.delete_vlan %}
     {% if perms.ipam.delete_vlan %}
         <a href="{% url 'ipam:vlan_delete' pk=vlan.pk %}" class="btn btn-danger">
         <a href="{% url 'ipam:vlan_delete' pk=vlan.pk %}" class="btn btn-danger">
-            <span class="glyphicon glyphicon-trash" aria-hidden="true"></span>
+            <span class="fa fa-trash" aria-hidden="true"></span>
             Delete this VLAN
             Delete this VLAN
         </a>
         </a>
     {% endif %}
     {% endif %}
@@ -70,10 +73,10 @@
                     <td>{{ vlan.name }}</td>
                     <td>{{ vlan.name }}</td>
                 </tr>
                 </tr>
                 <tr>
                 <tr>
-                    <td>Description</td>
+                    <td>Tenant</td>
                     <td>
                     <td>
-                        {% if vlan.description %}
-                            {{ vlan.description }}
+                        {% if vlan.tenant %}
+                            <a href="{{ vlan.tenant.get_absolute_url }}">{{ vlan.tenant }}</a>
                         {% else %}
                         {% else %}
                             <span class="text-muted">None</span>
                             <span class="text-muted">None</span>
                         {% endif %}
                         {% endif %}
@@ -87,7 +90,23 @@
                 </tr>
                 </tr>
                 <tr>
                 <tr>
                     <td>Role</td>
                     <td>Role</td>
-                    <td>{{ vlan.role }}</td>
+                    <td>
+                        {% if vlan.role %}
+                            <span>{{ vlan.role }}</span>
+                        {% else %}
+                            <span class="text-muted">None</span>
+                        {% endif %}
+                    </td>
+                </tr>
+                <tr>
+                    <td>Description</td>
+                    <td>
+                        {% if vlan.description %}
+                            {{ vlan.description }}
+                        {% else %}
+                            <span class="text-muted">N/A</span>
+                        {% endif %}
+                    </td>
                 </tr>
                 </tr>
                 <tr>
                 <tr>
                     <td>Created</td>
                     <td>Created</td>
@@ -105,26 +124,7 @@
             <div class="panel-heading">
             <div class="panel-heading">
                 <strong>Prefixes</strong>
                 <strong>Prefixes</strong>
             </div>
             </div>
-            {% if prefixes %}
-                <table class="table table-hover panel-body">
-                    {% for p in prefixes %}
-                        <tr>
-                            <td>
-                                <a href="{% url 'ipam:prefix' pk=p.pk %}">{{ p }}</a>
-                            </td>
-                            <td>
-                                {% if p.site %}
-                                    <a href="{% url 'dcim:site' slug=p.site.slug %}">{{ p.site }}</a>
-                                {% endif %}
-                            </td>
-                            <td>{{ p.get_status_display }}</td>
-                            <td>{{ p.role }}</td>
-                        </tr>
-                    {% endfor %}
-                </table>
-            {% else %}
-                <div class="panel-body text-muted">None</div>
-            {% endif %}
+            {% render_table prefix_table %}
         </div>
         </div>
 	</div>
 	</div>
 </div>
 </div>

+ 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><a href="{% url 'ipam:vlan' pk=vlan.pk %}">{{ vlan.vid }}</a></td>
             <td>{{ vlan.name }}</td>
             <td>{{ vlan.name }}</td>
             <td>{{ vlan.site }}</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.role }}</td>
             <td>{{ vlan.description }}</td>
             <td>{{ vlan.description }}</td>
         </tr>
         </tr>

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

@@ -48,6 +48,11 @@
 					<td>Configured VLAN name</td>
 					<td>Configured VLAN name</td>
 					<td>Cameras</td>
 					<td>Cameras</td>
 				</tr>
 				</tr>
+				<tr>
+					<td>Tenant</td>
+					<td>Name of tenant (optional)</td>
+					<td>Internal</td>
+				</tr>
 				<tr>
 				<tr>
 					<td>Status</td>
 					<td>Status</td>
 					<td>Current status</td>
 					<td>Current status</td>
@@ -66,7 +71,7 @@
 			</tbody>
 			</tbody>
 		</table>
 		</table>
 		<h4>Example</h4>
 		<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>
 </div>
 </div>
 {% endblock %}
 {% endblock %}

+ 3 - 20
netbox/templates/ipam/vlan_list.html

@@ -8,11 +8,11 @@
 <div class="pull-right">
 <div class="pull-right">
     {% if perms.ipam.add_vlan %}
     {% if perms.ipam.add_vlan %}
 		<a href="{% url 'ipam:vlan_add' %}" class="btn btn-primary">
 		<a href="{% url 'ipam:vlan_add' %}" class="btn btn-primary">
-			<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
+			<span class="fa fa-plus" aria-hidden="true"></span>
 			Add a VLAN
 			Add a VLAN
 		</a>
 		</a>
 		<a href="{% url 'ipam:vlan_import' %}" class="btn btn-info">
 		<a href="{% url 'ipam:vlan_import' %}" class="btn btn-info">
-			<span class="glyphicon glyphicon-import" aria-hidden="true"></span>
+			<span class="fa fa-download" aria-hidden="true"></span>
 			Import VLANs
 			Import VLANs
 		</a>
 		</a>
 	{% endif %}
 	{% endif %}
@@ -24,24 +24,7 @@
         {% include 'utilities/obj_table.html' with bulk_edit_url='ipam:vlan_bulk_edit' bulk_delete_url='ipam:vlan_bulk_delete' %}
         {% include 'utilities/obj_table.html' with bulk_edit_url='ipam:vlan_bulk_edit' bulk_delete_url='ipam:vlan_bulk_delete' %}
 	</div>
 	</div>
 	<div class="col-md-3">
 	<div class="col-md-3">
-		<div class="panel panel-default">
-			<div class="panel-heading">
-				<span class="glyphicon glyphicon-search" aria-hidden="true"></span> 
-				<strong>Search by ID</strong>
-			</div>
-			<div class="panel-body">
-				<form action="{% url 'ipam:vlan_list' %}" method="get">
-					<div class="input-group">
-						<input type="text" name="vid" class="form-control" placeholder="VLAN ID" {%  if request.GET.vid %}value="{{ request.GET.vid }}" {% endif %}/>
-						<span class="input-group-btn">
-							<button type="submit" class="btn btn-primary">
-								<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
-							</button>
-						</span>
-					</div>
-				</form>
-			</div>
-		</div>
+		{% include 'inc/search_panel.html' %}
 		{% include 'inc/filter_panel.html' %}
 		{% include 'inc/filter_panel.html' %}
 	</div>
 	</div>
 </div>
 </div>

Some files were not shown because too many files changed in this diff