From b790d7d50f6b5f34e16eab3a04e9c9fcb2132702 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 26 Jul 2016 12:24:32 -0400 Subject: [PATCH 01/43] Post-release version bump --- netbox/netbox/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 5d61f83d5..649049378 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -12,7 +12,7 @@ except ImportError: "the documentation.") -VERSION = '1.3.2' +VERSION = '1.3.3-dev' # Import local configuration for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']: From fa2ccc1c18643f65892e0d5c706a11e5ac18992a Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 26 Jul 2016 14:58:37 -0400 Subject: [PATCH 02/43] Initial multitenancy implementation --- docs/data-model/tenancy.md | 9 ++ netbox/netbox/settings.py | 3 +- netbox/netbox/urls.py | 2 + netbox/netbox/views.py | 6 +- netbox/templates/_base.html | 27 +++-- netbox/templates/home.html | 42 ++++--- netbox/templates/tenancy/tenant.html | 81 ++++++++++++++ .../templates/tenancy/tenant_bulk_edit.html | 13 +++ netbox/templates/tenancy/tenant_edit.html | 20 ++++ netbox/templates/tenancy/tenant_import.html | 52 +++++++++ netbox/templates/tenancy/tenant_list.html | 42 +++++++ .../templates/tenancy/tenantgroup_list.html | 21 ++++ netbox/tenancy/__init__.py | 0 netbox/tenancy/admin.py | 23 ++++ netbox/tenancy/api/__init__.py | 0 netbox/tenancy/api/serializers.py | 38 +++++++ netbox/tenancy/api/urls.py | 16 +++ netbox/tenancy/api/views.py | 39 +++++++ netbox/tenancy/apps.py | 5 + netbox/tenancy/filters.py | 29 +++++ netbox/tenancy/forms.py | 61 +++++++++++ netbox/tenancy/migrations/0001_initial.py | 47 ++++++++ netbox/tenancy/migrations/__init__.py | 0 netbox/tenancy/models.py | 48 ++++++++ netbox/tenancy/tables.py | 43 ++++++++ netbox/tenancy/urls.py | 24 ++++ netbox/tenancy/views.py | 103 ++++++++++++++++++ 27 files changed, 768 insertions(+), 26 deletions(-) create mode 100644 docs/data-model/tenancy.md create mode 100644 netbox/templates/tenancy/tenant.html create mode 100644 netbox/templates/tenancy/tenant_bulk_edit.html create mode 100644 netbox/templates/tenancy/tenant_edit.html create mode 100644 netbox/templates/tenancy/tenant_import.html create mode 100644 netbox/templates/tenancy/tenant_list.html create mode 100644 netbox/templates/tenancy/tenantgroup_list.html create mode 100644 netbox/tenancy/__init__.py create mode 100644 netbox/tenancy/admin.py create mode 100644 netbox/tenancy/api/__init__.py create mode 100644 netbox/tenancy/api/serializers.py create mode 100644 netbox/tenancy/api/urls.py create mode 100644 netbox/tenancy/api/views.py create mode 100644 netbox/tenancy/apps.py create mode 100644 netbox/tenancy/filters.py create mode 100644 netbox/tenancy/forms.py create mode 100644 netbox/tenancy/migrations/0001_initial.py create mode 100644 netbox/tenancy/migrations/__init__.py create mode 100644 netbox/tenancy/models.py create mode 100644 netbox/tenancy/tables.py create mode 100644 netbox/tenancy/urls.py create mode 100644 netbox/tenancy/views.py diff --git a/docs/data-model/tenancy.md b/docs/data-model/tenancy.md new file mode 100644 index 000000000..d3e8b8c29 --- /dev/null +++ b/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." diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 649049378..256c6d4ce 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -12,7 +12,7 @@ except ImportError: "the documentation.") -VERSION = '1.3.3-dev' +VERSION = '1.4.0-dev' # Import local configuration for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']: @@ -108,6 +108,7 @@ INSTALLED_APPS = ( 'ipam', 'extras', 'secrets', + 'tenancy', 'users', 'utilities', ) diff --git a/netbox/netbox/urls.py b/netbox/netbox/urls.py index 3a9d6b00a..b67f04cfd 100644 --- a/netbox/netbox/urls.py +++ b/netbox/netbox/urls.py @@ -22,6 +22,7 @@ urlpatterns = [ url(r'^dcim/', include('dcim.urls', namespace='dcim')), url(r'^ipam/', include('ipam.urls', namespace='ipam')), url(r'^secrets/', include('secrets.urls', namespace='secrets')), + url(r'^tenancy/', include('tenancy.urls', namespace='tenancy')), url(r'^profile/', include('users.urls', namespace='users')), # API @@ -29,6 +30,7 @@ urlpatterns = [ 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/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-auth/', include('rest_framework.urls', namespace='rest_framework')), diff --git a/netbox/netbox/views.py b/netbox/netbox/views.py index 9fda7f92c..5abde702e 100644 --- a/netbox/netbox/views.py +++ b/netbox/netbox/views.py @@ -7,14 +7,18 @@ from dcim.models import Site, Rack, Device, ConsolePort, PowerPort, InterfaceCon from extras.models import UserAction from ipam.models import Aggregate, Prefix, IPAddress, VLAN from secrets.models import Secret +from tenancy.models import Tenant def home(request): stats = { - # DCIM + # Organization 'site_count': Site.objects.count(), + 'tenant_count': Tenant.objects.count(), + + # DCIM 'rack_count': Rack.objects.count(), 'device_count': Device.objects.count(), 'interface_connections_count': InterfaceConnection.objects.count(), diff --git a/netbox/templates/_base.html b/netbox/templates/_base.html index fedbd43bf..d3097fb68 100644 --- a/netbox/templates/_base.html +++ b/netbox/templates/_base.html @@ -24,17 +24,26 @@ {% endblock %} diff --git a/netbox/templates/dcim/rack.html b/netbox/templates/dcim/rack.html index 552716d39..93df009b9 100644 --- a/netbox/templates/dcim/rack.html +++ b/netbox/templates/dcim/rack.html @@ -86,6 +86,16 @@ {% endif %} + + Tenant + + {% if rack.tenant %} + {{ rack.tenant }} + {% else %} + None + {% endif %} + + Height {{ rack.u_height }}U diff --git a/netbox/templates/dcim/rack_bulk_edit.html b/netbox/templates/dcim/rack_bulk_edit.html index 2d58a1556..1085dd1e2 100644 --- a/netbox/templates/dcim/rack_bulk_edit.html +++ b/netbox/templates/dcim/rack_bulk_edit.html @@ -9,6 +9,7 @@ {{ rack }} {{ rack.facility_id }} {{ rack.site }} + {{ rack.tenant }} {{ rack.u_height }} {% endfor %} diff --git a/netbox/templates/dcim/rack_edit.html b/netbox/templates/dcim/rack_edit.html index 990b25465..ab055e8ee 100644 --- a/netbox/templates/dcim/rack_edit.html +++ b/netbox/templates/dcim/rack_edit.html @@ -9,6 +9,7 @@ {% render_field form.group %} {% render_field form.name %} {% render_field form.facility_id %} + {% render_field form.tenant %} {% render_field form.u_height %} diff --git a/netbox/templates/dcim/rack_import.html b/netbox/templates/dcim/rack_import.html index f370a6242..b6e797d39 100644 --- a/netbox/templates/dcim/rack_import.html +++ b/netbox/templates/dcim/rack_import.html @@ -48,6 +48,11 @@ Rack ID assigned by the facility (optional) J12.100 + + Tenant + Name of tenant (optional) + Pied Piper + Height Height in rack units @@ -56,7 +61,7 @@

Example

-
DC-4,Cage 1400,R101,J12.100,42
+
DC-4,Cage 1400,R101,J12.100,Pied Piper,42
{% endblock %} diff --git a/netbox/templates/dcim/site.html b/netbox/templates/dcim/site.html index a9db55e19..28562c088 100644 --- a/netbox/templates/dcim/site.html +++ b/netbox/templates/dcim/site.html @@ -52,6 +52,16 @@ Site + + + + diff --git a/netbox/templates/dcim/site_edit.html b/netbox/templates/dcim/site_edit.html index b951cefc1..405f3fd52 100644 --- a/netbox/templates/dcim/site_edit.html +++ b/netbox/templates/dcim/site_edit.html @@ -7,6 +7,7 @@
{% render_field form.name %} {% render_field form.slug %} + {% render_field form.tenant %} {% render_field form.facility %} {% render_field form.asn %} {% render_field form.physical_address %} diff --git a/netbox/templates/dcim/site_import.html b/netbox/templates/dcim/site_import.html index 4c2c79fef..030a3aaa0 100644 --- a/netbox/templates/dcim/site_import.html +++ b/netbox/templates/dcim/site_import.html @@ -38,6 +38,11 @@
+ + + + + @@ -51,7 +56,7 @@
Tenant + {% if site.tenant %} + {{ site.tenant }} + {% else %} + None + {% endif %} +
Facility {{ site.facility }}URL-friendly name ash4-south
TenantName of tenant (optional)Pied Piper
Facility Name of the hosting facility (optional)

Example

-
ASH-4 South,ash4-south,Equinix DC6,65000
+
ASH-4 South,ash4-south,Pied Piper,Equinix DC6,65000
{% endblock %} From 6f686283771d104db91c8b6d44dcb88c46a0489b Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 26 Jul 2016 17:10:11 -0400 Subject: [PATCH 06/43] Added tenant to circuit bulk editing; enabled filtering of circuits by tenant --- netbox/circuits/filters.py | 12 ++++++++++++ netbox/circuits/forms.py | 8 ++++++++ netbox/circuits/views.py | 2 +- 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/netbox/circuits/filters.py b/netbox/circuits/filters.py index fc6628e87..59ff6ca4c 100644 --- a/netbox/circuits/filters.py +++ b/netbox/circuits/filters.py @@ -3,6 +3,7 @@ import django_filters from django.db.models import Q from dcim.models import Site +from tenancy.models import Tenant from .models import Provider, Circuit, CircuitType @@ -62,6 +63,17 @@ class CircuitFilter(django_filters.FilterSet): to_field_name='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( name='site', queryset=Site.objects.all(), diff --git a/netbox/circuits/forms.py b/netbox/circuits/forms.py index f246bf130..d5535ef53 100644 --- a/netbox/circuits/forms.py +++ b/netbox/circuits/forms.py @@ -180,6 +180,7 @@ class CircuitBulkEditForm(forms.Form, BootstrapMixin): pk = forms.ModelMultipleChoiceField(queryset=Circuit.objects.all(), widget=forms.MultipleHiddenInput) type = forms.ModelChoiceField(queryset=CircuitType.objects.all(), required=False) provider = forms.ModelChoiceField(queryset=Provider.objects.all(), required=False) + tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False) port_speed = forms.IntegerField(required=False, label='Port speed (Kbps)') commit_rate = forms.IntegerField(required=False, label='Commit rate (Kbps)') comments = CommentField() @@ -195,6 +196,11 @@ def circuit_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(): site_choices = Site.objects.annotate(circuit_count=Count('circuits')) return [(s.slug, u'{} ({})'.format(s.name, s.circuit_count)) for s in site_choices] @@ -204,5 +210,7 @@ class CircuitFilterForm(forms.Form, BootstrapMixin): type = forms.MultipleChoiceField(required=False, choices=circuit_type_choices) provider = forms.MultipleChoiceField(required=False, choices=circuit_provider_choices, 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, widget=forms.SelectMultiple(attrs={'size': 8})) diff --git a/netbox/circuits/views.py b/netbox/circuits/views.py index db42a6db9..3076a9141 100644 --- a/netbox/circuits/views.py +++ b/netbox/circuits/views.py @@ -159,7 +159,7 @@ class CircuitBulkEditView(PermissionRequiredMixin, BulkEditView): def update_objects(self, pk_list, form): fields_to_update = {} - for field in ['type', 'provider', 'port_speed', 'commit_rate', 'comments']: + for field in ['type', 'provider', 'tenant', 'port_speed', 'commit_rate', 'comments']: if form.cleaned_data[field]: fields_to_update[field] = form.cleaned_data[field] From 7ca4c816c062a759093ad56e6e9f0a2f53309c2c Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 26 Jul 2016 17:16:03 -0400 Subject: [PATCH 07/43] Added related_name to tenant fields on Site, Rack, and Device --- .../migrations/0013_tenant_related_names.py | 31 +++++++++++++++++++ netbox/dcim/models.py | 6 ++-- 2 files changed, 34 insertions(+), 3 deletions(-) create mode 100644 netbox/dcim/migrations/0013_tenant_related_names.py diff --git a/netbox/dcim/migrations/0013_tenant_related_names.py b/netbox/dcim/migrations/0013_tenant_related_names.py new file mode 100644 index 000000000..f5e3bd725 --- /dev/null +++ b/netbox/dcim/migrations/0013_tenant_related_names.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.8 on 2016-07-26 21:15 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('dcim', '0012_site_rack_device_add_tenant'), + ] + + operations = [ + migrations.AlterField( + 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.AlterField( + 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.AlterField( + 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'), + ), + ] diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index aab81806f..39d075a94 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -153,7 +153,7 @@ class Site(CreatedUpdatedModel): """ name = models.CharField(max_length=50, unique=True) slug = models.SlugField(unique=True) - tenant = models.ForeignKey(Tenant, blank=True, null=True, on_delete=models.PROTECT) + tenant = models.ForeignKey(Tenant, blank=True, null=True, related_name='sites', on_delete=models.PROTECT) facility = models.CharField(max_length=50, blank=True) asn = ASNField(blank=True, null=True, verbose_name='ASN') physical_address = models.CharField(max_length=200, blank=True) @@ -240,7 +240,7 @@ class Rack(CreatedUpdatedModel): 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) group = models.ForeignKey('RackGroup', related_name='racks', blank=True, null=True, on_delete=models.SET_NULL) - tenant = models.ForeignKey(Tenant, blank=True, null=True, on_delete=models.PROTECT) + 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)') comments = models.TextField(blank=True) @@ -636,7 +636,7 @@ class Device(CreatedUpdatedModel): """ device_type = models.ForeignKey('DeviceType', related_name='instances', on_delete=models.PROTECT) device_role = models.ForeignKey('DeviceRole', related_name='devices', on_delete=models.PROTECT) - tenant = models.ForeignKey(Tenant, blank=True, null=True, 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) name = NullableCharField(max_length=50, blank=True, null=True, unique=True) serial = models.CharField(max_length=50, blank=True, verbose_name='Serial number') From faa12abc70efa8a05c6166fd3066dd91f6644351 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 26 Jul 2016 17:28:46 -0400 Subject: [PATCH 08/43] Enabled filtering of sites, racks, and devices by tenant --- netbox/dcim/filters.py | 34 ++++++++++++++++++++++++++++ netbox/dcim/forms.py | 24 ++++++++++++++++++++ netbox/dcim/views.py | 1 + netbox/templates/dcim/site_list.html | 1 + 4 files changed, 60 insertions(+) diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index 0a4b14150..08f0d671e 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -6,6 +6,7 @@ from .models import ( ConsolePort, ConsoleServerPort, Device, DeviceRole, DeviceType, Interface, InterfaceConnection, Manufacturer, Platform, PowerOutlet, PowerPort, Rack, RackGroup, Site, ) +from tenancy.models import Tenant class SiteFilter(django_filters.FilterSet): @@ -13,6 +14,17 @@ class SiteFilter(django_filters.FilterSet): action='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: model = Site @@ -74,6 +86,17 @@ class RackFilter(django_filters.FilterSet): to_field_name='slug', 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: model = Rack @@ -143,6 +166,17 @@ class DeviceFilter(django_filters.FilterSet): to_field_name='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( name='device_type', queryset=DeviceType.objects.all(), diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index c755a55a0..9658de907 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -76,6 +76,16 @@ class SiteImportForm(BulkImportForm, BootstrapMixin): csv = CSVDataField(csv_form=SiteFromCSVForm) +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 # @@ -181,11 +191,18 @@ def rack_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): site = forms.MultipleChoiceField(required=False, choices=rack_site_choices, widget=forms.SelectMultiple(attrs={'size': 8})) group_id = forms.MultipleChoiceField(required=False, choices=rack_group_choices, label='Rack Group', widget=forms.SelectMultiple(attrs={'size': 8})) + tenant = forms.MultipleChoiceField(required=False, choices=rack_tenant_choices, + widget=forms.SelectMultiple(attrs={'size': 8})) # @@ -542,6 +559,11 @@ def device_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(): 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] @@ -559,6 +581,8 @@ class DeviceFilterForm(forms.Form, BootstrapMixin): widget=forms.SelectMultiple(attrs={'size': 8})) role = forms.MultipleChoiceField(required=False, choices=device_role_choices, 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', widget=forms.SelectMultiple(attrs={'size': 8})) platform = forms.MultipleChoiceField(required=False, choices=device_platform_choices) diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 0d1034974..dc09403b3 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -63,6 +63,7 @@ def expand_pattern(string): class SiteListView(ObjectListView): queryset = Site.objects.select_related('tenant') filter = filters.SiteFilter + filter_form = forms.SiteFilterForm table = tables.SiteTable template_name = 'dcim/site_list.html' diff --git a/netbox/templates/dcim/site_list.html b/netbox/templates/dcim/site_list.html index d35410443..0c04faa27 100644 --- a/netbox/templates/dcim/site_list.html +++ b/netbox/templates/dcim/site_list.html @@ -37,6 +37,7 @@ + {% include 'inc/filter_panel.html' %} {% endblock %} From 27c21237ff728439e871514c3120b390226011c6 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 26 Jul 2016 17:44:32 -0400 Subject: [PATCH 09/43] Added description to Tenant model --- netbox/templates/tenancy/tenant.html | 10 ++++++++ netbox/tenancy/forms.py | 4 +-- .../migrations/0002_add_tenant_description.py | 25 +++++++++++++++++++ netbox/tenancy/models.py | 3 ++- netbox/tenancy/tables.py | 3 ++- 5 files changed, 41 insertions(+), 4 deletions(-) create mode 100644 netbox/tenancy/migrations/0002_add_tenant_description.py diff --git a/netbox/templates/tenancy/tenant.html b/netbox/templates/tenancy/tenant.html index 780b63d8e..90bccd99c 100644 --- a/netbox/templates/tenancy/tenant.html +++ b/netbox/templates/tenancy/tenant.html @@ -52,6 +52,16 @@ {{ tenant.group }} + + Description + + {% if tenant.description %} + {{ tenant.description }} + {% else %} + N/A + {% endif %} + + Created {{ tenant.created }} diff --git a/netbox/tenancy/forms.py b/netbox/tenancy/forms.py index a80956238..2661f9101 100644 --- a/netbox/tenancy/forms.py +++ b/netbox/tenancy/forms.py @@ -30,7 +30,7 @@ class TenantForm(forms.ModelForm, BootstrapMixin): class Meta: model = Tenant - fields = ['name', 'slug', 'group', 'comments'] + fields = ['name', 'slug', 'group', 'description', 'comments'] class TenantFromCSVForm(forms.ModelForm): @@ -39,7 +39,7 @@ class TenantFromCSVForm(forms.ModelForm): class Meta: model = Tenant - fields = ['name', 'slug', 'group', 'comments'] + fields = ['name', 'slug', 'group', 'description', 'comments'] class TenantImportForm(BulkImportForm, BootstrapMixin): diff --git a/netbox/tenancy/migrations/0002_add_tenant_description.py b/netbox/tenancy/migrations/0002_add_tenant_description.py new file mode 100644 index 000000000..4dea3ac38 --- /dev/null +++ b/netbox/tenancy/migrations/0002_add_tenant_description.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.8 on 2016-07-26 21:44 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tenancy', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='tenant', + name='description', + field=models.CharField(blank=True, help_text=b'Long-form name (optional)', max_length=100), + ), + migrations.AlterField( + model_name='tenant', + name='name', + field=models.CharField(max_length=30, unique=True), + ), + ] diff --git a/netbox/tenancy/models.py b/netbox/tenancy/models.py index 26d92b79d..748a68ea5 100644 --- a/netbox/tenancy/models.py +++ b/netbox/tenancy/models.py @@ -26,9 +26,10 @@ class Tenant(CreatedUpdatedModel): A Tenant represents an organization served by the NetBox owner. This is typically a customer or an internal department. """ - name = models.CharField(max_length=50, unique=True) + name = models.CharField(max_length=30, unique=True) slug = models.SlugField(unique=True) group = models.ForeignKey('TenantGroup', related_name='tenants', on_delete=models.PROTECT) + description = models.CharField(max_length=100, blank=True, help_text="Long-form name (optional)") comments = models.TextField(blank=True) class Meta: diff --git a/netbox/tenancy/tables.py b/netbox/tenancy/tables.py index 34496f446..d6e271747 100644 --- a/netbox/tenancy/tables.py +++ b/netbox/tenancy/tables.py @@ -37,7 +37,8 @@ class TenantTable(BaseTable): pk = ToggleColumn() name = tables.LinkColumn('tenancy:tenant', args=[Accessor('slug')], verbose_name='Name') group = tables.Column(verbose_name='Group') + description = tables.Column(verbose_name='Description') class Meta(BaseTable.Meta): model = Tenant - fields = ('pk', 'name', 'group') + fields = ('pk', 'name', 'group', 'description') From 41b2b7dbf638e5efe56919726726141fff9a3f3b Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 26 Jul 2016 17:47:40 -0400 Subject: [PATCH 10/43] Fixed Tenant import --- netbox/templates/tenancy/tenant_import.html | 11 ++++++++--- netbox/tenancy/forms.py | 2 +- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/netbox/templates/tenancy/tenant_import.html b/netbox/templates/tenancy/tenant_import.html index 9d05fa8d7..ef76cacb1 100644 --- a/netbox/templates/tenancy/tenant_import.html +++ b/netbox/templates/tenancy/tenant_import.html @@ -31,22 +31,27 @@ Name Tenant name - Widgets Inc. + WIDG01 Slug URL-friendly name - widgets-inc + widg01 Group Tenant group Customers + + Description + Long-form name or other text (optional) + Widgets Inc. +

Example

-
Widgets Inc.,widgets-inc,Customers
+
WIDG01,widg01,Customers,Widgets Inc.
{% endblock %} diff --git a/netbox/tenancy/forms.py b/netbox/tenancy/forms.py index 2661f9101..14ffe7ef4 100644 --- a/netbox/tenancy/forms.py +++ b/netbox/tenancy/forms.py @@ -39,7 +39,7 @@ class TenantFromCSVForm(forms.ModelForm): class Meta: model = Tenant - fields = ['name', 'slug', 'group', 'description', 'comments'] + fields = ['name', 'slug', 'group', 'description'] class TenantImportForm(BulkImportForm, BootstrapMixin): From 2236d2f941ec2c6df2eab5b3c0e99eb1d5ada915 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 26 Jul 2016 17:49:41 -0400 Subject: [PATCH 11/43] Fixed tenant assignment on bulk edit of racks, devices --- netbox/dcim/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index dc09403b3..00c462d9e 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -201,7 +201,7 @@ class RackBulkEditView(PermissionRequiredMixin, BulkEditView): def update_objects(self, pk_list, form): fields_to_update = {} - for field in ['site', 'group', 'u_height', 'comments']: + for field in ['site', 'group', 'tenant', 'u_height', 'comments']: if form.cleaned_data[field]: fields_to_update[field] = form.cleaned_data[field] @@ -633,7 +633,7 @@ class DeviceBulkEditView(PermissionRequiredMixin, BulkEditView): if form.cleaned_data['status']: status = form.cleaned_data['status'] 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]: fields_to_update[field] = form.cleaned_data[field] From 65b008a493df0c9501422b15106cee35aff7ab60 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 26 Jul 2016 18:01:01 -0400 Subject: [PATCH 12/43] Cleaned up migrations --- .../migrations/0004_circuit_add_tenant.py | 2 +- .../0012_site_rack_device_add_tenant.py | 8 ++--- .../migrations/0013_tenant_related_names.py | 31 ------------------- netbox/tenancy/migrations/0001_initial.py | 5 +-- .../migrations/0002_add_tenant_description.py | 25 --------------- 5 files changed, 8 insertions(+), 63 deletions(-) delete mode 100644 netbox/dcim/migrations/0013_tenant_related_names.py delete mode 100644 netbox/tenancy/migrations/0002_add_tenant_description.py diff --git a/netbox/circuits/migrations/0004_circuit_add_tenant.py b/netbox/circuits/migrations/0004_circuit_add_tenant.py index 87988bb43..641b13afd 100644 --- a/netbox/circuits/migrations/0004_circuit_add_tenant.py +++ b/netbox/circuits/migrations/0004_circuit_add_tenant.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Generated by Django 1.9.8 on 2016-07-26 19:29 +# Generated by Django 1.9.8 on 2016-07-26 21:59 from __future__ import unicode_literals from django.db import migrations, models diff --git a/netbox/dcim/migrations/0012_site_rack_device_add_tenant.py b/netbox/dcim/migrations/0012_site_rack_device_add_tenant.py index f1092b4fa..8dcf8f81a 100644 --- a/netbox/dcim/migrations/0012_site_rack_device_add_tenant.py +++ b/netbox/dcim/migrations/0012_site_rack_device_add_tenant.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Generated by Django 1.9.8 on 2016-07-26 20:06 +# Generated by Django 1.9.8 on 2016-07-26 21:59 from __future__ import unicode_literals from django.db import migrations, models @@ -17,16 +17,16 @@ class Migration(migrations.Migration): migrations.AddField( model_name='device', name='tenant', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='tenancy.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, to='tenancy.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, to='tenancy.Tenant'), + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='sites', to='tenancy.Tenant'), ), ] diff --git a/netbox/dcim/migrations/0013_tenant_related_names.py b/netbox/dcim/migrations/0013_tenant_related_names.py deleted file mode 100644 index f5e3bd725..000000000 --- a/netbox/dcim/migrations/0013_tenant_related_names.py +++ /dev/null @@ -1,31 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.9.8 on 2016-07-26 21:15 -from __future__ import unicode_literals - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('dcim', '0012_site_rack_device_add_tenant'), - ] - - operations = [ - migrations.AlterField( - 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.AlterField( - 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.AlterField( - 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'), - ), - ] diff --git a/netbox/tenancy/migrations/0001_initial.py b/netbox/tenancy/migrations/0001_initial.py index 990f78874..ed2f800ef 100644 --- a/netbox/tenancy/migrations/0001_initial.py +++ b/netbox/tenancy/migrations/0001_initial.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Generated by Django 1.9.8 on 2016-07-26 18:15 +# Generated by Django 1.9.8 on 2016-07-26 21:58 from __future__ import unicode_literals from django.db import migrations, models @@ -20,8 +20,9 @@ class Migration(migrations.Migration): ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('created', models.DateField(auto_now_add=True)), ('last_updated', models.DateTimeField(auto_now=True)), - ('name', models.CharField(max_length=50, unique=True)), + ('name', models.CharField(max_length=30, unique=True)), ('slug', models.SlugField(unique=True)), + ('description', models.CharField(blank=True, help_text=b'Long-form name (optional)', max_length=100)), ('comments', models.TextField(blank=True)), ], options={ diff --git a/netbox/tenancy/migrations/0002_add_tenant_description.py b/netbox/tenancy/migrations/0002_add_tenant_description.py deleted file mode 100644 index 4dea3ac38..000000000 --- a/netbox/tenancy/migrations/0002_add_tenant_description.py +++ /dev/null @@ -1,25 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.9.8 on 2016-07-26 21:44 -from __future__ import unicode_literals - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('tenancy', '0001_initial'), - ] - - operations = [ - migrations.AddField( - model_name='tenant', - name='description', - field=models.CharField(blank=True, help_text=b'Long-form name (optional)', max_length=100), - ), - migrations.AlterField( - model_name='tenant', - name='name', - field=models.CharField(max_length=30, unique=True), - ), - ] From 2abee211a20b241139f70bc6bd702ca01fccc38b Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 27 Jul 2016 11:29:20 -0400 Subject: [PATCH 13/43] Implemented tenancy for VRFs and VLANs --- netbox/ipam/admin.py | 13 +++++--- netbox/ipam/filters.py | 23 +++++++++++++ netbox/ipam/forms.py | 32 ++++++++++++++++--- .../migrations/0006_vrf_vlan_add_tenant.py | 27 ++++++++++++++++ netbox/ipam/models.py | 10 ++++-- netbox/ipam/tables.py | 6 ++-- netbox/ipam/views.py | 7 ++-- netbox/templates/ipam/vlan.html | 16 ++++++++-- netbox/templates/ipam/vlan_bulk_edit.html | 3 +- netbox/templates/ipam/vlan_import.html | 7 +++- netbox/templates/ipam/vrf.html | 10 ++++++ netbox/templates/ipam/vrf_bulk_edit.html | 1 + netbox/templates/ipam/vrf_import.html | 7 +++- netbox/templates/ipam/vrf_list.html | 1 + netbox/templates/tenancy/tenant_edit.html | 1 + netbox/tenancy/models.py | 1 + 16 files changed, 144 insertions(+), 21 deletions(-) create mode 100644 netbox/ipam/migrations/0006_vrf_vlan_add_tenant.py diff --git a/netbox/ipam/admin.py b/netbox/ipam/admin.py index 8668aeb77..5ab79b92d 100644 --- a/netbox/ipam/admin.py +++ b/netbox/ipam/admin.py @@ -7,7 +7,12 @@ from .models import ( @admin.register(VRF) class VRFAdmin(admin.ModelAdmin): - list_display = ['name', 'rd'] + list_display = ['name', 'rd', 'tenant', 'enforce_unique'] + list_filter = ['tenant'] + + def get_queryset(self, request): + qs = super(VRFAdmin, self).get_queryset(request) + return qs.select_related('tenant') @admin.register(Role) @@ -67,10 +72,10 @@ class VLANGroupAdmin(admin.ModelAdmin): @admin.register(VLAN) class VLANAdmin(admin.ModelAdmin): - list_display = ['site', 'vid', 'name', 'status', 'role'] - list_filter = ['site', 'status', 'role'] + list_display = ['site', 'vid', 'name', 'tenant', 'status', 'role'] + list_filter = ['site', 'tenant', 'status', 'role'] search_fields = ['vid', 'name'] def get_queryset(self, request): qs = super(VLANAdmin, self).get_queryset(request) - return qs.select_related('site', 'role') + return qs.select_related('site', 'tenant', 'role') diff --git a/netbox/ipam/filters.py b/netbox/ipam/filters.py index ef87bbaa1..badd9a08f 100644 --- a/netbox/ipam/filters.py +++ b/netbox/ipam/filters.py @@ -3,6 +3,7 @@ from netaddr import IPNetwork from netaddr.core import AddrFormatError from dcim.models import Site, Device, Interface +from tenancy.models import Tenant from .models import RIR, Aggregate, VRF, Prefix, IPAddress, VLAN, VLANGroup, Role @@ -13,6 +14,17 @@ class VRFFilter(django_filters.FilterSet): lookup_type='icontains', label='Name', ) + tenant_id = django_filters.ModelMultipleChoiceFilter( + name='tenant', + queryset=Tenant.objects.all(), + label='Tenant (ID)', + ) + tenant = django_filters.ModelMultipleChoiceFilter( + name='tenant', + queryset=Tenant.objects.all(), + to_field_name='slug', + label='Tenant (slug)', + ) class Meta: model = VRF @@ -226,6 +238,17 @@ class VLANFilter(django_filters.FilterSet): name='vid', label='VLAN number (1-4095)', ) + tenant_id = django_filters.ModelMultipleChoiceFilter( + name='tenant', + queryset=Tenant.objects.all(), + label='Tenant (ID)', + ) + tenant = django_filters.ModelMultipleChoiceFilter( + name='tenant', + queryset=Tenant.objects.all(), + to_field_name='slug', + label='Tenant (slug)', + ) role_id = django_filters.ModelMultipleChoiceFilter( name='role', queryset=Role.objects.all(), diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index c2286122c..9c8f4a4b8 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -4,6 +4,7 @@ from django import forms from django.db.models import Count from dcim.models import Site, Device, Interface +from tenancy.models import Tenant from utilities.forms import BootstrapMixin, APISelect, Livesearch, CSVDataField, BulkImportForm, SlugField from .models import ( @@ -23,7 +24,7 @@ class VRFForm(forms.ModelForm, BootstrapMixin): class Meta: model = VRF - fields = ['name', 'rd', 'enforce_unique', 'description'] + fields = ['name', 'rd', 'tenant', 'enforce_unique', 'description'] labels = { 'rd': "RD", } @@ -33,10 +34,12 @@ class VRFForm(forms.ModelForm, BootstrapMixin): class VRFFromCSVForm(forms.ModelForm): + tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False, + error_messages={'invalid_choice': 'Tenant not found.'}) class Meta: model = VRF - fields = ['name', 'rd', 'enforce_unique', 'description'] + fields = ['name', 'rd', 'tenant', 'enforce_unique', 'description'] class VRFImportForm(BulkImportForm, BootstrapMixin): @@ -45,9 +48,20 @@ class VRFImportForm(BulkImportForm, BootstrapMixin): class VRFBulkEditForm(forms.Form, BootstrapMixin): pk = forms.ModelMultipleChoiceField(queryset=VRF.objects.all(), widget=forms.MultipleHiddenInput) + tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False) description = forms.CharField(max_length=100, required=False) +def vrf_tenant_choices(): + tenant_choices = Tenant.objects.annotate(vrf_count=Count('vrfs')) + return [(t.slug, u'{} ({})'.format(t.name, t.vrf_count)) for t in tenant_choices] + + +class VRFFilterForm(forms.Form, BootstrapMixin): + tenant = forms.MultipleChoiceField(required=False, choices=vrf_tenant_choices, + widget=forms.SelectMultiple(attrs={'size': 8})) + + # # RIRs # @@ -444,7 +458,7 @@ class VLANForm(forms.ModelForm, BootstrapMixin): class Meta: model = VLAN - fields = ['site', 'group', 'vid', 'name', 'description', 'status', 'role'] + fields = ['site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description'] help_texts = { 'site': "The site at which this VLAN exists", 'group': "VLAN group (optional)", @@ -475,13 +489,15 @@ class VLANFromCSVForm(forms.ModelForm): error_messages={'invalid_choice': 'Device not found.'}) group = forms.ModelChoiceField(queryset=VLANGroup.objects.all(), required=False, to_field_name='name', error_messages={'invalid_choice': 'VLAN group not found.'}) + tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False, + error_messages={'invalid_choice': 'Tenant not found.'}) status_name = forms.ChoiceField(choices=[(s[1], s[0]) for s in VLAN_STATUS_CHOICES]) role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False, to_field_name='name', error_messages={'invalid_choice': 'Invalid role.'}) class Meta: model = VLAN - fields = ['site', 'group', 'vid', 'name', 'status_name', 'role', 'description'] + fields = ['site', 'group', 'vid', 'name', 'tenant', 'status_name', 'role', 'description'] def save(self, *args, **kwargs): m = super(VLANFromCSVForm, self).save(commit=False) @@ -500,6 +516,7 @@ class VLANBulkEditForm(forms.Form, BootstrapMixin): pk = forms.ModelMultipleChoiceField(queryset=VLAN.objects.all(), widget=forms.MultipleHiddenInput) site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False) group = forms.ModelChoiceField(queryset=VLANGroup.objects.all(), required=False) + tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False) status = forms.ChoiceField(choices=FORM_VLAN_STATUS_CHOICES, required=False) role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False) description = forms.CharField(max_length=100, required=False) @@ -515,6 +532,11 @@ def vlan_group_choices(): return [(g.pk, u'{} ({})'.format(g, g.vlan_count)) for g in group_choices] +def vlan_tenant_choices(): + tenant_choices = Tenant.objects.annotate(vrf_count=Count('vlans')) + return [(t.slug, u'{} ({})'.format(t.name, t.vrf_count)) for t in tenant_choices] + + def vlan_status_choices(): status_counts = {} for status in VLAN.objects.values('status').annotate(count=Count('status')).order_by('status'): @@ -532,6 +554,8 @@ class VLANFilterForm(forms.Form, BootstrapMixin): widget=forms.SelectMultiple(attrs={'size': 8})) group_id = forms.MultipleChoiceField(required=False, choices=vlan_group_choices, label='VLAN Group', widget=forms.SelectMultiple(attrs={'size': 8})) + tenant = forms.MultipleChoiceField(required=False, choices=vlan_tenant_choices, + widget=forms.SelectMultiple(attrs={'size': 8})) status = forms.MultipleChoiceField(required=False, choices=vlan_status_choices) role = forms.MultipleChoiceField(required=False, choices=vlan_role_choices, widget=forms.SelectMultiple(attrs={'size': 8})) diff --git a/netbox/ipam/migrations/0006_vrf_vlan_add_tenant.py b/netbox/ipam/migrations/0006_vrf_vlan_add_tenant.py new file mode 100644 index 000000000..8d519261d --- /dev/null +++ b/netbox/ipam/migrations/0006_vrf_vlan_add_tenant.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.8 on 2016-07-27 14:39 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('tenancy', '0001_initial'), + ('ipam', '0005_auto_20160725_1842'), + ] + + operations = [ + migrations.AddField( + model_name='vlan', + name='tenant', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='vlans', to='tenancy.Tenant'), + ), + migrations.AddField( + model_name='vrf', + name='tenant', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='vrfs', to='tenancy.Tenant'), + ), + ] diff --git a/netbox/ipam/models.py b/netbox/ipam/models.py index 510d8410a..d28e4eb12 100644 --- a/netbox/ipam/models.py +++ b/netbox/ipam/models.py @@ -7,6 +7,7 @@ from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models from dcim.models import Interface +from tenancy.models import Tenant from utilities.models import CreatedUpdatedModel from .fields import IPNetworkField, IPAddressField @@ -46,6 +47,7 @@ class VRF(CreatedUpdatedModel): """ name = models.CharField(max_length=50) rd = models.CharField(max_length=21, unique=True, verbose_name='Route distinguisher') + tenant = models.ForeignKey(Tenant, related_name='vrfs', blank=True, null=True, on_delete=models.PROTECT) enforce_unique = models.BooleanField(default=True, verbose_name='Enforce unique space', help_text="Prevent duplicate prefixes/IP addresses within this VRF") description = models.CharField(max_length=100, blank=True) @@ -65,6 +67,8 @@ class VRF(CreatedUpdatedModel): return ','.join([ self.name, self.rd, + self.tenant.name if self.tenant else '', + 'True' if self.enforce_unique else '', self.description, ]) @@ -291,7 +295,7 @@ class Prefix(CreatedUpdatedModel): class IPAddress(CreatedUpdatedModel): """ - An IPAddress represents an individual IPV4 or IPv6 address and its mask. The mask length should match what is + An IPAddress represents an individual IPv4 or IPv6 address and its mask. The mask length should match what is configured in the real world. (Typically, only loopback interfaces are configured with /32 or /128 masks.) Like Prefixes, IPAddresses can optionally be assigned to a VRF. An IPAddress can optionally be assigned to an Interface. Interfaces can have zero or more IPAddresses assigned to them. @@ -407,9 +411,10 @@ class VLAN(CreatedUpdatedModel): MaxValueValidator(4094) ]) name = models.CharField(max_length=64) - description = models.CharField(max_length=100, blank=True) + tenant = models.ForeignKey(Tenant, related_name='vlans', blank=True, null=True, on_delete=models.PROTECT) status = models.PositiveSmallIntegerField('Status', choices=VLAN_STATUS_CHOICES, default=1) role = models.ForeignKey('Role', related_name='vlans', on_delete=models.SET_NULL, blank=True, null=True) + description = models.CharField(max_length=100, blank=True) class Meta: ordering = ['site', 'group', 'vid'] @@ -438,6 +443,7 @@ class VLAN(CreatedUpdatedModel): self.group.name if self.group else '', str(self.vid), self.name, + self.tenant.name if self.tenant else '', self.get_status_display(), self.role.name if self.role else '', self.description, diff --git a/netbox/ipam/tables.py b/netbox/ipam/tables.py index 602462a2e..796854105 100644 --- a/netbox/ipam/tables.py +++ b/netbox/ipam/tables.py @@ -58,11 +58,12 @@ class VRFTable(BaseTable): pk = ToggleColumn() name = tables.LinkColumn('ipam:vrf', args=[Accessor('pk')], verbose_name='Name') rd = tables.Column(verbose_name='RD') + tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant') description = tables.Column(orderable=False, verbose_name='Description') class Meta(BaseTable.Meta): model = VRF - fields = ('pk', 'name', 'rd', 'description') + fields = ('pk', 'name', 'rd', 'tenant', 'description') # @@ -203,9 +204,10 @@ class VLANTable(BaseTable): site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site') group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group') name = tables.Column(verbose_name='Name') + tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant') status = tables.TemplateColumn(STATUS_LABEL, verbose_name='Status') role = tables.Column(verbose_name='Role') class Meta(BaseTable.Meta): model = VLAN - fields = ('pk', 'vid', 'site', 'group', 'name', 'status', 'role') + fields = ('pk', 'vid', 'site', 'group', 'name', 'tenant', 'status', 'role') diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index 193adf273..90afcfdd2 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -36,8 +36,9 @@ def add_available_prefixes(parent, prefix_list): # class VRFListView(ObjectListView): - queryset = VRF.objects.all() + queryset = VRF.objects.select_related('tenant') filter = filters.VRFFilter + filter_form = forms.VRFFilterForm table = tables.VRFTable edit_permissions = ['ipam.change_vrf', 'ipam.delete_vrf'] template_name = 'ipam/vrf_list.html' @@ -85,7 +86,7 @@ class VRFBulkEditView(PermissionRequiredMixin, BulkEditView): def update_objects(self, pk_list, form): fields_to_update = {} - for field in ['description']: + for field in ['tenant', 'description']: if form.cleaned_data[field]: fields_to_update[field] = form.cleaned_data[field] @@ -558,7 +559,7 @@ class VLANBulkEditView(PermissionRequiredMixin, BulkEditView): def update_objects(self, pk_list, form): fields_to_update = {} - for field in ['site', 'group', 'status', 'role', 'description']: + for field in ['site', 'group', 'tenant', 'status', 'role', 'description']: if form.cleaned_data[field]: fields_to_update[field] = form.cleaned_data[field] diff --git a/netbox/templates/ipam/vlan.html b/netbox/templates/ipam/vlan.html index 8adbcd1be..cf525ff58 100644 --- a/netbox/templates/ipam/vlan.html +++ b/netbox/templates/ipam/vlan.html @@ -70,10 +70,10 @@ {{ vlan.name }} - Description + Tenant - {% if vlan.description %} - {{ vlan.description }} + {% if vlan.tenant %} + {{ vlan.tenant }} {% else %} None {% endif %} @@ -89,6 +89,16 @@ Role {{ vlan.role }} + + Description + + {% if vlan.description %} + {{ vlan.description }} + {% else %} + None + {% endif %} + + Created {{ vlan.created }} diff --git a/netbox/templates/ipam/vlan_bulk_edit.html b/netbox/templates/ipam/vlan_bulk_edit.html index 67f98be08..38b01fe24 100644 --- a/netbox/templates/ipam/vlan_bulk_edit.html +++ b/netbox/templates/ipam/vlan_bulk_edit.html @@ -9,7 +9,8 @@ {{ vlan.vid }} {{ vlan.name }} {{ vlan.site }} - {{ vlan.status }} + {{ vlan.tenant }} + {{ vlan.get_status_display }} {{ vlan.role }} {{ vlan.description }} diff --git a/netbox/templates/ipam/vlan_import.html b/netbox/templates/ipam/vlan_import.html index affee3c18..2ba22feb7 100644 --- a/netbox/templates/ipam/vlan_import.html +++ b/netbox/templates/ipam/vlan_import.html @@ -48,6 +48,11 @@ Configured VLAN name Cameras + + Tenant + Name of tenant (optional) + Internal + Status Current status @@ -66,7 +71,7 @@

Example

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

Example

-
Customer_ABC,65000:123456,True,Native VRF for customer ABC
+
Customer_ABC,65000:123456,ABC01,True,Native VRF for customer ABC
{% endblock %} diff --git a/netbox/templates/ipam/vrf_list.html b/netbox/templates/ipam/vrf_list.html index 641aae1d6..eaa03e461 100644 --- a/netbox/templates/ipam/vrf_list.html +++ b/netbox/templates/ipam/vrf_list.html @@ -41,6 +41,7 @@ + {% include 'inc/filter_panel.html' %} {% endblock %} diff --git a/netbox/templates/tenancy/tenant_edit.html b/netbox/templates/tenancy/tenant_edit.html index cffb29510..3616e5966 100644 --- a/netbox/templates/tenancy/tenant_edit.html +++ b/netbox/templates/tenancy/tenant_edit.html @@ -9,6 +9,7 @@ {% render_field form.name %} {% render_field form.slug %} {% render_field form.group %} + {% render_field form.description %}
diff --git a/netbox/tenancy/models.py b/netbox/tenancy/models.py index 748a68ea5..72bc92cae 100644 --- a/netbox/tenancy/models.py +++ b/netbox/tenancy/models.py @@ -46,4 +46,5 @@ class Tenant(CreatedUpdatedModel): self.name, self.slug, self.group.name, + self.description, ]) From e4960873f379dd313fbc8763280f86dcb7296fde Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 27 Jul 2016 11:56:47 -0400 Subject: [PATCH 14/43] Added stats to tenant view --- netbox/templates/tenancy/tenant.html | 39 +++++++++++++++++++++++++--- netbox/tenancy/admin.py | 2 +- netbox/tenancy/views.py | 9 ++++++- 3 files changed, 45 insertions(+), 5 deletions(-) diff --git a/netbox/templates/tenancy/tenant.html b/netbox/templates/tenancy/tenant.html index 90bccd99c..c52252f63 100644 --- a/netbox/templates/tenancy/tenant.html +++ b/netbox/templates/tenancy/tenant.html @@ -40,7 +40,7 @@

{{ tenant }}

-
+
Tenant @@ -72,8 +72,6 @@
-
-
Comments @@ -86,6 +84,41 @@ {% endif %}
+
+
+
+
+ Stats +
+
+ + + +
+
+ + + +
+
{% endblock %} diff --git a/netbox/tenancy/admin.py b/netbox/tenancy/admin.py index d381b88ff..efd0d2ac8 100644 --- a/netbox/tenancy/admin.py +++ b/netbox/tenancy/admin.py @@ -16,7 +16,7 @@ class TenantAdmin(admin.ModelAdmin): prepopulated_fields = { 'slug': ['name'], } - list_display = ['name', 'slug', 'group'] + list_display = ['name', 'slug', 'group', 'description'] def get_queryset(self, request): qs = super(TenantAdmin, self).get_queryset(request) diff --git a/netbox/tenancy/views.py b/netbox/tenancy/views.py index db8befedf..0cf10aa80 100644 --- a/netbox/tenancy/views.py +++ b/netbox/tenancy/views.py @@ -50,7 +50,14 @@ class TenantListView(ObjectListView): def tenant(request, slug): - tenant = get_object_or_404(Tenant, slug=slug) + tenant = get_object_or_404(Tenant.objects.annotate( + site_count=Count('sites', distinct=True), + rack_count=Count('racks', distinct=True), + device_count=Count('devices', distinct=True), + vrf_count=Count('vrfs', distinct=True), + vlan_count=Count('vlans', distinct=True), + circuit_count=Count('circuits', distinct=True), + ), slug=slug) return render(request, 'tenancy/tenant.html', { 'tenant': tenant, From 2981ead41b468ac7060260b156b3a5a9fd1a2fef Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 27 Jul 2016 13:37:55 -0400 Subject: [PATCH 15/43] Extended IPAM API to support tenancy --- netbox/dcim/tests/test_apis.py | 6 ++++++ netbox/ipam/api/serializers.py | 7 +++++-- netbox/ipam/api/views.py | 8 ++++---- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/netbox/dcim/tests/test_apis.py b/netbox/dcim/tests/test_apis.py index 22128ba29..8b0d8ca53 100644 --- a/netbox/dcim/tests/test_apis.py +++ b/netbox/dcim/tests/test_apis.py @@ -15,6 +15,7 @@ class SiteTest(APITestCase): 'id', 'name', 'slug', + 'tenant', 'facility', 'asn', 'physical_address', @@ -40,6 +41,7 @@ class SiteTest(APITestCase): 'display_name', 'site', 'group', + 'tenant', 'u_height', 'comments' ] @@ -115,6 +117,7 @@ class RackTest(APITestCase): 'display_name', 'site', 'group', + 'tenant', 'u_height', 'comments' ] @@ -126,6 +129,7 @@ class RackTest(APITestCase): 'display_name', 'site', 'group', + 'tenant', 'u_height', 'comments', 'front_units', @@ -311,6 +315,7 @@ class DeviceTest(APITestCase): 'display_name', 'device_type', 'device_role', + 'tenant', 'platform', 'serial', 'rack', @@ -388,6 +393,7 @@ class DeviceTest(APITestCase): 'rack_name', 'serial', 'status', + 'tenant', ] response = self.client.get(endpoint) diff --git a/netbox/ipam/api/serializers.py b/netbox/ipam/api/serializers.py index bda8b1076..bdcab381c 100644 --- a/netbox/ipam/api/serializers.py +++ b/netbox/ipam/api/serializers.py @@ -2,6 +2,7 @@ from rest_framework import serializers from dcim.api.serializers import SiteNestedSerializer, InterfaceNestedSerializer 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): + tenant = TenantNestedSerializer() class Meta: model = VRF - fields = ['id', 'name', 'rd', 'enforce_unique', 'description'] + fields = ['id', 'name', 'rd', 'tenant', 'enforce_unique', 'description'] class VRFNestedSerializer(VRFSerializer): @@ -98,11 +100,12 @@ class VLANGroupNestedSerializer(VLANGroupSerializer): class VLANSerializer(serializers.ModelSerializer): site = SiteNestedSerializer() group = VLANGroupNestedSerializer() + tenant = TenantNestedSerializer() role = RoleNestedSerializer() class Meta: 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): diff --git a/netbox/ipam/api/views.py b/netbox/ipam/api/views.py index 30a15e218..baa1050cd 100644 --- a/netbox/ipam/api/views.py +++ b/netbox/ipam/api/views.py @@ -14,7 +14,7 @@ class VRFListView(generics.ListAPIView): """ List all VRFs """ - queryset = VRF.objects.all() + queryset = VRF.objects.select_related('tenant') serializer_class = serializers.VRFSerializer filter_class = filters.VRFFilter @@ -23,7 +23,7 @@ class VRFDetailView(generics.RetrieveAPIView): """ Retrieve a single VRF """ - queryset = VRF.objects.all() + queryset = VRF.objects.select_related('tenant') serializer_class = serializers.VRFSerializer @@ -161,7 +161,7 @@ class VLANListView(generics.ListAPIView): """ List VLANs (filterable) """ - queryset = VLAN.objects.select_related('site', 'role') + queryset = VLAN.objects.select_related('site', 'tenant', 'role') serializer_class = serializers.VLANSerializer filter_class = filters.VLANFilter @@ -170,5 +170,5 @@ class VLANDetailView(generics.RetrieveAPIView): """ Retrieve a single VLAN """ - queryset = VLAN.objects.select_related('site', 'role') + queryset = VLAN.objects.select_related('site', 'tenant', 'role') serializer_class = serializers.VLANSerializer From 300e67388b2f8d6fd81f76065e9eebbb973221aa Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 27 Jul 2016 13:42:17 -0400 Subject: [PATCH 16/43] Tenancy-related API cleanup --- netbox/circuits/api/views.py | 4 ++-- netbox/dcim/api/views.py | 13 +++++++------ 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/netbox/circuits/api/views.py b/netbox/circuits/api/views.py index d2d368302..74cc6656d 100644 --- a/netbox/circuits/api/views.py +++ b/netbox/circuits/api/views.py @@ -42,7 +42,7 @@ class CircuitListView(generics.ListAPIView): """ 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 filter_class = CircuitFilter @@ -51,5 +51,5 @@ class CircuitDetailView(generics.RetrieveAPIView): """ 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 diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index 8eac377f2..3775eda52 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -27,7 +27,7 @@ class SiteListView(generics.ListAPIView): """ List all sites """ - queryset = Site.objects.all() + queryset = Site.objects.select_related('tenant') serializer_class = serializers.SiteSerializer @@ -35,7 +35,7 @@ class SiteDetailView(generics.RetrieveAPIView): """ Retrieve a single site """ - queryset = Site.objects.all() + queryset = Site.objects.select_related('tenant') serializer_class = serializers.SiteSerializer @@ -68,7 +68,7 @@ class RackListView(generics.ListAPIView): """ List racks (filterable) """ - queryset = Rack.objects.select_related('site') + queryset = Rack.objects.select_related('site', 'tenant') serializer_class = serializers.RackSerializer filter_class = filters.RackFilter @@ -77,7 +77,7 @@ class RackDetailView(generics.RetrieveAPIView): """ Retrieve a single rack """ - queryset = Rack.objects.select_related('site') + queryset = Rack.objects.select_related('site', 'tenant') serializer_class = serializers.RackDetailSerializer @@ -193,8 +193,9 @@ class DeviceListView(generics.ListAPIView): """ 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').prefetch_related('primary_ip4__nat_outside', + 'primary_ip6__nat_outside') serializer_class = serializers.DeviceSerializer filter_class = filters.DeviceFilter renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES + [BINDZoneRenderer, FlatJSONRenderer] From 4cc84aed5a980f24dbd4236d96660c401cb94a90 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 27 Jul 2016 13:59:18 -0400 Subject: [PATCH 17/43] PEP8 fix --- netbox/ipam/forms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index 9c8f4a4b8..1fe790292 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -555,7 +555,7 @@ class VLANFilterForm(forms.Form, BootstrapMixin): group_id = forms.MultipleChoiceField(required=False, choices=vlan_group_choices, label='VLAN Group', widget=forms.SelectMultiple(attrs={'size': 8})) tenant = forms.MultipleChoiceField(required=False, choices=vlan_tenant_choices, - widget=forms.SelectMultiple(attrs={'size': 8})) + widget=forms.SelectMultiple(attrs={'size': 8})) status = forms.MultipleChoiceField(required=False, choices=vlan_status_choices) role = forms.MultipleChoiceField(required=False, choices=vlan_role_choices, widget=forms.SelectMultiple(attrs={'size': 8})) From 618566abe8bfce6c5670070cc0c8458c9f98efd5 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 27 Jul 2016 14:13:07 -0400 Subject: [PATCH 18/43] Added VRF stats to home page --- netbox/netbox/views.py | 3 ++- netbox/templates/home.html | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/netbox/netbox/views.py b/netbox/netbox/views.py index 5abde702e..801a0a8b9 100644 --- a/netbox/netbox/views.py +++ b/netbox/netbox/views.py @@ -5,7 +5,7 @@ from django.shortcuts import render from circuits.models import Provider, Circuit from dcim.models import Site, Rack, Device, ConsolePort, PowerPort, InterfaceConnection 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 tenancy.models import Tenant @@ -26,6 +26,7 @@ def home(request): 'power_connections_count': PowerPort.objects.filter(power_outlet__isnull=False).count(), # IPAM + 'vrf_count': VRF.objects.count(), 'aggregate_count': Aggregate.objects.count(), 'prefix_count': Prefix.objects.count(), 'ipaddress_count': IPAddress.objects.count(), diff --git a/netbox/templates/home.html b/netbox/templates/home.html index b9362b2c4..9c0b69cef 100644 --- a/netbox/templates/home.html +++ b/netbox/templates/home.html @@ -98,6 +98,11 @@ IPAM
+
+ {{ stats.vrf_count }} +

VRFs

+

Virtual routing and forwarding tables

+
{{ stats.aggregate_count }}

Aggregates

From 483ad256a8c12cd3fa391677c78b34dac0ae2759 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 27 Jul 2016 14:40:19 -0400 Subject: [PATCH 19/43] Miscellaneous API query optimizations --- netbox/dcim/api/views.py | 15 ++++++++------- netbox/ipam/api/views.py | 8 ++++---- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index 3775eda52..e14eb8f87 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -47,7 +47,7 @@ class RackGroupListView(generics.ListAPIView): """ List all rack groups """ - queryset = RackGroup.objects.all() + queryset = RackGroup.objects.select_related('site') serializer_class = serializers.RackGroupSerializer filter_class = filters.RackGroupFilter @@ -56,7 +56,7 @@ class RackGroupDetailView(generics.RetrieveAPIView): """ Retrieve a single rack group """ - queryset = RackGroup.objects.all() + queryset = RackGroup.objects.select_related('site') serializer_class = serializers.RackGroupSerializer @@ -68,7 +68,7 @@ class RackListView(generics.ListAPIView): """ List racks (filterable) """ - queryset = Rack.objects.select_related('site', 'tenant') + queryset = Rack.objects.select_related('site', 'group', 'tenant') serializer_class = serializers.RackSerializer filter_class = filters.RackFilter @@ -77,7 +77,7 @@ class RackDetailView(generics.RetrieveAPIView): """ Retrieve a single rack """ - queryset = Rack.objects.select_related('site', 'tenant') + queryset = Rack.objects.select_related('site', 'group', 'tenant') serializer_class = serializers.RackDetailSerializer @@ -194,8 +194,8 @@ class DeviceListView(generics.ListAPIView): List devices (filterable) """ queryset = Device.objects.select_related('device_type__manufacturer', 'device_role', 'tenant', 'platform', - 'rack__site').prefetch_related('primary_ip4__nat_outside', - 'primary_ip6__nat_outside') + 'rack__site', 'parent_bay').prefetch_related('primary_ip4__nat_outside', + 'primary_ip6__nat_outside') serializer_class = serializers.DeviceSerializer filter_class = filters.DeviceFilter renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES + [BINDZoneRenderer, FlatJSONRenderer] @@ -205,7 +205,8 @@ class DeviceDetailView(generics.RetrieveAPIView): """ 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 diff --git a/netbox/ipam/api/views.py b/netbox/ipam/api/views.py index baa1050cd..36bf1158d 100644 --- a/netbox/ipam/api/views.py +++ b/netbox/ipam/api/views.py @@ -140,7 +140,7 @@ class VLANGroupListView(generics.ListAPIView): """ List all VLAN groups """ - queryset = VLANGroup.objects.all() + queryset = VLANGroup.objects.select_related('site') serializer_class = serializers.VLANGroupSerializer filter_class = filters.VLANGroupFilter @@ -149,7 +149,7 @@ class VLANGroupDetailView(generics.RetrieveAPIView): """ Retrieve a single VLAN group """ - queryset = VLANGroup.objects.all() + queryset = VLANGroup.objects.select_related('site') serializer_class = serializers.VLANGroupSerializer @@ -161,7 +161,7 @@ class VLANListView(generics.ListAPIView): """ List VLANs (filterable) """ - queryset = VLAN.objects.select_related('site', 'tenant', 'role') + queryset = VLAN.objects.select_related('site', 'group', 'tenant', 'role') serializer_class = serializers.VLANSerializer filter_class = filters.VLANFilter @@ -170,5 +170,5 @@ class VLANDetailView(generics.RetrieveAPIView): """ Retrieve a single VLAN """ - queryset = VLAN.objects.select_related('site', 'tenant', 'role') + queryset = VLAN.objects.select_related('site', 'group', 'tenant', 'role') serializer_class = serializers.VLANSerializer From 76efea87ff19444d96cf873d4cdfe807db985404 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 27 Jul 2016 16:29:30 -0400 Subject: [PATCH 20/43] Closes #394: Added global option to VRF selection widget during bulk editing --- netbox/ipam/forms.py | 38 ++++++++++++++++++++------------------ netbox/ipam/views.py | 12 ++++++------ 2 files changed, 26 insertions(+), 24 deletions(-) diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index 1fe790292..7905e3721 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -239,23 +239,24 @@ class PrefixImportForm(BulkImportForm, BootstrapMixin): csv = CSVDataField(csv_form=PrefixFromCSVForm) +def prefix_vrf_choices(): + choices = [ + (None, '---------'), + (0, 'Global'), + ] + choices += [(v.pk, v.name) for v in VRF.objects.all()] + return choices + + class PrefixBulkEditForm(forms.Form, BootstrapMixin): pk = forms.ModelMultipleChoiceField(queryset=Prefix.objects.all(), widget=forms.MultipleHiddenInput) 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=prefix_vrf_choices, coerce=int, required=False, label='VRF') status = forms.ChoiceField(choices=FORM_PREFIX_STATUS_CHOICES, required=False) role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False) description = forms.CharField(max_length=100, required=False) -def prefix_vrf_choices(): - vrf_choices = [('', 'All'), (0, 'Global')] - vrf_choices += [(v.pk, v.name) for v in VRF.objects.all()] - return vrf_choices - - def prefix_site_choices(): site_choices = Site.objects.annotate(prefix_count=Count('prefixes')) return [(s.slug, u'{} ({})'.format(s.name, s.prefix_count)) for s in site_choices] @@ -402,11 +403,18 @@ class IPAddressImportForm(BulkImportForm, BootstrapMixin): csv = CSVDataField(csv_form=IPAddressFromCSVForm) +def ipaddress_vrf_choices(): + choices = [ + (None, '---------'), + (0, 'Global'), + ] + choices += [(v.pk, v.name) for v in VRF.objects.all()] + return choices + + class IPAddressBulkEditForm(forms.Form, BootstrapMixin): 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=ipaddress_vrf_choices, coerce=int, required=False, label='VRF') description = forms.CharField(max_length=100, required=False) @@ -414,12 +422,6 @@ def ipaddress_family_choices(): return [('', 'All'), (4, 'IPv4'), (6, 'IPv6')] -def ipaddress_vrf_choices(): - vrf_choices = [('', 'All'), (0, 'Global')] - vrf_choices += [(v.pk, v.name) for v in VRF.objects.all()] - return vrf_choices - - class IPAddressFilterForm(forms.Form, BootstrapMixin): family = forms.ChoiceField(required=False, choices=ipaddress_family_choices, label='Address Family') vrf = forms.ChoiceField(required=False, choices=ipaddress_vrf_choices, label='VRF') diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index 90afcfdd2..bd9ff4ba1 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -337,10 +337,10 @@ class PrefixBulkEditView(PermissionRequiredMixin, BulkEditView): def update_objects(self, pk_list, form): fields_to_update = {} - if form.cleaned_data['vrf']: - fields_to_update['vrf'] = form.cleaned_data['vrf'] - elif form.cleaned_data['vrf_global']: + if form.cleaned_data['vrf'] == 0: fields_to_update['vrf'] = None + elif form.cleaned_data['vrf']: + fields_to_update['vrf'] = form.cleaned_data['vrf'] for field in ['site', 'status', 'role', 'description']: if form.cleaned_data[field]: fields_to_update[field] = form.cleaned_data[field] @@ -461,10 +461,10 @@ class IPAddressBulkEditView(PermissionRequiredMixin, BulkEditView): def update_objects(self, pk_list, form): fields_to_update = {} - if form.cleaned_data['vrf']: - fields_to_update['vrf'] = form.cleaned_data['vrf'] - elif form.cleaned_data['vrf_global']: + if form.cleaned_data['vrf'] == 0: fields_to_update['vrf'] = None + elif form.cleaned_data['vrf']: + fields_to_update['vrf'] = form.cleaned_data['vrf'] for field in ['description']: if form.cleaned_data[field]: fields_to_update[field] = form.cleaned_data[field] From 1f9e4dc7071d3080367776ff99dd8f996103f6e2 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 27 Jul 2016 16:38:21 -0400 Subject: [PATCH 21/43] Fixed platform selection during bulk editing of devices --- netbox/dcim/forms.py | 13 +++++++++++-- netbox/dcim/views.py | 6 +++--- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 9658de907..aa7cbf820 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -533,13 +533,22 @@ class ChildDeviceImportForm(BulkImportForm, BootstrapMixin): csv = CSVDataField(csv_form=ChildDeviceFromCSVForm) +def device_edit_platform_choices(): + choices = [ + (None, '---------'), + (0, 'None'), + ] + choices += [(p.pk, p.name) for p in Platform.objects.all()] + return choices + + class DeviceBulkEditForm(forms.Form, BootstrapMixin): pk = forms.ModelMultipleChoiceField(queryset=Device.objects.all(), widget=forms.MultipleHiddenInput) device_type = forms.ModelChoiceField(queryset=DeviceType.objects.all(), required=False, label='Type') device_role = forms.ModelChoiceField(queryset=DeviceRole.objects.all(), required=False, label='Role') tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False, label='Tenant') - platform = forms.ModelChoiceField(queryset=Platform.objects.all(), required=False, label='Platform') - platform_delete = forms.BooleanField(required=False, label='Set platform to "none"') + platform = forms.TypedChoiceField(choices=device_edit_platform_choices, coerce=int, required=False, + label='Platform') status = forms.ChoiceField(choices=FORM_STATUS_CHOICES, required=False, initial='', label='Status') serial = forms.CharField(max_length=50, required=False, label='Serial Number') diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 00c462d9e..05a3f6804 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -626,10 +626,10 @@ class DeviceBulkEditView(PermissionRequiredMixin, BulkEditView): def update_objects(self, pk_list, form): fields_to_update = {} - if form.cleaned_data['platform']: - fields_to_update['platform'] = form.cleaned_data['platform'] - elif form.cleaned_data['platform_delete']: + if form.cleaned_data['platform'] == 0: fields_to_update['platform'] = None + elif form.cleaned_data['platform']: + fields_to_update['platform'] = form.cleaned_data['platform'] if form.cleaned_data['status']: status = form.cleaned_data['status'] fields_to_update['status'] = True if status == 'True' else False From 3bb10bca1b5b0d0ada32b32cac4aae6f3a3cd0c5 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 27 Jul 2016 16:52:20 -0400 Subject: [PATCH 22/43] Linkified VRF column in prefix and IP address tables --- netbox/ipam/tables.py | 4 ++-- netbox/ipam/views.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/netbox/ipam/tables.py b/netbox/ipam/tables.py index 796854105..bec2f0f49 100644 --- a/netbox/ipam/tables.py +++ b/netbox/ipam/tables.py @@ -125,7 +125,7 @@ class PrefixTable(BaseTable): pk = ToggleColumn() status = tables.TemplateColumn(STATUS_LABEL, verbose_name='Status') 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') site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site') role = tables.Column(verbose_name='Role') description = tables.Column(orderable=False, verbose_name='Description') @@ -153,7 +153,7 @@ class PrefixBriefTable(BaseTable): class IPAddressTable(BaseTable): pk = ToggleColumn() 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') device = tables.LinkColumn('dcim:device', args=[Accessor('interface.device.pk')], orderable=False, verbose_name='Device') interface = tables.Column(orderable=False, verbose_name='Interface') diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index bd9ff4ba1..fa5c887bc 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -249,7 +249,7 @@ class RoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): # class PrefixListView(ObjectListView): - queryset = Prefix.objects.select_related('site', 'role') + queryset = Prefix.objects.select_related('site', 'vrf', 'role') filter = filters.PrefixFilter filter_form = forms.PrefixFilterForm table = tables.PrefixTable @@ -379,7 +379,7 @@ def prefix_ipaddresses(request, pk): # class IPAddressListView(ObjectListView): - queryset = IPAddress.objects.select_related('vrf', 'interface__device', 'primary_ip4_for', 'primary_ip6_for') + queryset = IPAddress.objects.select_related('vrf', 'interface__device') filter = filters.IPAddressFilter filter_form = forms.IPAddressFilterForm table = tables.IPAddressTable From aee9314bbf38d734aea242da8bf05afa8b302c35 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 28 Jul 2016 09:47:16 -0400 Subject: [PATCH 23/43] Added tenancy page --- mkdocs.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/mkdocs.yml b/mkdocs.yml index d0bcb1ba3..9a96fe0f7 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -17,6 +17,7 @@ pages: - 'DCIM': 'data-model/dcim.md' - 'IPAM': 'data-model/ipam.md' - 'Secrets': 'data-model/secrets.md' + - 'Tenancy': 'data-model/tenancy.md' - 'Extras': 'data-model/extras.md' - 'API Integration': 'api-integration.md' From aa6c840c457f62ff1761cee2dd1610cd98bd999d Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 28 Jul 2016 10:06:25 -0400 Subject: [PATCH 24/43] Fixes #392: Don't include child devices in non-racked devices table --- netbox/dcim/views.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 05a3f6804..2534f25ff 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -142,7 +142,8 @@ class RackGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): # 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_form = forms.RackFilterForm table = tables.RackTable @@ -154,7 +155,7 @@ def rack(request, 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') 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() From a25534f3de4121046f588009abbd6b2e731f3e96 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 28 Jul 2016 11:24:25 -0400 Subject: [PATCH 25/43] Fixes #397: Only include child IPs which belong to the same VRF as the parent prefix --- netbox/ipam/views.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index fa5c887bc..30ce9acdd 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -272,7 +272,8 @@ def prefix(request, pk): aggregate = None # 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 = Prefix.objects.filter(vrf=prefix.vrf, prefix__net_contains=str(prefix.prefix))\ @@ -359,7 +360,7 @@ def prefix_ipaddresses(request, pk): prefix = get_object_or_404(Prefix.objects.all(), pk=pk) # 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') ip_table = tables.IPAddressTable(ipaddresses) From e6c06b39e8547f77085dda3da7bd9495475ada0f Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 28 Jul 2016 13:50:46 -0400 Subject: [PATCH 26/43] Adds tenant assignment to Prefix and IPAddress objects --- netbox/ipam/admin.py | 4 +- netbox/ipam/api/serializers.py | 19 +++- netbox/ipam/api/views.py | 8 +- netbox/ipam/filters.py | 54 ++++++++++++ netbox/ipam/forms.py | 86 ++++++++++++------- .../0007_prefix_ipaddress_add_tenant.py | 27 ++++++ netbox/ipam/models.py | 2 + netbox/ipam/tables.py | 16 +++- netbox/ipam/views.py | 4 +- netbox/templates/ipam/ipaddress.html | 13 +++ .../templates/ipam/ipaddress_bulk_edit.html | 3 +- netbox/templates/ipam/ipaddress_edit.html | 1 + netbox/templates/ipam/ipaddress_import.html | 7 +- netbox/templates/ipam/prefix.html | 13 +++ netbox/templates/ipam/prefix_bulk_edit.html | 1 + netbox/templates/ipam/prefix_import.html | 7 +- netbox/templates/tenancy/tenant.html | 24 ++++-- netbox/tenancy/views.py | 2 + 18 files changed, 235 insertions(+), 56 deletions(-) create mode 100644 netbox/ipam/migrations/0007_prefix_ipaddress_add_tenant.py diff --git a/netbox/ipam/admin.py b/netbox/ipam/admin.py index 5ab79b92d..56113a44e 100644 --- a/netbox/ipam/admin.py +++ b/netbox/ipam/admin.py @@ -40,7 +40,7 @@ class AggregateAdmin(admin.ModelAdmin): @admin.register(Prefix) 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'] search_fields = ['prefix'] @@ -51,7 +51,7 @@ class PrefixAdmin(admin.ModelAdmin): @admin.register(IPAddress) class IPAddressAdmin(admin.ModelAdmin): - list_display = ['address', 'vrf', 'nat_inside'] + list_display = ['address', 'vrf', 'tenant', 'nat_inside'] list_filter = ['family'] fields = ['address', 'vrf', 'device', 'interface', 'nat_inside'] readonly_fields = ['interface', 'device', 'nat_inside'] diff --git a/netbox/ipam/api/serializers.py b/netbox/ipam/api/serializers.py index bdcab381c..a24e1454c 100644 --- a/netbox/ipam/api/serializers.py +++ b/netbox/ipam/api/serializers.py @@ -23,6 +23,15 @@ class VRFNestedSerializer(VRFSerializer): 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 # @@ -120,13 +129,14 @@ class VLANNestedSerializer(VLANSerializer): class PrefixSerializer(serializers.ModelSerializer): site = SiteNestedSerializer() - vrf = VRFNestedSerializer() + vrf = VRFTenantSerializer() + tenant = TenantNestedSerializer() vlan = VLANNestedSerializer() role = RoleNestedSerializer() class Meta: 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): @@ -140,12 +150,13 @@ class PrefixNestedSerializer(PrefixSerializer): # class IPAddressSerializer(serializers.ModelSerializer): - vrf = VRFNestedSerializer() + vrf = VRFTenantSerializer() + tenant = TenantNestedSerializer() interface = InterfaceNestedSerializer() class Meta: 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): diff --git a/netbox/ipam/api/views.py b/netbox/ipam/api/views.py index 36bf1158d..78d2b7f8e 100644 --- a/netbox/ipam/api/views.py +++ b/netbox/ipam/api/views.py @@ -96,7 +96,7 @@ class PrefixListView(generics.ListAPIView): """ 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 filter_class = filters.PrefixFilter @@ -105,7 +105,7 @@ class PrefixDetailView(generics.RetrieveAPIView): """ 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 @@ -117,7 +117,7 @@ class IPAddressListView(generics.ListAPIView): """ 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') serializer_class = serializers.IPAddressSerializer filter_class = filters.IPAddressFilter @@ -127,7 +127,7 @@ class IPAddressDetailView(generics.RetrieveAPIView): """ 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') serializer_class = serializers.IPAddressSerializer diff --git a/netbox/ipam/filters.py b/netbox/ipam/filters.py index badd9a08f..70a382856 100644 --- a/netbox/ipam/filters.py +++ b/netbox/ipam/filters.py @@ -2,6 +2,8 @@ import django_filters from netaddr import IPNetwork from netaddr.core import AddrFormatError +from django.db.models import Q + from dcim.models import Site, Device, Interface from tenancy.models import Tenant @@ -67,6 +69,14 @@ class PrefixFilter(django_filters.FilterSet): action='_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( name='site', queryset=Site.objects.all(), @@ -132,6 +142,24 @@ class PrefixFilter(django_filters.FilterSet): return queryset.filter(vrf__isnull=True) 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): q = django_filters.MethodFilter( @@ -147,6 +175,14 @@ class IPAddressFilter(django_filters.FilterSet): action='_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( name='interface__device', queryset=Device.objects.all(), @@ -187,6 +223,24 @@ class IPAddressFilter(django_filters.FilterSet): return queryset.filter(vrf__isnull=True) 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): site_id = django_filters.ModelMultipleChoiceFilter( diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index 7905e3721..2a2c5413b 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -16,6 +16,24 @@ FORM_PREFIX_STATUS_CHOICES = (('', '---------'),) + PREFIX_STATUS_CHOICES FORM_VLAN_STATUS_CHOICES = (('', '---------'),) + VLAN_STATUS_CHOICES +def bulkedit_vrf_choices(): + choices = [ + (None, '---------'), + (0, 'Global'), + ] + choices += [(v.pk, v.name) for v in VRF.objects.all()] + return choices + + +def bulkedit_tenant_choices(): + choices = [ + (None, '---------'), + (0, 'None'), + ] + choices += [(t.pk, t.name) for t in Tenant.objects.all()] + return choices + + # # VRFs # @@ -48,7 +66,7 @@ class VRFImportForm(BulkImportForm, BootstrapMixin): class VRFBulkEditForm(forms.Form, BootstrapMixin): pk = forms.ModelMultipleChoiceField(queryset=VRF.objects.all(), widget=forms.MultipleHiddenInput) - tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False) + tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant') description = forms.CharField(max_length=100, required=False) @@ -145,7 +163,7 @@ class PrefixForm(forms.ModelForm, BootstrapMixin): class Meta: model = Prefix - fields = ['prefix', 'vrf', 'site', 'vlan', 'status', 'role', 'description'] + fields = ['prefix', 'vrf', 'tenant', 'site', 'vlan', 'status', 'role', 'description'] help_texts = { 'prefix': "IPv4 or IPv6 network", 'vrf': "VRF (if applicable)", @@ -186,6 +204,8 @@ class PrefixForm(forms.ModelForm, BootstrapMixin): class PrefixFromCSVForm(forms.ModelForm): vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, to_field_name='rd', 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', error_messages={'invalid_choice': 'Site not found.'}) vlan_group_name = forms.CharField(required=False) @@ -196,7 +216,8 @@ class PrefixFromCSVForm(forms.ModelForm): class Meta: 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): @@ -239,24 +260,21 @@ class PrefixImportForm(BulkImportForm, BootstrapMixin): csv = CSVDataField(csv_form=PrefixFromCSVForm) -def prefix_vrf_choices(): - choices = [ - (None, '---------'), - (0, 'Global'), - ] - choices += [(v.pk, v.name) for v in VRF.objects.all()] - return choices - - class PrefixBulkEditForm(forms.Form, BootstrapMixin): pk = forms.ModelMultipleChoiceField(queryset=Prefix.objects.all(), widget=forms.MultipleHiddenInput) site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False) - vrf = forms.TypedChoiceField(choices=prefix_vrf_choices, coerce=int, required=False, label='VRF') + 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) role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False) description = forms.CharField(max_length=100, required=False) +def prefix_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(): site_choices = Site.objects.annotate(prefix_count=Count('prefixes')) return [(s.slug, u'{} ({})'.format(s.name, s.prefix_count)) for s in site_choices] @@ -276,12 +294,16 @@ def prefix_role_choices(): class PrefixFilterForm(forms.Form, BootstrapMixin): 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, - widget=forms.SelectMultiple(attrs={'size': 8})) + widget=forms.SelectMultiple(attrs={'size': 6})) 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') @@ -304,7 +326,7 @@ class IPAddressForm(forms.ModelForm, BootstrapMixin): class Meta: model = IPAddress - fields = ['address', 'vrf', 'nat_device', 'nat_inside', 'description'] + fields = ['address', 'vrf', 'tenant', 'nat_device', 'nat_inside', 'description'] help_texts = { 'address': "IPv4 or IPv6 address and mask", 'vrf': "VRF (if applicable)", @@ -353,6 +375,8 @@ class IPAddressForm(forms.ModelForm, BootstrapMixin): class IPAddressFromCSVForm(forms.ModelForm): vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, to_field_name='rd', 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', error_messages={'invalid_choice': 'Device not found.'}) interface_name = forms.CharField(required=False) @@ -360,7 +384,7 @@ class IPAddressFromCSVForm(forms.ModelForm): class Meta: model = IPAddress - fields = ['address', 'vrf', 'device', 'interface_name', 'is_primary', 'description'] + fields = ['address', 'vrf', 'tenant', 'device', 'interface_name', 'is_primary', 'description'] def clean(self): @@ -403,18 +427,10 @@ class IPAddressImportForm(BulkImportForm, BootstrapMixin): csv = CSVDataField(csv_form=IPAddressFromCSVForm) -def ipaddress_vrf_choices(): - choices = [ - (None, '---------'), - (0, 'Global'), - ] - choices += [(v.pk, v.name) for v in VRF.objects.all()] - return choices - - class IPAddressBulkEditForm(forms.Form, BootstrapMixin): pk = forms.ModelMultipleChoiceField(queryset=IPAddress.objects.all(), widget=forms.MultipleHiddenInput) - vrf = forms.TypedChoiceField(choices=ipaddress_vrf_choices, coerce=int, required=False, label='VRF') + 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) @@ -422,9 +438,17 @@ def ipaddress_family_choices(): return [('', 'All'), (4, 'IPv4'), (6, 'IPv6')] +def ipaddress_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): 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})) # @@ -518,7 +542,7 @@ class VLANBulkEditForm(forms.Form, BootstrapMixin): pk = forms.ModelMultipleChoiceField(queryset=VLAN.objects.all(), widget=forms.MultipleHiddenInput) site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False) group = forms.ModelChoiceField(queryset=VLANGroup.objects.all(), required=False) - tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False) + tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant') status = forms.ChoiceField(choices=FORM_VLAN_STATUS_CHOICES, required=False) role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False) description = forms.CharField(max_length=100, required=False) diff --git a/netbox/ipam/migrations/0007_prefix_ipaddress_add_tenant.py b/netbox/ipam/migrations/0007_prefix_ipaddress_add_tenant.py new file mode 100644 index 000000000..eab3b9a47 --- /dev/null +++ b/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'), + ), + ] diff --git a/netbox/ipam/models.py b/netbox/ipam/models.py index d28e4eb12..7c981a8cb 100644 --- a/netbox/ipam/models.py +++ b/netbox/ipam/models.py @@ -233,6 +233,7 @@ class Prefix(CreatedUpdatedModel): 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, 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, verbose_name='VLAN') status = models.PositiveSmallIntegerField('Status', choices=PREFIX_STATUS_CHOICES, default=1) @@ -308,6 +309,7 @@ class IPAddress(CreatedUpdatedModel): address = IPAddressField() vrf = models.ForeignKey('VRF', related_name='ip_addresses', on_delete=models.PROTECT, blank=True, null=True, 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, null=True) nat_inside = models.OneToOneField('self', related_name='nat_outside', on_delete=models.SET_NULL, blank=True, diff --git a/netbox/ipam/tables.py b/netbox/ipam/tables.py index bec2f0f49..30e383666 100644 --- a/netbox/ipam/tables.py +++ b/netbox/ipam/tables.py @@ -49,6 +49,16 @@ VLANGROUP_EDIT_LINK = """ {% endif %} """ +TENANT_LINK = """ +{% if record.tenant %} + {{ record.tenant }} +{% elif record.vrf.tenant %} + {{ record.vrf.tenant }}* +{% else %} + — +{% endif %} +""" + # # VRFs @@ -126,13 +136,14 @@ class PrefixTable(BaseTable): status = tables.TemplateColumn(STATUS_LABEL, verbose_name='Status') prefix = tables.TemplateColumn(PREFIX_LINK, verbose_name='Prefix') 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') role = tables.Column(verbose_name='Role') description = tables.Column(orderable=False, verbose_name='Description') class Meta(BaseTable.Meta): model = Prefix - fields = ('pk', 'prefix', 'status', 'vrf', 'site', 'role', 'description') + fields = ('pk', 'prefix', 'status', 'vrf', 'tenant', 'site', 'role', 'description') class PrefixBriefTable(BaseTable): @@ -154,6 +165,7 @@ class IPAddressTable(BaseTable): pk = ToggleColumn() address = tables.LinkColumn('ipam:ipaddress', args=[Accessor('pk')], verbose_name='IP Address') 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, verbose_name='Device') interface = tables.Column(orderable=False, verbose_name='Interface') @@ -161,7 +173,7 @@ class IPAddressTable(BaseTable): class Meta(BaseTable.Meta): model = IPAddress - fields = ('pk', 'address', 'vrf', 'device', 'interface', 'description') + fields = ('pk', 'address', 'vrf', 'tenant', 'device', 'interface', 'description') class IPAddressBriefTable(BaseTable): diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index 30ce9acdd..7402247b8 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -249,7 +249,7 @@ class RoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): # class PrefixListView(ObjectListView): - queryset = Prefix.objects.select_related('site', 'vrf', 'role') + queryset = Prefix.objects.select_related('site', 'vrf__tenant', 'role') filter = filters.PrefixFilter filter_form = forms.PrefixFilterForm table = tables.PrefixTable @@ -380,7 +380,7 @@ def prefix_ipaddresses(request, pk): # class IPAddressListView(ObjectListView): - queryset = IPAddress.objects.select_related('vrf', 'interface__device') + queryset = IPAddress.objects.select_related('vrf__tenant', 'interface__device') filter = filters.IPAddressFilter filter_form = forms.IPAddressFilterForm table = tables.IPAddressTable diff --git a/netbox/templates/ipam/ipaddress.html b/netbox/templates/ipam/ipaddress.html index 6834ded9e..9112707a7 100644 --- a/netbox/templates/ipam/ipaddress.html +++ b/netbox/templates/ipam/ipaddress.html @@ -64,6 +64,19 @@ {% endif %} + + Tenant + + {% if ipaddress.tenant %} + {{ ipaddress.tenant }} + {% elif ipaddress.vrf.tenant %} + {{ ipaddress.vrf.tenant }} + + {% else %} + None + {% endif %} + + Description diff --git a/netbox/templates/ipam/ipaddress_bulk_edit.html b/netbox/templates/ipam/ipaddress_bulk_edit.html index 8f53df143..bb809920b 100644 --- a/netbox/templates/ipam/ipaddress_bulk_edit.html +++ b/netbox/templates/ipam/ipaddress_bulk_edit.html @@ -7,7 +7,8 @@ {% for ipaddress in selected_objects %} {{ ipaddress }} - {{ ipaddress.vrf }} + {{ ipaddress.vrf|default:"Global" }} + {{ ipaddress.tenant }} {{ ipaddress.interface.device }} {{ ipaddress.interface }} {{ ipaddress.description }} diff --git a/netbox/templates/ipam/ipaddress_edit.html b/netbox/templates/ipam/ipaddress_edit.html index 1be63713b..97991c095 100644 --- a/netbox/templates/ipam/ipaddress_edit.html +++ b/netbox/templates/ipam/ipaddress_edit.html @@ -8,6 +8,7 @@
{% render_field form.address %} {% render_field form.vrf %} + {% render_field form.tenant %} {% if obj %}
diff --git a/netbox/templates/ipam/ipaddress_import.html b/netbox/templates/ipam/ipaddress_import.html index a415cb56a..b819df494 100644 --- a/netbox/templates/ipam/ipaddress_import.html +++ b/netbox/templates/ipam/ipaddress_import.html @@ -38,6 +38,11 @@ VRF route distinguisher (optional) 65000:123 + + Tenant + Name of tenant (optional) + ABC01 + Device Device name (optional) @@ -61,7 +66,7 @@

Example

-
192.0.2.42/24,65000:123,switch12,ge-0/0/31,True,Management IP
+
192.0.2.42/24,65000:123,ABC01,switch12,ge-0/0/31,True,Management IP
{% endblock %} diff --git a/netbox/templates/ipam/prefix.html b/netbox/templates/ipam/prefix.html index 6403717f4..0ba97eeb0 100644 --- a/netbox/templates/ipam/prefix.html +++ b/netbox/templates/ipam/prefix.html @@ -26,6 +26,19 @@ {% endif %} + + Tenant + + {% if prefix.tenant %} + {{ prefix.tenant }} + {% elif prefix.vrf.tenant %} + {{ prefix.vrf.tenant }} + + {% else %} + None + {% endif %} + + Aggregate diff --git a/netbox/templates/ipam/prefix_bulk_edit.html b/netbox/templates/ipam/prefix_bulk_edit.html index 03e358725..6c17d5b71 100644 --- a/netbox/templates/ipam/prefix_bulk_edit.html +++ b/netbox/templates/ipam/prefix_bulk_edit.html @@ -8,6 +8,7 @@ {{ prefix }} {{ prefix.vrf|default:"Global" }} + {{ prefix.tenant }} {{ prefix.site }} {{ prefix.status }} {{ prefix.role }} diff --git a/netbox/templates/ipam/prefix_import.html b/netbox/templates/ipam/prefix_import.html index 902f23443..413732d3f 100644 --- a/netbox/templates/ipam/prefix_import.html +++ b/netbox/templates/ipam/prefix_import.html @@ -38,6 +38,11 @@ VRF route distinguisher (optional) 65000:123 + + Tenant + Name of tenant (optional) + ABC01 + Site Name of assigned site (optional) @@ -71,7 +76,7 @@

Example

-
192.168.42.0/24,65000:123,HQ,Customers,801,Active,Customer,7th floor WiFi
+
192.168.42.0/24,65000:123,ABC01,HQ,Customers,801,Active,Customer,7th floor WiFi
{% endblock %} diff --git a/netbox/templates/tenancy/tenant.html b/netbox/templates/tenancy/tenant.html index c52252f63..a12ed8fa8 100644 --- a/netbox/templates/tenancy/tenant.html +++ b/netbox/templates/tenancy/tenant.html @@ -91,29 +91,37 @@ Stats
-
+ -
+ - -
-
+ -
+
+
+
+

{{ tenant.prefix_count }}

+

Prefixes

+
+
+

{{ tenant.ipaddress_count }}

+

IP addresses

+
+ -
+ diff --git a/netbox/tenancy/views.py b/netbox/tenancy/views.py index 0cf10aa80..bab624589 100644 --- a/netbox/tenancy/views.py +++ b/netbox/tenancy/views.py @@ -55,6 +55,8 @@ def tenant(request, slug): rack_count=Count('racks', distinct=True), device_count=Count('devices', distinct=True), vrf_count=Count('vrfs', distinct=True), + prefix_count=Count('prefixes', distinct=True), + ipaddress_count=Count('ip_addresses', distinct=True), vlan_count=Count('vlans', distinct=True), circuit_count=Count('circuits', distinct=True), ), slug=slug) From b6e5bafd65abcaaf351ef0ecc8b498dfbe0adc4f Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 28 Jul 2016 15:04:27 -0400 Subject: [PATCH 27/43] Replaced edit links with buttons --- netbox/circuits/tables.py | 9 +++++---- netbox/dcim/tables.py | 35 +++++++++++++++++++---------------- netbox/ipam/tables.py | 29 +++++++++++++++++------------ netbox/secrets/tables.py | 9 +++++---- 4 files changed, 46 insertions(+), 36 deletions(-) diff --git a/netbox/circuits/tables.py b/netbox/circuits/tables.py index 0fa236fac..b5ea02977 100644 --- a/netbox/circuits/tables.py +++ b/netbox/circuits/tables.py @@ -6,9 +6,9 @@ from utilities.tables import BaseTable, ToggleColumn from .models import Circuit, CircuitType, Provider -CIRCUITTYPE_EDIT_LINK = """ +CIRCUITTYPE_ACTIONS = """ {% if perms.circuit.change_circuittype %} - Edit + {% endif %} """ @@ -37,11 +37,12 @@ class CircuitTypeTable(BaseTable): name = tables.LinkColumn(verbose_name='Name') circuit_count = tables.Column(verbose_name='Circuits') 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): model = CircuitType - fields = ('pk', 'name', 'circuit_count', 'slug', 'edit') + fields = ('pk', 'name', 'circuit_count', 'slug', 'actions') # diff --git a/netbox/dcim/tables.py b/netbox/dcim/tables.py index 75ad3fb72..334a75cb0 100644 --- a/netbox/dcim/tables.py +++ b/netbox/dcim/tables.py @@ -16,27 +16,27 @@ DEVICE_LINK = """ """ -RACKGROUP_EDIT_LINK = """ +RACKGROUP_ACTIONS = """ {% if perms.dcim.change_rackgroup %} - Edit + {% endif %} """ -DEVICEROLE_EDIT_LINK = """ +DEVICEROLE_ACTIONS = """ {% if perms.dcim.change_devicerole %} - Edit + {% endif %} """ -MANUFACTURER_EDIT_LINK = """ +MANUFACTURER_ACTIONS = """ {% if perms.dcim.change_manufacturer %} - Edit + {% endif %} """ -PLATFORM_EDIT_LINK = """ +PLATFORM_ACTIONS = """ {% if perms.dcim.change_platform %} - Edit + {% endif %} """ @@ -85,11 +85,12 @@ class RackGroupTable(BaseTable): site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site') rack_count = tables.Column(verbose_name='Racks') 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): model = RackGroup - fields = ('pk', 'name', 'site', 'rack_count', 'slug', 'edit') + fields = ('pk', 'name', 'site', 'rack_count', 'slug', 'actions') # @@ -136,11 +137,12 @@ class ManufacturerTable(BaseTable): name = tables.LinkColumn(verbose_name='Name') devicetype_count = tables.Column(verbose_name='Device Types') 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): model = Manufacturer - fields = ('pk', 'name', 'devicetype_count', 'slug', 'edit') + fields = ('pk', 'name', 'devicetype_count', 'slug', 'actions') # @@ -232,11 +234,12 @@ class DeviceRoleTable(BaseTable): device_count = tables.Column(verbose_name='Devices') slug = tables.Column(verbose_name='Slug') 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): model = DeviceRole - fields = ('pk', 'name', 'device_count', 'slug', 'color') + fields = ('pk', 'name', 'device_count', 'slug', 'color', 'actions') # @@ -248,11 +251,11 @@ class PlatformTable(BaseTable): name = tables.LinkColumn(verbose_name='Name') device_count = tables.Column(verbose_name='Devices') 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): model = Platform - fields = ('pk', 'name', 'device_count', 'slug', 'edit') + fields = ('pk', 'name', 'device_count', 'slug', 'actions') # diff --git a/netbox/ipam/tables.py b/netbox/ipam/tables.py index 30e383666..19c139e9d 100644 --- a/netbox/ipam/tables.py +++ b/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 -RIR_EDIT_LINK = """ -{% if perms.ipam.change_rir %}Edit{% endif %} +RIR_ACTIONS = """ +{% if perms.ipam.change_rir %} + +{% endif %} """ UTILIZATION_GRAPH = """ @@ -15,8 +17,10 @@ UTILIZATION_GRAPH = """ {% utilization_graph record.get_utilization %} """ -ROLE_EDIT_LINK = """ -{% if perms.ipam.change_role %}Edit{% endif %} +ROLE_ACTIONS = """ +{% if perms.ipam.change_role %} + +{% endif %} """ PREFIX_LINK = """ @@ -43,9 +47,9 @@ STATUS_LABEL = """ {% endif %} """ -VLANGROUP_EDIT_LINK = """ +VLANGROUP_ACTIONS = """ {% if perms.ipam.change_vlangroup %} - Edit + {% endif %} """ @@ -85,11 +89,11 @@ class RIRTable(BaseTable): name = tables.LinkColumn(verbose_name='Name') aggregate_count = tables.Column(verbose_name='Aggregates') 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): model = RIR - fields = ('pk', 'name', 'aggregate_count', 'slug', 'edit') + fields = ('pk', 'name', 'aggregate_count', 'slug', 'actions') # @@ -120,11 +124,11 @@ class RoleTable(BaseTable): 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') 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): model = Role - fields = ('pk', 'name', 'prefix_count', 'vlan_count', 'slug', 'edit') + fields = ('pk', 'name', 'prefix_count', 'vlan_count', 'slug', 'actions') # @@ -199,11 +203,12 @@ class VLANGroupTable(BaseTable): site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site') vlan_count = tables.Column(verbose_name='VLANs') 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): model = VLANGroup - fields = ('pk', 'name', 'site', 'vlan_count', 'slug', 'edit') + fields = ('pk', 'name', 'site', 'vlan_count', 'slug', 'actions') # diff --git a/netbox/secrets/tables.py b/netbox/secrets/tables.py index 60db5e708..2fb6d9bbe 100644 --- a/netbox/secrets/tables.py +++ b/netbox/secrets/tables.py @@ -6,9 +6,9 @@ from utilities.tables import BaseTable, ToggleColumn from .models import SecretRole, Secret -SECRETROLE_EDIT_LINK = """ +SECRETROLE_ACTIONS = """ {% if perms.secrets.change_secretrole %} - Edit + {% endif %} """ @@ -22,11 +22,12 @@ class SecretRoleTable(BaseTable): name = tables.LinkColumn(verbose_name='Name') secret_count = tables.Column(verbose_name='Secrets') 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): model = SecretRole - fields = ('pk', 'name', 'secret_count', 'slug', 'edit') + fields = ('pk', 'name', 'secret_count', 'slug', 'actions') # From 6b41794e12f195b667fbab56db197b465894bfda Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 28 Jul 2016 15:30:29 -0400 Subject: [PATCH 28/43] Implemented bulk editing for sites --- netbox/dcim/forms.py | 5 +++++ netbox/dcim/tables.py | 5 +++-- netbox/dcim/urls.py | 1 + netbox/dcim/views.py | 18 ++++++++++++++++++ netbox/templates/dcim/site_bulk_edit.html | 13 +++++++++++++ netbox/templates/dcim/site_list.html | 7 +++++-- 6 files changed, 45 insertions(+), 4 deletions(-) create mode 100644 netbox/templates/dcim/site_bulk_edit.html diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index aa7cbf820..244952885 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -76,6 +76,11 @@ class SiteImportForm(BulkImportForm, BootstrapMixin): csv = CSVDataField(csv_form=SiteFromCSVForm) +class SiteBulkEditForm(forms.Form, BootstrapMixin): + pk = forms.ModelMultipleChoiceField(queryset=Site.objects.all(), widget=forms.MultipleHiddenInput) + tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False) + + 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] diff --git a/netbox/dcim/tables.py b/netbox/dcim/tables.py index 334a75cb0..f266b3d45 100644 --- a/netbox/dcim/tables.py +++ b/netbox/dcim/tables.py @@ -59,6 +59,7 @@ UTILIZATION_GRAPH = """ # class SiteTable(BaseTable): + pk = ToggleColumn() name = tables.LinkColumn('dcim:site', args=[Accessor('slug')], verbose_name='Name') facility = tables.Column(verbose_name='Facility') tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant') @@ -71,8 +72,8 @@ class SiteTable(BaseTable): class Meta(BaseTable.Meta): model = Site - fields = ('name', 'facility', 'tenant', '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') # diff --git a/netbox/dcim/urls.py b/netbox/dcim/urls.py index ea9d96211..12aa60d4f 100644 --- a/netbox/dcim/urls.py +++ b/netbox/dcim/urls.py @@ -15,6 +15,7 @@ urlpatterns = [ url(r'^sites/$', views.SiteListView.as_view(), name='site_list'), 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/edit/$', views.SiteBulkEditView.as_view(), name='site_bulk_edit'), url(r'^sites/(?P[\w-]+)/$', views.site, name='site'), url(r'^sites/(?P[\w-]+)/edit/$', views.SiteEditView.as_view(), name='site_edit'), url(r'^sites/(?P[\w-]+)/delete/$', views.SiteDeleteView.as_view(), name='site_delete'), diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 2534f25ff..3f22bca85 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -65,6 +65,7 @@ class SiteListView(ObjectListView): filter = filters.SiteFilter filter_form = forms.SiteFilterForm table = tables.SiteTable + edit_permissions = ['dcim.change_rack', 'dcim.delete_rack'] template_name = 'dcim/site_list.html' @@ -111,6 +112,23 @@ class SiteBulkImportView(PermissionRequiredMixin, BulkImportView): 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 = {} + for field in ['tenant']: + if form.cleaned_data[field]: + fields_to_update[field] = form.cleaned_data[field] + + return self.cls.objects.filter(pk__in=pk_list).update(**fields_to_update) + + # # Rack groups # diff --git a/netbox/templates/dcim/site_bulk_edit.html b/netbox/templates/dcim/site_bulk_edit.html new file mode 100644 index 000000000..c5b0e4aa0 --- /dev/null +++ b/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 %} + + {{ site.slug }} + {{ site.tenant }} + + {% endfor %} +{% endblock %} diff --git a/netbox/templates/dcim/site_list.html b/netbox/templates/dcim/site_list.html index 0c04faa27..af20c947d 100644 --- a/netbox/templates/dcim/site_list.html +++ b/netbox/templates/dcim/site_list.html @@ -1,5 +1,4 @@ {% extends '_base.html' %} -{% load render_table from django_tables2 %} {% block title %}Sites{% endblock %} @@ -10,13 +9,17 @@ Add a site + + + Import sites + {% endif %} {% include 'inc/export_button.html' with obj_type='sites' %}

Sites

- {% render_table table 'table.html' %} + {% include 'utilities/obj_table.html' with bulk_edit_url='dcim:site_bulk_edit' %}
From 397943b2225a5c002ccff4201ad09bc943ef7e90 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 28 Jul 2016 15:59:49 -0400 Subject: [PATCH 29/43] Allow unassigning VRF and tenants when editing objects in bulk --- netbox/circuits/forms.py | 3 ++- netbox/circuits/views.py | 6 +++++- netbox/dcim/forms.py | 27 ++++++++++++++------------- netbox/dcim/views.py | 20 +++++++++++++------- netbox/ipam/forms.py | 13 ++++--------- netbox/ipam/views.py | 30 ++++++++++++++++++++---------- netbox/tenancy/forms.py | 12 ++++++++++++ 7 files changed, 70 insertions(+), 41 deletions(-) diff --git a/netbox/circuits/forms.py b/netbox/circuits/forms.py index d5535ef53..b5152e08c 100644 --- a/netbox/circuits/forms.py +++ b/netbox/circuits/forms.py @@ -2,6 +2,7 @@ from django import forms from django.db.models import Count 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 ( APISelect, BootstrapMixin, BulkImportForm, CommentField, CSVDataField, Livesearch, SmallTextarea, SlugField, @@ -180,7 +181,7 @@ class CircuitBulkEditForm(forms.Form, BootstrapMixin): pk = forms.ModelMultipleChoiceField(queryset=Circuit.objects.all(), widget=forms.MultipleHiddenInput) type = forms.ModelChoiceField(queryset=CircuitType.objects.all(), required=False) provider = forms.ModelChoiceField(queryset=Provider.objects.all(), required=False) - tenant = forms.ModelChoiceField(queryset=Tenant.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)') commit_rate = forms.IntegerField(required=False, label='Commit rate (Kbps)') comments = CommentField() diff --git a/netbox/circuits/views.py b/netbox/circuits/views.py index 3076a9141..381670d89 100644 --- a/netbox/circuits/views.py +++ b/netbox/circuits/views.py @@ -159,7 +159,11 @@ class CircuitBulkEditView(PermissionRequiredMixin, BulkEditView): def update_objects(self, pk_list, form): fields_to_update = {} - for field in ['type', 'provider', 'tenant', 'port_speed', 'commit_rate', '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 ['type', 'provider', 'port_speed', 'commit_rate', 'comments']: if form.cleaned_data[field]: fields_to_update[field] = form.cleaned_data[field] diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 244952885..fef87ded0 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -4,6 +4,7 @@ from django import forms from django.db.models import Count, Q from ipam.models import IPAddress +from tenancy.forms import bulkedit_tenant_choices from tenancy.models import Tenant from utilities.forms import ( APISelect, BootstrapMixin, BulkImportForm, CommentField, CSVDataField, ExpandableNameField, @@ -39,6 +40,15 @@ def get_device_by_name_or_pk(name): return device +def bulkedit_platform_choices(): + choices = [ + (None, '---------'), + (0, 'None'), + ] + choices += [(p.pk, p.name) for p in Platform.objects.all()] + return choices + + # # Sites # @@ -78,7 +88,7 @@ class SiteImportForm(BulkImportForm, BootstrapMixin): class SiteBulkEditForm(forms.Form, BootstrapMixin): pk = forms.ModelMultipleChoiceField(queryset=Site.objects.all(), widget=forms.MultipleHiddenInput) - tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False) + tenant = forms.TypedChoiceField(choices=bulkedit_tenant_choices, coerce=int, required=False, label='Tenant') def site_tenant_choices(): @@ -181,7 +191,7 @@ class RackBulkEditForm(forms.Form, BootstrapMixin): pk = forms.ModelMultipleChoiceField(queryset=Rack.objects.all(), widget=forms.MultipleHiddenInput) site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False) group = forms.ModelChoiceField(queryset=RackGroup.objects.all(), required=False) - tenant = forms.ModelChoiceField(queryset=Tenant.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)') comments = CommentField() @@ -538,21 +548,12 @@ class ChildDeviceImportForm(BulkImportForm, BootstrapMixin): csv = CSVDataField(csv_form=ChildDeviceFromCSVForm) -def device_edit_platform_choices(): - choices = [ - (None, '---------'), - (0, 'None'), - ] - choices += [(p.pk, p.name) for p in Platform.objects.all()] - return choices - - class DeviceBulkEditForm(forms.Form, BootstrapMixin): pk = forms.ModelMultipleChoiceField(queryset=Device.objects.all(), widget=forms.MultipleHiddenInput) device_type = forms.ModelChoiceField(queryset=DeviceType.objects.all(), required=False, label='Type') device_role = forms.ModelChoiceField(queryset=DeviceRole.objects.all(), required=False, label='Role') - tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False, label='Tenant') - platform = forms.TypedChoiceField(choices=device_edit_platform_choices, coerce=int, required=False, + 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') serial = forms.CharField(max_length=50, required=False, label='Serial Number') diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 3f22bca85..f0b3ce55a 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -122,9 +122,10 @@ class SiteBulkEditView(PermissionRequiredMixin, BulkEditView): def update_objects(self, pk_list, form): fields_to_update = {} - for field in ['tenant']: - if form.cleaned_data[field]: - fields_to_update[field] = form.cleaned_data[field] + 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) @@ -220,6 +221,10 @@ class RackBulkEditView(PermissionRequiredMixin, BulkEditView): 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'] for field in ['site', 'group', 'tenant', 'u_height', 'comments']: if form.cleaned_data[field]: fields_to_update[field] = form.cleaned_data[field] @@ -645,10 +650,11 @@ class DeviceBulkEditView(PermissionRequiredMixin, BulkEditView): def update_objects(self, pk_list, form): fields_to_update = {} - if form.cleaned_data['platform'] == 0: - fields_to_update['platform'] = None - elif form.cleaned_data['platform']: - fields_to_update['platform'] = form.cleaned_data['platform'] + 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']: status = form.cleaned_data['status'] fields_to_update['status'] = True if status == 'True' else False diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index 2a2c5413b..0449efdb2 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -4,6 +4,7 @@ from django import forms from django.db.models import Count from dcim.models import Site, Device, Interface +from tenancy.forms import bulkedit_tenant_choices from tenancy.models import Tenant from utilities.forms import BootstrapMixin, APISelect, Livesearch, CSVDataField, BulkImportForm, SlugField @@ -17,6 +18,9 @@ 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'), @@ -25,15 +29,6 @@ def bulkedit_vrf_choices(): return choices -def bulkedit_tenant_choices(): - choices = [ - (None, '---------'), - (0, 'None'), - ] - choices += [(t.pk, t.name) for t in Tenant.objects.all()] - return choices - - # # VRFs # diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index 7402247b8..b99438a02 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -86,7 +86,11 @@ class VRFBulkEditView(PermissionRequiredMixin, BulkEditView): def update_objects(self, pk_list, form): fields_to_update = {} - for field in ['tenant', 'description']: + 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']: if form.cleaned_data[field]: fields_to_update[field] = form.cleaned_data[field] @@ -338,10 +342,11 @@ class PrefixBulkEditView(PermissionRequiredMixin, BulkEditView): def update_objects(self, pk_list, form): fields_to_update = {} - if form.cleaned_data['vrf'] == 0: - fields_to_update['vrf'] = None - elif form.cleaned_data['vrf']: - fields_to_update['vrf'] = form.cleaned_data['vrf'] + 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']: if form.cleaned_data[field]: fields_to_update[field] = form.cleaned_data[field] @@ -462,10 +467,11 @@ class IPAddressBulkEditView(PermissionRequiredMixin, BulkEditView): def update_objects(self, pk_list, form): fields_to_update = {} - if form.cleaned_data['vrf'] == 0: - fields_to_update['vrf'] = None - elif form.cleaned_data['vrf']: - fields_to_update['vrf'] = form.cleaned_data['vrf'] + 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']: if form.cleaned_data[field]: fields_to_update[field] = form.cleaned_data[field] @@ -560,7 +566,11 @@ class VLANBulkEditView(PermissionRequiredMixin, BulkEditView): def update_objects(self, pk_list, form): fields_to_update = {} - for field in ['site', 'group', 'tenant', 'status', 'role', 'description']: + 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']: if form.cleaned_data[field]: fields_to_update[field] = form.cleaned_data[field] diff --git a/netbox/tenancy/forms.py b/netbox/tenancy/forms.py index 14ffe7ef4..bf04454ba 100644 --- a/netbox/tenancy/forms.py +++ b/netbox/tenancy/forms.py @@ -8,6 +8,18 @@ from utilities.forms import ( from .models import Tenant, TenantGroup +def bulkedit_tenant_choices(): + """ + Include an option to remove the currently assigned Tenant from an object. + """ + choices = [ + (None, '---------'), + (0, 'None'), + ] + choices += [(t.pk, t.name) for t in Tenant.objects.all()] + return choices + + # # Tenant groups # From d933d034e05aca0d834eec91f68e193d48195e87 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 28 Jul 2016 16:03:59 -0400 Subject: [PATCH 30/43] Changed TenantGroup edit link to a button --- netbox/tenancy/tables.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/netbox/tenancy/tables.py b/netbox/tenancy/tables.py index d6e271747..0b77a83ed 100644 --- a/netbox/tenancy/tables.py +++ b/netbox/tenancy/tables.py @@ -6,9 +6,9 @@ from utilities.tables import BaseTable, ToggleColumn from .models import Tenant, TenantGroup -TENANTGROUP_EDIT_LINK = """ +TENANTGROUP_ACTIONS = """ {% if perms.tenancy.change_tenantgroup %} - Edit + {% endif %} """ @@ -22,11 +22,11 @@ class TenantGroupTable(BaseTable): name = tables.LinkColumn(verbose_name='Name') tenant_count = tables.Column(verbose_name='Tenants') slug = tables.Column(verbose_name='Slug') - edit = tables.TemplateColumn(template_code=TENANTGROUP_EDIT_LINK, verbose_name='') + actions = tables.TemplateColumn(template_code=TENANTGROUP_ACTIONS, verbose_name='') class Meta(BaseTable.Meta): model = TenantGroup - fields = ('pk', 'name', 'tenant_count', 'slug', 'edit') + fields = ('pk', 'name', 'tenant_count', 'slug', 'actions') # From fa906c74c0c66baf04e7123e8adfb507bfa85a80 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 29 Jul 2016 10:57:09 -0400 Subject: [PATCH 31/43] Fixed actions column alignment --- netbox/tenancy/tables.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/netbox/tenancy/tables.py b/netbox/tenancy/tables.py index 0b77a83ed..0ce0d577c 100644 --- a/netbox/tenancy/tables.py +++ b/netbox/tenancy/tables.py @@ -22,7 +22,8 @@ class TenantGroupTable(BaseTable): name = tables.LinkColumn(verbose_name='Name') tenant_count = tables.Column(verbose_name='Tenants') slug = tables.Column(verbose_name='Slug') - actions = tables.TemplateColumn(template_code=TENANTGROUP_ACTIONS, verbose_name='') + actions = tables.TemplateColumn(template_code=TENANTGROUP_ACTIONS, attrs={'td': {'class': 'text-right'}}, + verbose_name='') class Meta(BaseTable.Meta): model = TenantGroup From 03d71f976496c9e4d3aeef81416bb6c8e0df9739 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 29 Jul 2016 11:20:47 -0400 Subject: [PATCH 32/43] Standardized breadcrumb hierarchies --- netbox/templates/circuits/circuit.html | 6 +++--- netbox/templates/dcim/rack.html | 4 ++-- netbox/templates/ipam/inc/prefix_header.html | 8 +++----- netbox/templates/ipam/ipaddress.html | 11 +++++------ netbox/templates/ipam/vlan.html | 7 +++++-- netbox/templates/ipam/vrf.html | 20 ++++++++++++++++++++ netbox/templates/secrets/secret.html | 4 ++-- netbox/templates/tenancy/tenant.html | 1 + 8 files changed, 41 insertions(+), 20 deletions(-) diff --git a/netbox/templates/circuits/circuit.html b/netbox/templates/circuits/circuit.html index c6fcbfd87..7c4f6b8fa 100644 --- a/netbox/templates/circuits/circuit.html +++ b/netbox/templates/circuits/circuit.html @@ -7,9 +7,9 @@
diff --git a/netbox/templates/dcim/rack.html b/netbox/templates/dcim/rack.html index 93df009b9..d5928e1c7 100644 --- a/netbox/templates/dcim/rack.html +++ b/netbox/templates/dcim/rack.html @@ -8,8 +8,8 @@
diff --git a/netbox/templates/ipam/inc/prefix_header.html b/netbox/templates/ipam/inc/prefix_header.html index 8bbdfe055..5c3896884 100644 --- a/netbox/templates/ipam/inc/prefix_header.html +++ b/netbox/templates/ipam/inc/prefix_header.html @@ -1,11 +1,9 @@
diff --git a/netbox/templates/ipam/ipaddress.html b/netbox/templates/ipam/ipaddress.html index 9112707a7..9c283e917 100644 --- a/netbox/templates/ipam/ipaddress.html +++ b/netbox/templates/ipam/ipaddress.html @@ -7,12 +7,11 @@
diff --git a/netbox/templates/ipam/vlan.html b/netbox/templates/ipam/vlan.html index cf525ff58..9555a6ea9 100644 --- a/netbox/templates/ipam/vlan.html +++ b/netbox/templates/ipam/vlan.html @@ -7,8 +7,11 @@
diff --git a/netbox/templates/ipam/vrf.html b/netbox/templates/ipam/vrf.html index 948ee1d89..8a8f67c9b 100644 --- a/netbox/templates/ipam/vrf.html +++ b/netbox/templates/ipam/vrf.html @@ -4,6 +4,26 @@ {% block title %}VRF {{ vrf }}{% endblock %} {% block content %} +
+
+ +
+
+
+
+ + + + +
+
+
+
diff --git a/netbox/templates/tenancy/tenant.html b/netbox/templates/tenancy/tenant.html index a12ed8fa8..fdeae4af7 100644 --- a/netbox/templates/tenancy/tenant.html +++ b/netbox/templates/tenancy/tenant.html @@ -7,6 +7,7 @@
From 2daffdf08783225ed77358238c84b108bc95c8ae Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 29 Jul 2016 12:33:40 -0400 Subject: [PATCH 33/43] Added account field to provider table --- netbox/circuits/tables.py | 3 ++- netbox/templates/inc/search_panel.html | 0 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 netbox/templates/inc/search_panel.html diff --git a/netbox/circuits/tables.py b/netbox/circuits/tables.py index b5ea02977..66cbd33ca 100644 --- a/netbox/circuits/tables.py +++ b/netbox/circuits/tables.py @@ -21,11 +21,12 @@ class ProviderTable(BaseTable): pk = ToggleColumn() name = tables.LinkColumn('circuits:provider', args=[Accessor('slug')], verbose_name='Name') asn = tables.Column(verbose_name='ASN') + account = tables.Column(verbose_name='Account') circuit_count = tables.Column(accessor=Accessor('count_circuits'), verbose_name='Circuits') class Meta(BaseTable.Meta): model = Provider - fields = ('pk', 'name', 'asn', 'circuit_count') + fields = ('pk', 'name', 'asn', 'account', 'circuit_count') # diff --git a/netbox/templates/inc/search_panel.html b/netbox/templates/inc/search_panel.html new file mode 100644 index 000000000..e69de29bb From 38aee33df01ccddb35607bebdbfe233f857c78dd Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 29 Jul 2016 12:51:23 -0400 Subject: [PATCH 34/43] Closes #358: Improved search of all objects --- netbox/circuits/filters.py | 12 +++-- netbox/dcim/filters.py | 13 +++-- netbox/ipam/filters.py | 54 ++++++++++++++++--- netbox/secrets/filters.py | 12 +++++ netbox/templates/circuits/circuit.html | 2 +- netbox/templates/circuits/circuit_list.html | 18 +------ netbox/templates/circuits/provider.html | 14 ++++- netbox/templates/circuits/provider_list.html | 18 +------ netbox/templates/dcim/device_list.html | 19 +------ netbox/templates/dcim/inc/_device_header.html | 2 +- netbox/templates/dcim/rack.html | 2 +- netbox/templates/dcim/rack_list.html | 19 +------ netbox/templates/dcim/site.html | 2 +- netbox/templates/dcim/site_list.html | 19 +------ netbox/templates/home.html | 8 +-- netbox/templates/inc/search_panel.html | 18 +++++++ netbox/templates/ipam/aggregate.html | 14 ++++- netbox/templates/ipam/aggregate_list.html | 1 + netbox/templates/ipam/inc/prefix_header.html | 2 +- netbox/templates/ipam/ipaddress.html | 2 +- netbox/templates/ipam/ipaddress_list.html | 19 +------ netbox/templates/ipam/prefix_list.html | 19 +------ netbox/templates/ipam/vlan.html | 2 +- netbox/templates/ipam/vlan_list.html | 19 +------ netbox/templates/ipam/vrf.html | 2 +- netbox/templates/ipam/vrf_list.html | 18 +------ netbox/templates/secrets/secret_list.html | 1 + netbox/templates/tenancy/tenant_list.html | 18 +------ netbox/tenancy/filters.py | 9 +++- 29 files changed, 147 insertions(+), 211 deletions(-) diff --git a/netbox/circuits/filters.py b/netbox/circuits/filters.py index 59ff6ca4c..e3faa6332 100644 --- a/netbox/circuits/filters.py +++ b/netbox/circuits/filters.py @@ -29,10 +29,10 @@ class ProviderFilter(django_filters.FilterSet): fields = ['q', 'name', 'account', 'asn'] def search(self, queryset, value): - value = value.strip() return queryset.filter( Q(name__icontains=value) | - Q(account__icontains=value) + Q(account__icontains=value) | + Q(comments__icontains=value) ) @@ -91,5 +91,9 @@ class CircuitFilter(django_filters.FilterSet): fields = ['q', 'provider_id', 'provider', 'type_id', 'type', 'site_id', 'site', 'interface', 'install_date'] 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) + ) diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index 08f0d671e..1800bf266 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -31,11 +31,10 @@ class SiteFilter(django_filters.FilterSet): fields = ['q', 'name', 'facility', 'asn'] def search(self, queryset, value): - value = value.strip() 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: - qs_filter |= Q(asn=int(value)) + qs_filter |= Q(asn=int(value.strip())) except ValueError: pass return queryset.filter(qs_filter) @@ -103,10 +102,10 @@ class RackFilter(django_filters.FilterSet): fields = ['q', 'site_id', 'site', 'u_height'] def search(self, queryset, value): - value = value.strip() return queryset.filter( Q(name__icontains=value) | - Q(facility_id__icontains=value) + Q(facility_id__icontains=value) | + Q(comments__icontains=value) ) @@ -234,11 +233,11 @@ class DeviceFilter(django_filters.FilterSet): 'is_network_device'] def search(self, queryset, value): - value = value.strip() return queryset.filter( Q(name__icontains=value) | Q(serial__icontains=value) | - Q(modules__serial__icontains=value) + Q(modules__serial__icontains=value) | + Q(comments__icontains=value) ).distinct() diff --git a/netbox/ipam/filters.py b/netbox/ipam/filters.py index 70a382856..de8a240bb 100644 --- a/netbox/ipam/filters.py +++ b/netbox/ipam/filters.py @@ -11,6 +11,10 @@ from .models import RIR, Aggregate, VRF, Prefix, IPAddress, VLAN, VLANGroup, Rol class VRFFilter(django_filters.FilterSet): + q = django_filters.MethodFilter( + action='search', + label='Search', + ) name = django_filters.CharFilter( name='name', lookup_type='icontains', @@ -28,12 +32,23 @@ class VRFFilter(django_filters.FilterSet): 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: model = VRF fields = ['name', 'rd'] class AggregateFilter(django_filters.FilterSet): + q = django_filters.MethodFilter( + action='search', + label='Search', + ) rir_id = django_filters.ModelMultipleChoiceFilter( name='rir', queryset=RIR.objects.all(), @@ -50,6 +65,15 @@ class AggregateFilter(django_filters.FilterSet): model = Aggregate 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): q = django_filters.MethodFilter( @@ -114,12 +138,13 @@ class PrefixFilter(django_filters.FilterSet): fields = ['family', 'site_id', 'site', 'vrf', 'vrf_id', 'vlan_id', 'vlan_vid', 'status', 'role_id', 'role'] def search(self, queryset, value): - value = value.strip() + qs_filter = Q(description__icontains=value) 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: - return queryset.none() + pass + return queryset.filter(qs_filter) def search_by_parent(self, queryset, value): value = value.strip() @@ -205,12 +230,13 @@ class IPAddressFilter(django_filters.FilterSet): fields = ['q', 'family', 'vrf_id', 'vrf', 'device_id', 'device', 'interface_id'] def search(self, queryset, value): - value = value.strip() + qs_filter = Q(description__icontains=value) 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: - return queryset.none() + pass + return queryset.filter(qs_filter) def _vrf(self, queryset, value): if str(value) == '': @@ -261,6 +287,10 @@ class VLANGroupFilter(django_filters.FilterSet): class VLANFilter(django_filters.FilterSet): + q = django_filters.MethodFilter( + action='search', + label='Search', + ) site_id = django_filters.ModelMultipleChoiceFilter( name='site', queryset=Site.objects.all(), @@ -318,3 +348,11 @@ class VLANFilter(django_filters.FilterSet): class Meta: model = VLAN 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) diff --git a/netbox/secrets/filters.py b/netbox/secrets/filters.py index 4606b3db5..a821402cf 100644 --- a/netbox/secrets/filters.py +++ b/netbox/secrets/filters.py @@ -1,10 +1,16 @@ import django_filters +from django.db.models import Q + from .models import Secret, SecretRole from dcim.models import Device class SecretFilter(django_filters.FilterSet): + q = django_filters.MethodFilter( + action='search', + label='Search', + ) role_id = django_filters.ModelMultipleChoiceFilter( name='role', queryset=SecretRole.objects.all(), @@ -26,3 +32,9 @@ class SecretFilter(django_filters.FilterSet): class Meta: model = Secret fields = ['name', 'role_id', 'role', 'device'] + + def search(self, queryset, value): + return queryset.filter( + Q(name__icontains=value) | + Q(device__name__icontains=value) + ) diff --git a/netbox/templates/circuits/circuit.html b/netbox/templates/circuits/circuit.html index 7c4f6b8fa..308e9a5c5 100644 --- a/netbox/templates/circuits/circuit.html +++ b/netbox/templates/circuits/circuit.html @@ -15,7 +15,7 @@
- +
-
-
- Search -
-
- -
- - - - -
- -
-
+ {% include 'inc/search_panel.html' %} {% include 'inc/filter_panel.html' %}
diff --git a/netbox/templates/circuits/provider.html b/netbox/templates/circuits/provider.html index 333a07faf..8ee2e0055 100644 --- a/netbox/templates/circuits/provider.html +++ b/netbox/templates/circuits/provider.html @@ -6,12 +6,24 @@ {% block content %}
-
+
+
+
+
+ + + + +
+
+
-
-
- Search -
-
-
-
- - - - -
-
-
-
+ {% include 'inc/search_panel.html' %} {% include 'inc/filter_panel.html' %}
diff --git a/netbox/templates/dcim/device_list.html b/netbox/templates/dcim/device_list.html index ee127cf48..c358ef90f 100644 --- a/netbox/templates/dcim/device_list.html +++ b/netbox/templates/dcim/device_list.html @@ -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' %}
-
-
- - Search -
-
-
-
- - - - -
-
-
-
+ {% include 'inc/search_panel.html' %} {% include 'inc/filter_panel.html' %}
diff --git a/netbox/templates/dcim/inc/_device_header.html b/netbox/templates/dcim/inc/_device_header.html index a038b96b2..ca3c25f87 100644 --- a/netbox/templates/dcim/inc/_device_header.html +++ b/netbox/templates/dcim/inc/_device_header.html @@ -16,7 +16,7 @@
- +
-
-
- - Search -
-
- -
- - - - -
- -
-
+ {% include 'inc/search_panel.html' %} {% include 'inc/filter_panel.html' %}
diff --git a/netbox/templates/dcim/site.html b/netbox/templates/dcim/site.html index 28562c088..f8f110679 100644 --- a/netbox/templates/dcim/site.html +++ b/netbox/templates/dcim/site.html @@ -16,7 +16,7 @@
- +
-
-
- - Search -
-
- -
- - - - -
- -
-
+ {% include 'inc/search_panel.html' %} {% include 'inc/filter_panel.html' %}
diff --git a/netbox/templates/home.html b/netbox/templates/home.html index 9c0b69cef..1228f1e45 100644 --- a/netbox/templates/home.html +++ b/netbox/templates/home.html @@ -6,7 +6,7 @@
- +
@@ -34,7 +34,7 @@
- + + +
+ +
+
diff --git a/netbox/templates/ipam/aggregate.html b/netbox/templates/ipam/aggregate.html index 0e9f54ebc..31e9c2709 100644 --- a/netbox/templates/ipam/aggregate.html +++ b/netbox/templates/ipam/aggregate.html @@ -5,13 +5,25 @@ {% block content %}
-
+ +
+
+
+ + + + +
+
+
{% if perms.ipam.change_aggregate %} diff --git a/netbox/templates/ipam/aggregate_list.html b/netbox/templates/ipam/aggregate_list.html index af8f87893..71223f596 100644 --- a/netbox/templates/ipam/aggregate_list.html +++ b/netbox/templates/ipam/aggregate_list.html @@ -22,6 +22,7 @@

IPv6 total: {{ ipv6_total|intcomma }} /64s

+ {% include 'inc/search_panel.html' %} {% include 'inc/filter_panel.html' %}
diff --git a/netbox/templates/ipam/inc/prefix_header.html b/netbox/templates/ipam/inc/prefix_header.html index 5c3896884..e5a5ed872 100644 --- a/netbox/templates/ipam/inc/prefix_header.html +++ b/netbox/templates/ipam/inc/prefix_header.html @@ -11,7 +11,7 @@
- +
-
-
- - Search -
-
- -
- - - - -
- -
-
+ {% include 'inc/search_panel.html' %} {% include 'inc/filter_panel.html' %}
diff --git a/netbox/templates/ipam/prefix_list.html b/netbox/templates/ipam/prefix_list.html index 81fd7b795..7020e2eb8 100644 --- a/netbox/templates/ipam/prefix_list.html +++ b/netbox/templates/ipam/prefix_list.html @@ -24,24 +24,7 @@ {% include 'utilities/obj_table.html' with bulk_edit_url='ipam:prefix_bulk_edit' bulk_delete_url='ipam:prefix_bulk_delete' %}
-
-
- - Search -
-
-
-
- - - - -
-
-
-
+ {% include 'inc/search_panel.html' %} {% include 'inc/filter_panel.html' %}
diff --git a/netbox/templates/ipam/vlan.html b/netbox/templates/ipam/vlan.html index 9555a6ea9..111f28eda 100644 --- a/netbox/templates/ipam/vlan.html +++ b/netbox/templates/ipam/vlan.html @@ -18,7 +18,7 @@
- +
-
-
- - Search by ID -
-
- -
- - - - -
- -
-
+ {% include 'inc/search_panel.html' %} {% include 'inc/filter_panel.html' %}
diff --git a/netbox/templates/ipam/vrf.html b/netbox/templates/ipam/vrf.html index 8a8f67c9b..5f357dd01 100644 --- a/netbox/templates/ipam/vrf.html +++ b/netbox/templates/ipam/vrf.html @@ -14,7 +14,7 @@
- +
-
-
- Search -
-
- -
- - - - -
- -
-
+ {% include 'inc/search_panel.html' %} {% include 'inc/filter_panel.html' %}
diff --git a/netbox/templates/secrets/secret_list.html b/netbox/templates/secrets/secret_list.html index c9a785084..5035ef6c5 100644 --- a/netbox/templates/secrets/secret_list.html +++ b/netbox/templates/secrets/secret_list.html @@ -18,6 +18,7 @@ {% include 'utilities/obj_table.html' with bulk_edit_url='secrets:secret_bulk_edit' bulk_delete_url='secrets:secret_bulk_delete' %}
+ {% include 'inc/search_panel.html' %} {% include 'inc/filter_panel.html' %}
diff --git a/netbox/templates/tenancy/tenant_list.html b/netbox/templates/tenancy/tenant_list.html index 2d46412a3..d6fdfbc75 100644 --- a/netbox/templates/tenancy/tenant_list.html +++ b/netbox/templates/tenancy/tenant_list.html @@ -19,23 +19,7 @@ {% include 'utilities/obj_table.html' with bulk_edit_url='tenancy:tenant_bulk_edit' bulk_delete_url='tenancy:tenant_bulk_delete' %}
-
-
- Search -
-
-
-
- - - - -
-
-
-
+ {% include 'inc/search_panel.html' %} {% include 'inc/filter_panel.html' %}
diff --git a/netbox/tenancy/filters.py b/netbox/tenancy/filters.py index 8ae273c88..3493c94ea 100644 --- a/netbox/tenancy/filters.py +++ b/netbox/tenancy/filters.py @@ -1,5 +1,7 @@ import django_filters +from django.db.models import Q + from .models import Tenant, TenantGroup @@ -25,5 +27,8 @@ class TenantFilter(django_filters.FilterSet): fields = ['q', 'group_id', 'group', 'name'] def search(self, queryset, value): - value = value.strip() - return queryset.filter(name__icontains=value) + return queryset.filter( + Q(name__icontains=value) | + Q(description__icontains=value) | + Q(comments__icontains=value) + ) From 75c3e62ca80234eca57cc453bc5b33323bc1a16d Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 29 Jul 2016 13:03:44 -0400 Subject: [PATCH 35/43] Changed VRF and VLAN views to use PrefixBriefTable --- netbox/ipam/tables.py | 1 + netbox/ipam/views.py | 6 ++++-- netbox/templates/ipam/vlan.html | 21 +-------------------- netbox/templates/ipam/vrf.html | 21 +-------------------- 4 files changed, 7 insertions(+), 42 deletions(-) diff --git a/netbox/ipam/tables.py b/netbox/ipam/tables.py index 19c139e9d..ca6f1b926 100644 --- a/netbox/ipam/tables.py +++ b/netbox/ipam/tables.py @@ -159,6 +159,7 @@ class PrefixBriefTable(BaseTable): class Meta(BaseTable.Meta): model = Prefix fields = ('prefix', 'status', 'site', 'role') + orderable = False # diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index b99438a02..3e8a9e64c 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -48,10 +48,11 @@ def vrf(request, pk): vrf = get_object_or_404(VRF.objects.all(), pk=pk) prefixes = Prefix.objects.filter(vrf=vrf) + prefix_table = tables.PrefixBriefTable(prefixes) return render(request, 'ipam/vrf.html', { 'vrf': vrf, - 'prefixes': prefixes, + 'prefix_table': prefix_table, }) @@ -528,10 +529,11 @@ def vlan(request, pk): vlan = get_object_or_404(VLAN.objects.select_related('site', 'role'), pk=pk) prefixes = Prefix.objects.filter(vlan=vlan) + prefix_table = tables.PrefixBriefTable(prefixes) return render(request, 'ipam/vlan.html', { 'vlan': vlan, - 'prefixes': prefixes, + 'prefix_table': prefix_table, }) diff --git a/netbox/templates/ipam/vlan.html b/netbox/templates/ipam/vlan.html index 111f28eda..55aab7b8a 100644 --- a/netbox/templates/ipam/vlan.html +++ b/netbox/templates/ipam/vlan.html @@ -118,26 +118,7 @@
Prefixes
- {% if prefixes %} - - {% for p in prefixes %} - - - - - - - {% endfor %} -
- {{ p }} - - {% if p.site %} - {{ p.site }} - {% endif %} - {{ p.get_status_display }}{{ p.role }}
- {% else %} -
None
- {% endif %} + {% render_table prefix_table %}
diff --git a/netbox/templates/ipam/vrf.html b/netbox/templates/ipam/vrf.html index 5f357dd01..57fef8d43 100644 --- a/netbox/templates/ipam/vrf.html +++ b/netbox/templates/ipam/vrf.html @@ -96,26 +96,7 @@
Prefixes
- {% if prefixes %} - - {% for p in prefixes %} - - - - - - - {% endfor %} -
- {{ p }} - - {% if p.site %} - {{ p.site }} - {% endif %} - {{ p.status }}{{ p.role }}
- {% else %} -
None
- {% endif %} + {% render_table prefix_table %}
From bcb9ab7116de708a138bca8501522ca19cf4ee1c Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 29 Jul 2016 13:19:58 -0400 Subject: [PATCH 36/43] Show graphs button only if there is at least one graph to display --- netbox/circuits/views.py | 3 +++ netbox/dcim/views.py | 8 +++++++- netbox/templates/circuits/provider.html | 10 ++++++---- netbox/templates/dcim/inc/_interface.html | 10 ++++++---- netbox/templates/dcim/site.html | 10 ++++++---- 5 files changed, 28 insertions(+), 13 deletions(-) diff --git a/netbox/circuits/views.py b/netbox/circuits/views.py index 381670d89..d84e23525 100644 --- a/netbox/circuits/views.py +++ b/netbox/circuits/views.py @@ -2,6 +2,7 @@ from django.contrib.auth.mixins import PermissionRequiredMixin from django.db.models import Count from django.shortcuts import get_object_or_404, render +from extras.models import Graph, GRAPH_TYPE_PROVIDER from utilities.views import ( BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView, ) @@ -27,10 +28,12 @@ def provider(request, slug): provider = get_object_or_404(Provider, slug=slug) 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', { 'provider': provider, 'circuits': circuits, + 'show_graphs': show_graphs, }) diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index f0b3ce55a..f9d54f62b 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -15,7 +15,7 @@ from django.views.generic import View from ipam.models import Prefix, IPAddress, VLAN from circuits.models import Circuit -from extras.models import TopologyMap +from extras.models import Graph, TopologyMap, GRAPH_TYPE_INTERFACE from utilities.forms import ConfirmationForm from utilities.views import ( BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView, @@ -81,12 +81,14 @@ def site(request, slug): } rack_groups = RackGroup.objects.filter(site=site).annotate(rack_count=Count('racks')) topology_maps = TopologyMap.objects.filter(site=site) + show_graphs = Graph.objects.filter(type=GRAPH_TYPE_SITE).exists() return render(request, 'dcim/site.html', { 'site': site, 'stats': stats, 'rack_groups': rack_groups, 'topology_maps': topology_maps, + 'show_graphs': show_graphs, }) @@ -585,6 +587,9 @@ def device(request, pk): related_devices = Device.objects.filter(name__istartswith=base_name).exclude(pk=device.pk)\ .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', { 'device': device, 'console_ports': console_ports, @@ -597,6 +602,7 @@ def device(request, pk): 'ip_addresses': ip_addresses, 'secrets': secrets, 'related_devices': related_devices, + 'show_graphs': show_graphs, }) diff --git a/netbox/templates/circuits/provider.html b/netbox/templates/circuits/provider.html index 8ee2e0055..3edb3e02d 100644 --- a/netbox/templates/circuits/provider.html +++ b/netbox/templates/circuits/provider.html @@ -26,10 +26,12 @@
- + {% if show_graphs %} + + {% endif %} {% if perms.dcim.change_site %} From d09ede8d1f096479efebeb0c4a949f8e88b6653a Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 29 Jul 2016 14:29:13 -0400 Subject: [PATCH 37/43] Corrected omitted variable --- netbox/dcim/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index f9d54f62b..cc5ea9af3 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -15,7 +15,7 @@ from django.views.generic import View from ipam.models import Prefix, IPAddress, VLAN from circuits.models import Circuit -from extras.models import Graph, TopologyMap, GRAPH_TYPE_INTERFACE +from extras.models import Graph, TopologyMap, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE from utilities.forms import ConfirmationForm from utilities.views import ( BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView, From 1bbe7f95d63f2d437db327bdccdebfc23f6b410b Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 29 Jul 2016 14:46:29 -0400 Subject: [PATCH 38/43] PEP8 cleanup --- netbox/dcim/tables.py | 6 +++--- netbox/ipam/tables.py | 2 +- netbox/ipam/views.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/netbox/dcim/tables.py b/netbox/dcim/tables.py index f266b3d45..b7920cb71 100644 --- a/netbox/dcim/tables.py +++ b/netbox/dcim/tables.py @@ -87,7 +87,7 @@ class RackGroupTable(BaseTable): rack_count = tables.Column(verbose_name='Racks') slug = tables.Column(verbose_name='Slug') actions = tables.TemplateColumn(template_code=RACKGROUP_ACTIONS, attrs={'td': {'class': 'text-right'}}, - verbose_name='') + verbose_name='') class Meta(BaseTable.Meta): model = RackGroup @@ -139,7 +139,7 @@ class ManufacturerTable(BaseTable): devicetype_count = tables.Column(verbose_name='Device Types') slug = tables.Column(verbose_name='Slug') actions = tables.TemplateColumn(template_code=MANUFACTURER_ACTIONS, attrs={'td': {'class': 'text-right'}}, - verbose_name='') + verbose_name='') class Meta(BaseTable.Meta): model = Manufacturer @@ -236,7 +236,7 @@ class DeviceRoleTable(BaseTable): slug = tables.Column(verbose_name='Slug') color = tables.Column(verbose_name='Color') actions = tables.TemplateColumn(template_code=DEVICEROLE_ACTIONS, attrs={'td': {'class': 'text-right'}}, - verbose_name='') + verbose_name='') class Meta(BaseTable.Meta): model = DeviceRole diff --git a/netbox/ipam/tables.py b/netbox/ipam/tables.py index ca6f1b926..a7444b6b8 100644 --- a/netbox/ipam/tables.py +++ b/netbox/ipam/tables.py @@ -205,7 +205,7 @@ class VLANGroupTable(BaseTable): vlan_count = tables.Column(verbose_name='VLANs') slug = tables.Column(verbose_name='Slug') actions = tables.TemplateColumn(template_code=VLANGROUP_ACTIONS, attrs={'td': {'class': 'text-right'}}, - verbose_name='') + verbose_name='') class Meta(BaseTable.Meta): model = VLANGroup diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index 3e8a9e64c..dbb280c61 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -151,7 +151,7 @@ class AggregateListView(ObjectListView): if a.prefix.version == 4: ipv4_total += a.prefix.size elif a.prefix.version == 6: - ipv6_total += a.prefix.size / 2**64 + ipv6_total += a.prefix.size / 2 ** 64 return { 'ipv4_total': ipv4_total, From 8a9c6ce37a35b2993d77cea88311f993dee43516 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 29 Jul 2016 15:31:35 -0400 Subject: [PATCH 39/43] Standardized display of attributes for primary objects --- netbox/templates/circuits/circuit.html | 42 ++++++++----- netbox/templates/circuits/provider.html | 40 ++++++++++-- netbox/templates/dcim/device.html | 10 +-- netbox/templates/dcim/rack.html | 4 +- netbox/templates/dcim/site.html | 82 +++++++++++++------------ netbox/templates/ipam/aggregate.html | 6 +- netbox/templates/ipam/ipaddress.html | 6 +- netbox/templates/ipam/prefix.html | 18 ++++-- netbox/templates/ipam/vlan.html | 10 ++- netbox/templates/ipam/vrf.html | 6 +- netbox/templates/tenancy/tenant.html | 20 +++--- 11 files changed, 149 insertions(+), 95 deletions(-) diff --git a/netbox/templates/circuits/circuit.html b/netbox/templates/circuits/circuit.html index 308e9a5c5..9fc264eac 100644 --- a/netbox/templates/circuits/circuit.html +++ b/netbox/templates/circuits/circuit.html @@ -1,7 +1,7 @@ {% extends '_base.html' %} {% load helpers %} -{% block title %}{{ circuit.provider }} Circuit {{ circuit.cid }}{% endblock %} +{% block title %}{{ circuit.provider }} - {{ circuit.cid }}{% endblock %} {% block content %} -

{{ circuit.provider }} Circuit {{ circuit.cid }}

+

{{ circuit.provider }} - {{ circuit.cid }}

@@ -66,6 +66,16 @@ {% if circuit.tenant %} {{ circuit.tenant }} + {% else %} + None + {% endif %} + + + + Install Date + + {% if circuit.install_date %} + {{ circuit.install_date }} {% else %} N/A {% endif %} @@ -73,11 +83,23 @@ Port Speed - {{ circuit.port_speed_human }} + + {% if circuit.port_speed %} + {{ circuit.port_speed_human }} + {% else %} + N/A + {% endif %} + Commit Rate - {% if circuit.commit_rate %}{{ circuit.commit_rate_human }}{% else %}N/A{% endif %} + + {% if circuit.commit_speed %} + {{ circuit.commit_speed_human }} + {% else %} + N/A + {% endif %} + Created @@ -112,16 +134,6 @@ {% endif %} - - Install Date - - {% if circuit.install_date %} - {{ circuit.install_date }} - {% else %} - N/A - {% endif %} - - Cross-Connect @@ -149,7 +161,7 @@ Comments
- {% if circuit.comments %} + {% if circuit.comments %} {{ circuit.comments|gfm }} {% else %} None diff --git a/netbox/templates/circuits/provider.html b/netbox/templates/circuits/provider.html index 3edb3e02d..7b8a4a3a5 100644 --- a/netbox/templates/circuits/provider.html +++ b/netbox/templates/circuits/provider.html @@ -55,25 +55,53 @@ - + - + - + - + @@ -90,7 +118,7 @@ Comments
- {% if provider.comments %} + {% if provider.comments %} {{ provider.comments|gfm }} {% else %} None diff --git a/netbox/templates/dcim/device.html b/netbox/templates/dcim/device.html index 2a0a7dee1..a25944262 100644 --- a/netbox/templates/dcim/device.html +++ b/netbox/templates/dcim/device.html @@ -65,7 +65,7 @@ {% if device.serial %} {{ device.serial }} {% else %} - Not defined + N/A {% endif %}
@@ -96,7 +96,7 @@ {% if device.platform %} {{ device.platform }} {% else %} - Not assigned + None {% endif %} @@ -121,7 +121,7 @@ (NAT: {{ device.primary_ip4.nat_outside.address.ip }}) {% endif %} {% else %} - Not defined + N/A {% endif %} @@ -136,7 +136,7 @@ (NAT: {{ device.primary_ip6.nat_outside.address.ip }}) {% endif %} {% else %} - Not defined + N/A {% endif %} @@ -267,7 +267,7 @@ Comments
- {% if device.comments %} + {% if device.comments %} {{ device.comments|gfm }} {% else %} None diff --git a/netbox/templates/dcim/rack.html b/netbox/templates/dcim/rack.html index a2074173a..a265f04b5 100644 --- a/netbox/templates/dcim/rack.html +++ b/netbox/templates/dcim/rack.html @@ -82,7 +82,7 @@ {% if rack.facility_id %} {{ rack.facility_id }} {% else %} - None + N/A {% endif %} @@ -156,7 +156,7 @@ Comments
- {% if rack.comments %} + {% if rack.comments %} {{ rack.comments|gfm }} {% else %} None diff --git a/netbox/templates/dcim/site.html b/netbox/templates/dcim/site.html index 9eb69ef43..3464de34e 100644 --- a/netbox/templates/dcim/site.html +++ b/netbox/templates/dcim/site.html @@ -48,7 +48,7 @@

{{ site.name }}

-
+
Site @@ -66,11 +66,23 @@
- + - + @@ -81,8 +93,10 @@ Map it + {{ site.physical_address|linebreaksbr }} + {% else %} + N/A {% endif %} - {{ site.physical_address|linebreaksbr }} @@ -91,7 +105,7 @@ {% if site.shipping_address %} {{ site.shipping_address|linebreaksbr }} {% else %} - See physical address + N/A {% endif %} @@ -110,7 +124,7 @@ Comments
- {% if site.comments %} + {% if site.comments %} {{ site.comments|gfm }} {% else %} None @@ -118,43 +132,33 @@
-
+
Stats
-
ASN{{ provider.asn }} + {% if provider.asn %} + {{ provider.asn }} + {% else %} + N/A + {% endif %} +
Account{{ provider.account }} + {% if provider.account %} + {{ provider.account }} + {% else %} + N/A + {% endif %} +
Customer Portal - {{ provider.portal_url }} + {% if provider.portal_url %} + {{ provider.portal_url }} + {% else %} + N/A + {% endif %}
NOC Contact{{ provider.noc_contact|linebreaksbr }} + {% if provider.noc_contact %} + {{ provider.noc_contact|linebreaksbr }} + {% else %} + N/A + {% endif %} +
Admin Contact{{ provider.admin_contact|linebreaksbr }} + {% if provider.admin_contact %} + {{ provider.admin_contact|linebreaksbr }} + {% else %} + N/A + {% endif %} +
Created
Facility{{ site.facility }} + {% if site.facility %} + {{ site.facility }} + {% else %} + N/A + {% endif %} +
AS Number{{ site.asn }} + {% if site.asn %} + {{ site.asn }} + {% else %} + N/A + {% endif %} +
Physical Address
- - - - - - - - - - - - - - - - - - - - -
Racks - {{ stats.rack_count }} -
Devices - {{ stats.device_count }} -
Prefixes - {{ stats.prefix_count }} -
VLANs - {{ stats.vlan_count }} -
Circuits - {{ stats.circuit_count }} -
+
+ +
+

{{ stats.device_count }}

+

Devices

+
+
+

{{ stats.prefix_count }}

+

Prefixes

+
+ +
+

{{ stats.circuit_count }}

+

Circuits

+
+
diff --git a/netbox/templates/ipam/aggregate.html b/netbox/templates/ipam/aggregate.html index 31e9c2709..add1e78fc 100644 --- a/netbox/templates/ipam/aggregate.html +++ b/netbox/templates/ipam/aggregate.html @@ -63,17 +63,17 @@ {% if aggregate.date_added %} {{ aggregate.date_added }} {% else %} - Not defined + N/A {% endif %} Description - {% if aggregate.description %} + {% if aggregate.description %} {{ aggregate.description }} {% else %} - None + N/A {% endif %} diff --git a/netbox/templates/ipam/ipaddress.html b/netbox/templates/ipam/ipaddress.html index 7343d2677..73566753d 100644 --- a/netbox/templates/ipam/ipaddress.html +++ b/netbox/templates/ipam/ipaddress.html @@ -70,7 +70,7 @@ {{ ipaddress.tenant }} {% elif ipaddress.vrf.tenant %} {{ ipaddress.vrf.tenant }} - + {% else %} None {% endif %} @@ -79,10 +79,10 @@ Description - {% if ipaddress.description %} + {% if ipaddress.description %} {{ ipaddress.description }} {% else %} - None + N/A {% endif %} diff --git a/netbox/templates/ipam/prefix.html b/netbox/templates/ipam/prefix.html index 0ba97eeb0..8dd442400 100644 --- a/netbox/templates/ipam/prefix.html +++ b/netbox/templates/ipam/prefix.html @@ -33,7 +33,7 @@ {{ prefix.tenant }} {% elif prefix.vrf.tenant %} {{ prefix.vrf.tenant }} - + {% else %} None {% endif %} @@ -55,7 +55,7 @@ {% if prefix.site %} {{ prefix.site }} {% else %} - Not assigned + None {% endif %} @@ -65,7 +65,7 @@ {% if prefix.vlan %} {{ prefix.vlan.display_name }} {% else %} - Not assigned + None {% endif %} @@ -77,15 +77,21 @@ Role - {{ prefix.role }} + + {% if prefix.role %} + {{ prefix.role }} + {% else %} + None + {% endif %} + Description - {% if prefix.description %} + {% if prefix.description %} {{ prefix.description }} {% else %} - None + N/A {% endif %} diff --git a/netbox/templates/ipam/vlan.html b/netbox/templates/ipam/vlan.html index 55aab7b8a..a3198722c 100644 --- a/netbox/templates/ipam/vlan.html +++ b/netbox/templates/ipam/vlan.html @@ -90,7 +90,13 @@ Role - {{ vlan.role }} + + {% if vlan.role %} + {{ vlan.role }} + {% else %} + None + {% endif %} + Description @@ -98,7 +104,7 @@ {% if vlan.description %} {{ vlan.description }} {% else %} - None + N/A {% endif %} diff --git a/netbox/templates/ipam/vrf.html b/netbox/templates/ipam/vrf.html index 57fef8d43..e6a208b96 100644 --- a/netbox/templates/ipam/vrf.html +++ b/netbox/templates/ipam/vrf.html @@ -61,7 +61,7 @@ - Enforce Uniqueness + Unique IP Space {% if vrf.enforce_unique %} @@ -73,10 +73,10 @@ Description - {% if vrf.description %} + {% if vrf.description %} {{ vrf.description }} {% else %} - None + N/A {% endif %} diff --git a/netbox/templates/tenancy/tenant.html b/netbox/templates/tenancy/tenant.html index fdeae4af7..eee92880e 100644 --- a/netbox/templates/tenancy/tenant.html +++ b/netbox/templates/tenancy/tenant.html @@ -78,7 +78,7 @@ Comments
- {% if tenant.comments %} + {% if tenant.comments %} {{ tenant.comments|gfm }} {% else %} None @@ -92,37 +92,35 @@ Stats
-
+ -
+ -
+ - -
-
+ -
+ -
+ -
+ From 65ea2af4b78d99e72099af1b46a74fe734eccc24 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 29 Jul 2016 18:04:38 -0400 Subject: [PATCH 40/43] Partial conversion from glyphicons to font awesome --- netbox/templates/_base.html | 134 +++++++++--------- .../dcim/console_connections_list.html | 2 +- netbox/templates/dcim/device_list.html | 2 +- .../dcim/interface_connections_list.html | 2 +- .../dcim/power_connections_list.html | 2 +- netbox/templates/dcim/rack_list.html | 2 +- netbox/templates/dcim/site_list.html | 2 +- netbox/templates/import_success.html | 2 +- netbox/templates/inc/export_button.html | 4 +- netbox/templates/ipam/ipaddress_list.html | 2 +- netbox/templates/ipam/prefix_list.html | 2 +- netbox/templates/ipam/vlan_list.html | 2 +- netbox/templates/ipam/vrf_list.html | 2 +- netbox/templates/secrets/secret_list.html | 2 +- 14 files changed, 81 insertions(+), 81 deletions(-) diff --git a/netbox/templates/_base.html b/netbox/templates/_base.html index d3097fb68..570f73a4d 100644 --- a/netbox/templates/_base.html +++ b/netbox/templates/_base.html @@ -27,179 +27,179 @@ @@ -207,14 +207,14 @@ @@ -224,12 +224,12 @@
diff --git a/netbox/templates/dcim/console_connections_list.html b/netbox/templates/dcim/console_connections_list.html index bdb5da5d9..eb9531069 100644 --- a/netbox/templates/dcim/console_connections_list.html +++ b/netbox/templates/dcim/console_connections_list.html @@ -7,7 +7,7 @@
{% if perms.dcim.change_consoleport %} - + Import connections {% endif %} diff --git a/netbox/templates/dcim/device_list.html b/netbox/templates/dcim/device_list.html index c358ef90f..7f13af650 100644 --- a/netbox/templates/dcim/device_list.html +++ b/netbox/templates/dcim/device_list.html @@ -11,7 +11,7 @@ Add a device - + Import devices {% endif %} diff --git a/netbox/templates/dcim/interface_connections_list.html b/netbox/templates/dcim/interface_connections_list.html index d27b31a61..56011af1d 100644 --- a/netbox/templates/dcim/interface_connections_list.html +++ b/netbox/templates/dcim/interface_connections_list.html @@ -7,7 +7,7 @@
{% if perms.dcim.add_interfaceconnection %} - + Import connections {% endif %} diff --git a/netbox/templates/dcim/power_connections_list.html b/netbox/templates/dcim/power_connections_list.html index 03a2d13bb..55c9e2ce5 100644 --- a/netbox/templates/dcim/power_connections_list.html +++ b/netbox/templates/dcim/power_connections_list.html @@ -7,7 +7,7 @@
{% if perms.dcim.change_powerport %} - + Import connections {% endif %} diff --git a/netbox/templates/dcim/rack_list.html b/netbox/templates/dcim/rack_list.html index ab97ff246..a07a7487f 100644 --- a/netbox/templates/dcim/rack_list.html +++ b/netbox/templates/dcim/rack_list.html @@ -11,7 +11,7 @@ Add a rack - + Import racks {% endif %} diff --git a/netbox/templates/dcim/site_list.html b/netbox/templates/dcim/site_list.html index a29e8b688..0bbdc7e21 100644 --- a/netbox/templates/dcim/site_list.html +++ b/netbox/templates/dcim/site_list.html @@ -10,7 +10,7 @@ Add a site - + Import sites {% endif %} diff --git a/netbox/templates/import_success.html b/netbox/templates/import_success.html index ec4dc1071..3056e39df 100644 --- a/netbox/templates/import_success.html +++ b/netbox/templates/import_success.html @@ -7,7 +7,7 @@

Import Completed

{% render_table table %} - + Import more {% endblock %} diff --git a/netbox/templates/inc/export_button.html b/netbox/templates/inc/export_button.html index 1d9651121..7b049b2c3 100644 --- a/netbox/templates/inc/export_button.html +++ b/netbox/templates/inc/export_button.html @@ -1,7 +1,7 @@ {% if export_templates %}
{% else %} - + Export {{ obj_type }} {% endif %} diff --git a/netbox/templates/ipam/ipaddress_list.html b/netbox/templates/ipam/ipaddress_list.html index f92892a3d..e1b78d0c6 100644 --- a/netbox/templates/ipam/ipaddress_list.html +++ b/netbox/templates/ipam/ipaddress_list.html @@ -12,7 +12,7 @@ Add an IP - + Import IPs {% endif %} diff --git a/netbox/templates/ipam/prefix_list.html b/netbox/templates/ipam/prefix_list.html index 7020e2eb8..2cbb92d66 100644 --- a/netbox/templates/ipam/prefix_list.html +++ b/netbox/templates/ipam/prefix_list.html @@ -12,7 +12,7 @@ Add a prefix - + Import prefixes {% endif %} diff --git a/netbox/templates/ipam/vlan_list.html b/netbox/templates/ipam/vlan_list.html index dfa15e56c..0933b79f8 100644 --- a/netbox/templates/ipam/vlan_list.html +++ b/netbox/templates/ipam/vlan_list.html @@ -12,7 +12,7 @@ Add a VLAN - + Import VLANs {% endif %} diff --git a/netbox/templates/ipam/vrf_list.html b/netbox/templates/ipam/vrf_list.html index 12b21d292..cebf9c40e 100644 --- a/netbox/templates/ipam/vrf_list.html +++ b/netbox/templates/ipam/vrf_list.html @@ -12,7 +12,7 @@ Add a VRF - + Import VRFs {% endif %} diff --git a/netbox/templates/secrets/secret_list.html b/netbox/templates/secrets/secret_list.html index 5035ef6c5..70bdcf1f1 100644 --- a/netbox/templates/secrets/secret_list.html +++ b/netbox/templates/secrets/secret_list.html @@ -7,7 +7,7 @@
{% if perms.secrets.add_secret %} - + Import secrets {% endif %} From b73f980eb20519a6e285934801e7e18ffcfc2374 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 1 Aug 2016 12:29:26 -0400 Subject: [PATCH 41/43] Closes #176: Added initial_data fixtures for new installs --- docs/installation/netbox.md | 12 ++ netbox/circuits/fixtures/initial_data.json | 26 +++ netbox/dcim/fixtures/initial_data.json | 201 +++++++++++++++++++++ netbox/ipam/fixtures/initial_data.json | 125 +++++++++++++ netbox/secrets/fixtures/initial_data.json | 42 +++++ 5 files changed, 406 insertions(+) create mode 100644 netbox/circuits/fixtures/initial_data.json create mode 100644 netbox/dcim/fixtures/initial_data.json create mode 100644 netbox/ipam/fixtures/initial_data.json create mode 100644 netbox/secrets/fixtures/initial_data.json diff --git a/docs/installation/netbox.md b/docs/installation/netbox.md index 813291033..dbfc23133 100644 --- a/docs/installation/netbox.md +++ b/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 ``` +# 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 At this point, NetBox should be able to run. We can verify this by starting a development instance: diff --git a/netbox/circuits/fixtures/initial_data.json b/netbox/circuits/fixtures/initial_data.json new file mode 100644 index 000000000..c918bbeea --- /dev/null +++ b/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" + } +} +] diff --git a/netbox/dcim/fixtures/initial_data.json b/netbox/dcim/fixtures/initial_data.json new file mode 100644 index 000000000..a26cbfcc5 --- /dev/null +++ b/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" + } +} +] diff --git a/netbox/ipam/fixtures/initial_data.json b/netbox/ipam/fixtures/initial_data.json new file mode 100644 index 000000000..5e7659422 --- /dev/null +++ b/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 + } +} +] diff --git a/netbox/secrets/fixtures/initial_data.json b/netbox/secrets/fixtures/initial_data.json new file mode 100644 index 000000000..1fffbb9bf --- /dev/null +++ b/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": [] + } +} +] From 1fd189f9b111ccb80b7dacc95c2ff5f6aab43899 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 1 Aug 2016 13:29:45 -0400 Subject: [PATCH 42/43] Replaced most glyphicons with Font Awesome --- netbox/templates/500.html | 2 +- netbox/templates/circuits/circuit.html | 6 +++--- netbox/templates/circuits/circuit_list.html | 2 +- netbox/templates/circuits/circuittype_list.html | 2 +- netbox/templates/circuits/provider.html | 8 ++++---- netbox/templates/circuits/provider_list.html | 2 +- netbox/templates/dcim/device_inventory.html | 2 +- netbox/templates/dcim/device_list.html | 2 +- netbox/templates/dcim/devicerole_list.html | 2 +- netbox/templates/dcim/devicetype.html | 4 ++-- netbox/templates/dcim/devicetype_list.html | 2 +- netbox/templates/dcim/inc/_device_header.html | 2 +- netbox/templates/dcim/manufacturer_list.html | 2 +- netbox/templates/dcim/platform_list.html | 2 +- netbox/templates/dcim/rack.html | 10 +++++----- netbox/templates/dcim/rack_list.html | 2 +- netbox/templates/dcim/rackgroup_list.html | 2 +- netbox/templates/dcim/site.html | 8 ++++---- netbox/templates/dcim/site_list.html | 2 +- netbox/templates/home.html | 6 +++--- netbox/templates/inc/filter_panel.html | 4 ++-- netbox/templates/inc/search_panel.html | 4 ++-- netbox/templates/ipam/aggregate.html | 6 +++--- netbox/templates/ipam/aggregate_list.html | 2 +- netbox/templates/ipam/inc/prefix_header.html | 8 ++++---- netbox/templates/ipam/ipaddress.html | 6 +++--- netbox/templates/ipam/ipaddress_list.html | 2 +- netbox/templates/ipam/prefix.html | 2 +- netbox/templates/ipam/prefix_list.html | 2 +- netbox/templates/ipam/rir_list.html | 2 +- netbox/templates/ipam/role_list.html | 2 +- netbox/templates/ipam/vlan.html | 6 +++--- netbox/templates/ipam/vlan_list.html | 2 +- netbox/templates/ipam/vlangroup_list.html | 2 +- netbox/templates/ipam/vrf.html | 6 +++--- netbox/templates/ipam/vrf_list.html | 2 +- netbox/templates/secrets/inc/private_key_modal.html | 2 +- netbox/templates/secrets/secret.html | 6 +++--- netbox/templates/secrets/secretrole_list.html | 2 +- netbox/templates/tenancy/tenant.html | 6 +++--- netbox/templates/tenancy/tenant_list.html | 2 +- netbox/templates/tenancy/tenantgroup_list.html | 2 +- netbox/templates/users/userkey.html | 4 ++-- 43 files changed, 76 insertions(+), 76 deletions(-) diff --git a/netbox/templates/500.html b/netbox/templates/500.html index 33fee68db..26f005f3b 100644 --- a/netbox/templates/500.html +++ b/netbox/templates/500.html @@ -13,7 +13,7 @@
- + Server Error
diff --git a/netbox/templates/circuits/circuit.html b/netbox/templates/circuits/circuit.html index 9fc264eac..481953af6 100644 --- a/netbox/templates/circuits/circuit.html +++ b/netbox/templates/circuits/circuit.html @@ -18,7 +18,7 @@
@@ -28,13 +28,13 @@
{% if perms.circuits.change_circuit %} - + Edit this circuit {% endif %} {% if perms.circuits.delete_circuit %} - + Delete this circuit {% endif %} diff --git a/netbox/templates/circuits/circuit_list.html b/netbox/templates/circuits/circuit_list.html index 1db19b150..f30339ec6 100644 --- a/netbox/templates/circuits/circuit_list.html +++ b/netbox/templates/circuits/circuit_list.html @@ -7,7 +7,7 @@
{% if perms.circuits.add_circuit %} - + Add a circuit {% endif %} diff --git a/netbox/templates/circuits/circuittype_list.html b/netbox/templates/circuits/circuittype_list.html index 68bdb1552..d59a5b82f 100644 --- a/netbox/templates/circuits/circuittype_list.html +++ b/netbox/templates/circuits/circuittype_list.html @@ -7,7 +7,7 @@
{% if perms.circuits.add_circuittype %} - + Add a circuit type {% endif %} diff --git a/netbox/templates/circuits/provider.html b/netbox/templates/circuits/provider.html index 7b8a4a3a5..1388a2c5d 100644 --- a/netbox/templates/circuits/provider.html +++ b/netbox/templates/circuits/provider.html @@ -18,7 +18,7 @@
@@ -28,19 +28,19 @@
{% if show_graphs %} {% endif %} {% if perms.circuits.change_provider %} - + Edit this provider {% endif %} {% if perms.circuits.delete_provider %} - + Delete this provider {% endif %} diff --git a/netbox/templates/circuits/provider_list.html b/netbox/templates/circuits/provider_list.html index ea277bc11..54dfdac93 100644 --- a/netbox/templates/circuits/provider_list.html +++ b/netbox/templates/circuits/provider_list.html @@ -6,7 +6,7 @@
{% if perms.circuits.add_provider %} - + Add a provider {% endif %} diff --git a/netbox/templates/dcim/device_inventory.html b/netbox/templates/dcim/device_inventory.html index e1e8f518d..bf33b1b74 100644 --- a/netbox/templates/dcim/device_inventory.html +++ b/netbox/templates/dcim/device_inventory.html @@ -107,7 +107,7 @@
{% if perms.dcim.add_module %} - + Add a Module {% endif %} diff --git a/netbox/templates/dcim/device_list.html b/netbox/templates/dcim/device_list.html index 7f13af650..18bc7d627 100644 --- a/netbox/templates/dcim/device_list.html +++ b/netbox/templates/dcim/device_list.html @@ -7,7 +7,7 @@
{% if perms.dcim.add_device %} - + Add a device diff --git a/netbox/templates/dcim/devicerole_list.html b/netbox/templates/dcim/devicerole_list.html index bf29d5cb3..67ff32979 100644 --- a/netbox/templates/dcim/devicerole_list.html +++ b/netbox/templates/dcim/devicerole_list.html @@ -7,7 +7,7 @@
{% if perms.dcim.add_devicerole %} - + Add a device role {% endif %} diff --git a/netbox/templates/dcim/devicetype.html b/netbox/templates/dcim/devicetype.html index ce1b69a22..241da123a 100644 --- a/netbox/templates/dcim/devicetype.html +++ b/netbox/templates/dcim/devicetype.html @@ -19,13 +19,13 @@
{% if perms.dcim.change_devicetype %} - + Edit this device type {% endif %} {% if perms.dcim.delete_devicetype %} - + Delete this device type {% endif %} diff --git a/netbox/templates/dcim/devicetype_list.html b/netbox/templates/dcim/devicetype_list.html index a379dd3ad..b16de0799 100644 --- a/netbox/templates/dcim/devicetype_list.html +++ b/netbox/templates/dcim/devicetype_list.html @@ -7,7 +7,7 @@
{% if perms.dcim.add_devicetype %} - + Add a device type {% endif %} diff --git a/netbox/templates/dcim/inc/_device_header.html b/netbox/templates/dcim/inc/_device_header.html index ca3c25f87..7b0b69d61 100644 --- a/netbox/templates/dcim/inc/_device_header.html +++ b/netbox/templates/dcim/inc/_device_header.html @@ -19,7 +19,7 @@
diff --git a/netbox/templates/dcim/manufacturer_list.html b/netbox/templates/dcim/manufacturer_list.html index 9a6f15427..d535e11e8 100644 --- a/netbox/templates/dcim/manufacturer_list.html +++ b/netbox/templates/dcim/manufacturer_list.html @@ -7,7 +7,7 @@
{% if perms.dcim.add_manufacturer %} - + Add a manufacturer {% endif %} diff --git a/netbox/templates/dcim/platform_list.html b/netbox/templates/dcim/platform_list.html index 8f5f40ad9..332447977 100644 --- a/netbox/templates/dcim/platform_list.html +++ b/netbox/templates/dcim/platform_list.html @@ -7,7 +7,7 @@
{% if perms.dcim.add_platform %} - + Add a platform {% endif %} diff --git a/netbox/templates/dcim/rack.html b/netbox/templates/dcim/rack.html index a265f04b5..cf6ebf567 100644 --- a/netbox/templates/dcim/rack.html +++ b/netbox/templates/dcim/rack.html @@ -19,7 +19,7 @@
@@ -29,25 +29,25 @@
{% if prev_rack %} - + Previous Rack {% endif %} {% if next_rack %} - + Next Rack {% endif %} {% if perms.dcim.change_rack %} - + Edit this rack {% endif %} {% if perms.dcim.delete_rack %} - + Delete this rack {% endif %} diff --git a/netbox/templates/dcim/rack_list.html b/netbox/templates/dcim/rack_list.html index a07a7487f..10ee4ff04 100644 --- a/netbox/templates/dcim/rack_list.html +++ b/netbox/templates/dcim/rack_list.html @@ -7,7 +7,7 @@
{% if perms.dcim.add_rack %} - + Add a rack diff --git a/netbox/templates/dcim/rackgroup_list.html b/netbox/templates/dcim/rackgroup_list.html index e0a1e59f3..7b9b0677d 100644 --- a/netbox/templates/dcim/rackgroup_list.html +++ b/netbox/templates/dcim/rackgroup_list.html @@ -7,7 +7,7 @@
{% if perms.dcim.add_rackgroup %} - + Add a rack group {% endif %} diff --git a/netbox/templates/dcim/site.html b/netbox/templates/dcim/site.html index 3464de34e..2bd0ffce8 100644 --- a/netbox/templates/dcim/site.html +++ b/netbox/templates/dcim/site.html @@ -19,7 +19,7 @@
@@ -29,19 +29,19 @@
{% if show_graphs %} {% endif %} {% if perms.dcim.change_site %} - + Edit this site {% endif %} {% if perms.dcim.delete_site %} - + Delete this site {% endif %} diff --git a/netbox/templates/dcim/site_list.html b/netbox/templates/dcim/site_list.html index 0bbdc7e21..45169afe6 100644 --- a/netbox/templates/dcim/site_list.html +++ b/netbox/templates/dcim/site_list.html @@ -6,7 +6,7 @@
{% if perms.dcim.add_site %} - + Add a site diff --git a/netbox/templates/home.html b/netbox/templates/home.html index 1228f1e45..8825d88a0 100644 --- a/netbox/templates/home.html +++ b/netbox/templates/home.html @@ -9,7 +9,7 @@ @@ -23,7 +23,7 @@ @@ -37,7 +37,7 @@ diff --git a/netbox/templates/inc/filter_panel.html b/netbox/templates/inc/filter_panel.html index 32a7e21f8..2c73ba0c0 100644 --- a/netbox/templates/inc/filter_panel.html +++ b/netbox/templates/inc/filter_panel.html @@ -2,7 +2,7 @@
- + Filter
@@ -19,7 +19,7 @@ {% endfor %}
diff --git a/netbox/templates/inc/search_panel.html b/netbox/templates/inc/search_panel.html index a04c1be16..692ef3fd2 100644 --- a/netbox/templates/inc/search_panel.html +++ b/netbox/templates/inc/search_panel.html @@ -1,6 +1,6 @@
- + Search
@@ -9,7 +9,7 @@
diff --git a/netbox/templates/ipam/aggregate.html b/netbox/templates/ipam/aggregate.html index add1e78fc..9a0a8db1f 100644 --- a/netbox/templates/ipam/aggregate.html +++ b/netbox/templates/ipam/aggregate.html @@ -18,7 +18,7 @@
@@ -28,13 +28,13 @@
{% if perms.ipam.change_aggregate %} - + Edit this aggregate {% endif %} {% if perms.ipam.delete_aggregate %} - + Delete this aggregate {% endif %} diff --git a/netbox/templates/ipam/aggregate_list.html b/netbox/templates/ipam/aggregate_list.html index 71223f596..52bca7219 100644 --- a/netbox/templates/ipam/aggregate_list.html +++ b/netbox/templates/ipam/aggregate_list.html @@ -8,7 +8,7 @@
{% if perms.ipam.add_aggregate %} - + Add an aggregate {% endif %} diff --git a/netbox/templates/ipam/inc/prefix_header.html b/netbox/templates/ipam/inc/prefix_header.html index e5a5ed872..7dc7a35a1 100644 --- a/netbox/templates/ipam/inc/prefix_header.html +++ b/netbox/templates/ipam/inc/prefix_header.html @@ -14,7 +14,7 @@
@@ -24,19 +24,19 @@
{% if perms.ipam.add_ipaddress %} - + Add an IP Address {% endif %} {% if perms.ipam.change_prefix %} - + Edit this prefix {% endif %} {% if perms.ipam.delete_prefix %} - + Delete this prefix {% endif %} diff --git a/netbox/templates/ipam/ipaddress.html b/netbox/templates/ipam/ipaddress.html index 73566753d..de5ed637b 100644 --- a/netbox/templates/ipam/ipaddress.html +++ b/netbox/templates/ipam/ipaddress.html @@ -20,7 +20,7 @@
@@ -30,13 +30,13 @@
{% if perms.ipam.change_ipaddress %} - + Edit this IP {% endif %} {% if perms.ipam.delete_ipaddress %} - + Delete this IP {% endif %} diff --git a/netbox/templates/ipam/ipaddress_list.html b/netbox/templates/ipam/ipaddress_list.html index e1b78d0c6..52391a2b4 100644 --- a/netbox/templates/ipam/ipaddress_list.html +++ b/netbox/templates/ipam/ipaddress_list.html @@ -8,7 +8,7 @@ diff --git a/netbox/templates/ipam/prefix_list.html b/netbox/templates/ipam/prefix_list.html index 2cbb92d66..14ac861a2 100644 --- a/netbox/templates/ipam/prefix_list.html +++ b/netbox/templates/ipam/prefix_list.html @@ -8,7 +8,7 @@
{% if perms.ipam.add_prefix %} - + Add a prefix diff --git a/netbox/templates/ipam/rir_list.html b/netbox/templates/ipam/rir_list.html index bd1d93cc8..51d63f4c2 100644 --- a/netbox/templates/ipam/rir_list.html +++ b/netbox/templates/ipam/rir_list.html @@ -7,7 +7,7 @@
{% if perms.ipam.add_rir %} - + Add a RIR {% endif %} diff --git a/netbox/templates/ipam/role_list.html b/netbox/templates/ipam/role_list.html index 5d331ec52..4ee83ad4f 100644 --- a/netbox/templates/ipam/role_list.html +++ b/netbox/templates/ipam/role_list.html @@ -7,7 +7,7 @@
{% if perms.dcim.add_devicerole %} - + Add a role {% endif %} diff --git a/netbox/templates/ipam/vlan.html b/netbox/templates/ipam/vlan.html index a3198722c..4e5037c7c 100644 --- a/netbox/templates/ipam/vlan.html +++ b/netbox/templates/ipam/vlan.html @@ -21,7 +21,7 @@
@@ -31,13 +31,13 @@
{% if perms.ipam.change_vlan %} - + Edit this VLAN {% endif %} {% if perms.ipam.delete_vlan %} - + Delete this VLAN {% endif %} diff --git a/netbox/templates/ipam/vlan_list.html b/netbox/templates/ipam/vlan_list.html index 0933b79f8..78ad140ff 100644 --- a/netbox/templates/ipam/vlan_list.html +++ b/netbox/templates/ipam/vlan_list.html @@ -8,7 +8,7 @@
{% if perms.ipam.add_vlan %} - + Add a VLAN diff --git a/netbox/templates/ipam/vlangroup_list.html b/netbox/templates/ipam/vlangroup_list.html index 1e14b0558..1c8f92387 100644 --- a/netbox/templates/ipam/vlangroup_list.html +++ b/netbox/templates/ipam/vlangroup_list.html @@ -7,7 +7,7 @@
{% if perms.ipam.add_vlangroup %} - + Add a VLAN group {% endif %} diff --git a/netbox/templates/ipam/vrf.html b/netbox/templates/ipam/vrf.html index e6a208b96..bd3cadc9e 100644 --- a/netbox/templates/ipam/vrf.html +++ b/netbox/templates/ipam/vrf.html @@ -17,7 +17,7 @@
@@ -27,13 +27,13 @@
{% if perms.ipam.change_vrf %} - + Edit this VRF {% endif %} {% if perms.ipam.delete_vrf %} - + Delete this VRF {% endif %} diff --git a/netbox/templates/ipam/vrf_list.html b/netbox/templates/ipam/vrf_list.html index cebf9c40e..e5506bfaa 100644 --- a/netbox/templates/ipam/vrf_list.html +++ b/netbox/templates/ipam/vrf_list.html @@ -8,7 +8,7 @@
{% if perms.ipam.add_vrf %} - + Add a VRF diff --git a/netbox/templates/secrets/inc/private_key_modal.html b/netbox/templates/secrets/inc/private_key_modal.html index 0e1e6219a..d55e6425e 100644 --- a/netbox/templates/secrets/inc/private_key_modal.html +++ b/netbox/templates/secrets/inc/private_key_modal.html @@ -4,7 +4,7 @@ diff --git a/netbox/templates/secrets/secret.html b/netbox/templates/secrets/secret.html index 953d6bcd0..53cdb4d7c 100644 --- a/netbox/templates/secrets/secret.html +++ b/netbox/templates/secrets/secret.html @@ -17,13 +17,13 @@
{% if perms.secrets.change_secret %} - + Edit this secret {% endif %} {% if perms.secrets.delete_secret %} - + Delete this secret {% endif %} @@ -93,7 +93,7 @@
{% else %}
- + You do not have permission to decrypt this secret.
{% endif %} diff --git a/netbox/templates/secrets/secretrole_list.html b/netbox/templates/secrets/secretrole_list.html index 2ad708248..55e12a1a8 100644 --- a/netbox/templates/secrets/secretrole_list.html +++ b/netbox/templates/secrets/secretrole_list.html @@ -7,7 +7,7 @@
{% if perms.dcim.add_devicerole %} - + Add a secret role {% endif %} diff --git a/netbox/templates/tenancy/tenant.html b/netbox/templates/tenancy/tenant.html index eee92880e..0d3d8fdaa 100644 --- a/netbox/templates/tenancy/tenant.html +++ b/netbox/templates/tenancy/tenant.html @@ -18,7 +18,7 @@
@@ -28,13 +28,13 @@
{% if perms.tenancy.change_tenant %} - + Edit this tenant {% endif %} {% if perms.tenancy.delete_tenant %} - + Delete this tenant {% endif %} diff --git a/netbox/templates/tenancy/tenant_list.html b/netbox/templates/tenancy/tenant_list.html index d6fdfbc75..24f796da3 100644 --- a/netbox/templates/tenancy/tenant_list.html +++ b/netbox/templates/tenancy/tenant_list.html @@ -7,7 +7,7 @@
{% if perms.tenancy.add_tenant %} - + Add a tenant {% endif %} diff --git a/netbox/templates/tenancy/tenantgroup_list.html b/netbox/templates/tenancy/tenantgroup_list.html index be270a95c..7be881b00 100644 --- a/netbox/templates/tenancy/tenantgroup_list.html +++ b/netbox/templates/tenancy/tenantgroup_list.html @@ -7,7 +7,7 @@
{% if perms.tenancy.add_tenantgroup %} - + Add a tenant group {% endif %} diff --git a/netbox/templates/users/userkey.html b/netbox/templates/users/userkey.html index 9728ef93f..cbc748d17 100644 --- a/netbox/templates/users/userkey.html +++ b/netbox/templates/users/userkey.html @@ -27,7 +27,7 @@
{{ userkey.public_key }}
@@ -35,7 +35,7 @@

You don't have a user key on file.

- + Create a User Key

From 00e0fb5798ccb7734d68869c7bf02a12fbae08c2 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 1 Aug 2016 13:35:49 -0400 Subject: [PATCH 43/43] Pre-release version bump --- netbox/netbox/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 256c6d4ce..3ddbc8af5 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -12,7 +12,7 @@ except ImportError: "the documentation.") -VERSION = '1.4.0-dev' +VERSION = '1.4.0' # Import local configuration for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']: