diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2a2b14446..2be85cf64 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -45,6 +45,10 @@ sure to include: * Any error messages generated * Screenshots (if applicable) +* Please avoid prepending any sort of tag (e.g. "[Bug]") to the issue title. +The issue will be reviewed by a moderator after submission and the appropriate +labels will be applied. + * Keep in mind that we prioritize bugs based on their severity and how much work is required to resolve them. It may take some time for someone to address your issue. @@ -91,6 +95,10 @@ following: * Any third-party libraries or other resources which would be involved +* Please avoid prepending any sort of tag (e.g. "[Feature]") to the issue title. +The issue will be reviewed by a moderator after submission and the appropriate +labels will be applied. + ## Submitting Pull Requests * Be sure to open an issue before starting work on a pull request, and diff --git a/docs/installation/upgrading.md b/docs/installation/upgrading.md index 193d7e74a..46b2252f7 100644 --- a/docs/installation/upgrading.md +++ b/docs/installation/upgrading.md @@ -58,6 +58,14 @@ This script: * Applies any database migrations that were included in the release * Collects all static files to be served by the HTTP service +!!! note + It's possible that the upgrade script will display a notice warning of unreflected database migrations: + + Your models have changes that are not yet reflected in a migration, and so won't be applied. + Run 'manage.py makemigrations' to make new migrations, and then re-run 'manage.py migrate' to apply them. + + This may occur due to semantic differences in environment, and can be safely ignored. Never attempt to create new migrations unless you are inentionally modifying the database schema. + # Restart the WSGI Service Finally, restart the WSGI service to run the new code. If you followed this guide for the initial installation, this is done using `supervisorctl`: diff --git a/netbox/circuits/api/serializers.py b/netbox/circuits/api/serializers.py index b36d22105..f2e6d0d00 100644 --- a/netbox/circuits/api/serializers.py +++ b/netbox/circuits/api/serializers.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from rest_framework import serializers from circuits.models import Provider, Circuit, CircuitTermination, CircuitType diff --git a/netbox/circuits/api/urls.py b/netbox/circuits/api/urls.py index dfe0624e8..25df44bfd 100644 --- a/netbox/circuits/api/urls.py +++ b/netbox/circuits/api/urls.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from rest_framework import routers from . import views diff --git a/netbox/circuits/api/views.py b/netbox/circuits/api/views.py index 582de3ab1..d14080531 100644 --- a/netbox/circuits/api/views.py +++ b/netbox/circuits/api/views.py @@ -1,9 +1,11 @@ -from django.shortcuts import get_object_or_404 +from __future__ import unicode_literals from rest_framework.decorators import detail_route from rest_framework.response import Response from rest_framework.viewsets import ModelViewSet +from django.shortcuts import get_object_or_404 + from circuits import filters from circuits.models import Provider, CircuitTermination, CircuitType, Circuit from extras.models import Graph, GRAPH_TYPE_PROVIDER diff --git a/netbox/circuits/apps.py b/netbox/circuits/apps.py index bc0b7d87d..613c347f2 100644 --- a/netbox/circuits/apps.py +++ b/netbox/circuits/apps.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from django.apps import AppConfig diff --git a/netbox/circuits/filters.py b/netbox/circuits/filters.py index e4a11faf0..6e9e1f443 100644 --- a/netbox/circuits/filters.py +++ b/netbox/circuits/filters.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + import django_filters from django.db.models import Q diff --git a/netbox/circuits/forms.py b/netbox/circuits/forms.py index f81abff04..79cad0a6b 100644 --- a/netbox/circuits/forms.py +++ b/netbox/circuits/forms.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from django import forms from django.db.models import Count @@ -165,7 +167,9 @@ class CircuitTerminationForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm ) rack = ChainedModelChoiceField( queryset=Rack.objects.all(), - chains={'site': 'site'}, + chains=( + ('site', 'site'), + ), required=False, label='Rack', widget=APISelect( @@ -175,7 +179,10 @@ class CircuitTerminationForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm ) device = ChainedModelChoiceField( queryset=Device.objects.all(), - chains={'site': 'site', 'rack': 'rack'}, + chains=( + ('site', 'site'), + ('rack', 'rack'), + ), required=False, label='Device', widget=APISelect( @@ -184,20 +191,13 @@ class CircuitTerminationForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm attrs={'filter-for': 'interface'} ) ) - livesearch = forms.CharField( - required=False, - label='Device', - widget=Livesearch( - query_key='q', - query_url='dcim-api:device-list', - field_to_update='device' - ) - ) interface = ChainedModelChoiceField( queryset=Interface.objects.exclude(form_factor__in=VIRTUAL_IFACE_TYPES).select_related( 'circuit_termination', 'connected_as_a', 'connected_as_b' ), - chains={'device': 'device'}, + chains=( + ('device', 'device'), + ), required=False, label='Interface', widget=APISelect( @@ -208,8 +208,10 @@ class CircuitTerminationForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm class Meta: model = CircuitTermination - fields = ['term_side', 'site', 'rack', 'device', 'livesearch', 'interface', 'port_speed', 'upstream_speed', - 'xconnect_id', 'pp_info'] + fields = [ + 'term_side', 'site', 'rack', 'device', 'interface', 'port_speed', 'upstream_speed', 'xconnect_id', + 'pp_info', + ] help_texts = { 'port_speed': "Physical circuit speed", 'xconnect_id': "ID of the local cross-connect", diff --git a/netbox/circuits/migrations/0009_unicode_literals.py b/netbox/circuits/migrations/0009_unicode_literals.py new file mode 100644 index 000000000..0f22a2268 --- /dev/null +++ b/netbox/circuits/migrations/0009_unicode_literals.py @@ -0,0 +1,81 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2017-05-24 15:34 +from __future__ import unicode_literals + +import dcim.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('circuits', '0008_circuittermination_interface_protect_on_delete'), + ] + + operations = [ + migrations.AlterField( + model_name='circuit', + name='cid', + field=models.CharField(max_length=50, verbose_name='Circuit ID'), + ), + migrations.AlterField( + model_name='circuit', + name='commit_rate', + field=models.PositiveIntegerField(blank=True, null=True, verbose_name='Commit rate (Kbps)'), + ), + migrations.AlterField( + model_name='circuit', + name='install_date', + field=models.DateField(blank=True, null=True, verbose_name='Date installed'), + ), + migrations.AlterField( + model_name='circuittermination', + name='port_speed', + field=models.PositiveIntegerField(verbose_name='Port speed (Kbps)'), + ), + migrations.AlterField( + model_name='circuittermination', + name='pp_info', + field=models.CharField(blank=True, max_length=100, verbose_name='Patch panel/port(s)'), + ), + migrations.AlterField( + model_name='circuittermination', + name='term_side', + field=models.CharField(choices=[('A', 'A'), ('Z', 'Z')], max_length=1, verbose_name='Termination'), + ), + migrations.AlterField( + model_name='circuittermination', + name='upstream_speed', + field=models.PositiveIntegerField(blank=True, help_text='Upstream speed, if different from port speed', null=True, verbose_name='Upstream speed (Kbps)'), + ), + migrations.AlterField( + model_name='circuittermination', + name='xconnect_id', + field=models.CharField(blank=True, max_length=50, verbose_name='Cross-connect ID'), + ), + migrations.AlterField( + model_name='provider', + name='account', + field=models.CharField(blank=True, max_length=30, verbose_name='Account number'), + ), + migrations.AlterField( + model_name='provider', + name='admin_contact', + field=models.TextField(blank=True, verbose_name='Admin contact'), + ), + migrations.AlterField( + model_name='provider', + name='asn', + field=dcim.fields.ASNField(blank=True, null=True, verbose_name='ASN'), + ), + migrations.AlterField( + model_name='provider', + name='noc_contact', + field=models.TextField(blank=True, verbose_name='NOC contact'), + ), + migrations.AlterField( + model_name='provider', + name='portal_url', + field=models.URLField(blank=True, verbose_name='Portal'), + ), + ] diff --git a/netbox/circuits/models.py b/netbox/circuits/models.py index 04d9b3e13..7da5c4f73 100644 --- a/netbox/circuits/models.py +++ b/netbox/circuits/models.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from django.contrib.contenttypes.fields import GenericRelation from django.db import models from django.urls import reverse @@ -110,7 +112,7 @@ class Circuit(CreatedUpdatedModel, CustomFieldModel): unique_together = ['provider', 'cid'] def __str__(self): - return u'{} {}'.format(self.provider, self.cid) + return '{} {}'.format(self.provider, self.cid) def get_absolute_url(self): return reverse('circuits:circuit', args=[self.pk]) @@ -166,7 +168,7 @@ class CircuitTermination(models.Model): unique_together = ['circuit', 'term_side'] def __str__(self): - return u'{} (Side {})'.format(self.circuit, self.get_term_side_display()) + return '{} (Side {})'.format(self.circuit, self.get_term_side_display()) def get_peer_termination(self): peer_side = 'Z' if self.term_side == 'A' else 'A' diff --git a/netbox/circuits/signals.py b/netbox/circuits/signals.py index bdfe8c0b6..40a1e1031 100644 --- a/netbox/circuits/signals.py +++ b/netbox/circuits/signals.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from django.db.models.signals import post_delete, post_save from django.dispatch import receiver from django.utils import timezone diff --git a/netbox/circuits/tables.py b/netbox/circuits/tables.py index 3cda30ccc..d09c5a7b2 100644 --- a/netbox/circuits/tables.py +++ b/netbox/circuits/tables.py @@ -1,8 +1,9 @@ +from __future__ import unicode_literals + import django_tables2 as tables from django_tables2.utils import Accessor from utilities.tables import BaseTable, SearchTable, ToggleColumn - from .models import Circuit, CircuitType, Provider diff --git a/netbox/circuits/tests/test_api.py b/netbox/circuits/tests/test_api.py index 7bd3d8040..fc39b72de 100644 --- a/netbox/circuits/tests/test_api.py +++ b/netbox/circuits/tests/test_api.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from rest_framework import status from rest_framework.test import APITestCase diff --git a/netbox/circuits/urls.py b/netbox/circuits/urls.py index b23f21a88..12a7dc298 100644 --- a/netbox/circuits/urls.py +++ b/netbox/circuits/urls.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from django.conf.urls import url from . import views @@ -12,7 +14,7 @@ urlpatterns = [ url(r'^providers/import/$', views.ProviderBulkImportView.as_view(), name='provider_import'), url(r'^providers/edit/$', views.ProviderBulkEditView.as_view(), name='provider_bulk_edit'), url(r'^providers/delete/$', views.ProviderBulkDeleteView.as_view(), name='provider_bulk_delete'), - url(r'^providers/(?P[\w-]+)/$', views.provider, name='provider'), + url(r'^providers/(?P[\w-]+)/$', views.ProviderView.as_view(), name='provider'), url(r'^providers/(?P[\w-]+)/edit/$', views.ProviderEditView.as_view(), name='provider_edit'), url(r'^providers/(?P[\w-]+)/delete/$', views.ProviderDeleteView.as_view(), name='provider_delete'), @@ -28,7 +30,7 @@ urlpatterns = [ url(r'^circuits/import/$', views.CircuitBulkImportView.as_view(), name='circuit_import'), url(r'^circuits/edit/$', views.CircuitBulkEditView.as_view(), name='circuit_bulk_edit'), url(r'^circuits/delete/$', views.CircuitBulkDeleteView.as_view(), name='circuit_bulk_delete'), - url(r'^circuits/(?P\d+)/$', views.circuit, name='circuit'), + url(r'^circuits/(?P\d+)/$', views.CircuitView.as_view(), name='circuit'), url(r'^circuits/(?P\d+)/edit/$', views.CircuitEditView.as_view(), name='circuit_edit'), url(r'^circuits/(?P\d+)/delete/$', views.CircuitDeleteView.as_view(), name='circuit_delete'), url(r'^circuits/(?P\d+)/terminations/swap/$', views.circuit_terminations_swap, name='circuit_terminations_swap'), diff --git a/netbox/circuits/views.py b/netbox/circuits/views.py index 9cc4dd3c4..eed612a33 100644 --- a/netbox/circuits/views.py +++ b/netbox/circuits/views.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from django.contrib import messages from django.contrib.auth.decorators import permission_required from django.contrib.auth.mixins import PermissionRequiredMixin @@ -5,13 +7,13 @@ from django.db import transaction from django.db.models import Count from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse +from django.views.generic import View from extras.models import Graph, GRAPH_TYPE_PROVIDER from utilities.forms import ConfirmationForm from utilities.views import ( BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView, ) - from . import filters, forms, tables from .models import Circuit, CircuitTermination, CircuitType, Provider, TERM_SIDE_A, TERM_SIDE_Z @@ -28,18 +30,23 @@ class ProviderListView(ObjectListView): template_name = 'circuits/provider_list.html' -def provider(request, slug): +class ProviderView(View): - provider = get_object_or_404(Provider, slug=slug) - circuits = Circuit.objects.filter(provider=provider).select_related('type', 'tenant')\ - .prefetch_related('terminations__site') - show_graphs = Graph.objects.filter(type=GRAPH_TYPE_PROVIDER).exists() + def get(self, request, slug): - return render(request, 'circuits/provider.html', { - 'provider': provider, - 'circuits': circuits, - 'show_graphs': show_graphs, - }) + provider = get_object_or_404(Provider, slug=slug) + circuits = Circuit.objects.filter(provider=provider).select_related( + 'type', 'tenant' + ).prefetch_related( + 'terminations__site' + ) + show_graphs = Graph.objects.filter(type=GRAPH_TYPE_PROVIDER).exists() + + return render(request, 'circuits/provider.html', { + 'provider': provider, + 'circuits': circuits, + 'show_graphs': show_graphs, + }) class ProviderEditView(PermissionRequiredMixin, ObjectEditView): @@ -117,25 +124,27 @@ class CircuitListView(ObjectListView): template_name = 'circuits/circuit_list.html' -def circuit(request, pk): +class CircuitView(View): - circuit = get_object_or_404(Circuit.objects.select_related('provider', 'type', 'tenant__group'), pk=pk) - termination_a = CircuitTermination.objects.select_related( - 'site__region', 'interface__device' - ).filter( - circuit=circuit, term_side=TERM_SIDE_A - ).first() - termination_z = CircuitTermination.objects.select_related( - 'site__region', 'interface__device' - ).filter( - circuit=circuit, term_side=TERM_SIDE_Z - ).first() + def get(self, request, pk): - return render(request, 'circuits/circuit.html', { - 'circuit': circuit, - 'termination_a': termination_a, - 'termination_z': termination_z, - }) + circuit = get_object_or_404(Circuit.objects.select_related('provider', 'type', 'tenant__group'), pk=pk) + termination_a = CircuitTermination.objects.select_related( + 'site__region', 'interface__device' + ).filter( + circuit=circuit, term_side=TERM_SIDE_A + ).first() + termination_z = CircuitTermination.objects.select_related( + 'site__region', 'interface__device' + ).filter( + circuit=circuit, term_side=TERM_SIDE_Z + ).first() + + return render(request, 'circuits/circuit.html', { + 'circuit': circuit, + 'termination_a': termination_a, + 'termination_z': termination_z, + }) class CircuitEditView(PermissionRequiredMixin, ObjectEditView): diff --git a/netbox/dcim/api/exceptions.py b/netbox/dcim/api/exceptions.py index 05ad86b5b..8804da436 100644 --- a/netbox/dcim/api/exceptions.py +++ b/netbox/dcim/api/exceptions.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from rest_framework.exceptions import APIException diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 2959aa901..8ca6cab35 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from rest_framework import serializers from rest_framework.validators import UniqueTogetherValidator @@ -618,10 +620,11 @@ class PeerInterfaceSerializer(serializers.ModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail') device = NestedDeviceSerializer() form_factor = ChoiceFieldSerializer(choices=IFACE_FF_CHOICES) + lag = NestedInterfaceSerializer() class Meta: model = Interface - fields = ['id', 'url', 'device', 'name', 'form_factor', 'mac_address', 'mgmt_only', 'description'] + fields = ['id', 'url', 'device', 'name', 'form_factor', 'lag', 'mac_address', 'mgmt_only', 'description'] class WritableInterfaceSerializer(serializers.ModelSerializer): diff --git a/netbox/dcim/api/urls.py b/netbox/dcim/api/urls.py index db537e0b7..6f16310e5 100644 --- a/netbox/dcim/api/urls.py +++ b/netbox/dcim/api/urls.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from rest_framework import routers from . import views diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index 82fc5d6ab..116aaa77c 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from rest_framework.decorators import detail_route from rest_framework.mixins import ListModelMixin from rest_framework.permissions import IsAuthenticated diff --git a/netbox/dcim/apps.py b/netbox/dcim/apps.py index fdfcc1f57..fb1f4ee39 100644 --- a/netbox/dcim/apps.py +++ b/netbox/dcim/apps.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from django.apps import AppConfig diff --git a/netbox/dcim/fields.py b/netbox/dcim/fields.py index 6b45f6e65..22e0be581 100644 --- a/netbox/dcim/fields.py +++ b/netbox/dcim/fields.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from netaddr import EUI, mac_unix_expanded from django.core.exceptions import ValidationError diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index 39838a265..93a325d98 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + import django_filters from netaddr.core import AddrFormatError diff --git a/netbox/dcim/formfields.py b/netbox/dcim/formfields.py index 4e568c2e6..83054c088 100644 --- a/netbox/dcim/formfields.py +++ b/netbox/dcim/formfields.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from netaddr import EUI, AddrFormatError from django import forms diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index c110f1d47..9e1cc657d 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from mptt.forms import TreeNodeChoiceField import re @@ -16,7 +18,6 @@ from utilities.forms import ( FilterChoiceField, FlexibleModelChoiceField, Livesearch, SelectWithDisabled, SmallTextarea, SlugField, FilterTreeNodeMultipleChoiceField, ) - from .formfields import MACAddressFormField from .models import ( DeviceBay, DeviceBayTemplate, CONNECTION_STATUS_CHOICES, CONNECTION_STATUS_PLANNED, CONNECTION_STATUS_CONNECTED, @@ -189,7 +190,9 @@ class RackRoleForm(BootstrapMixin, forms.ModelForm): class RackForm(BootstrapMixin, TenancyForm, CustomFieldForm): group = ChainedModelChoiceField( queryset=RackGroup.objects.all(), - chains={'site': 'site'}, + chains=( + ('site', 'site'), + ), required=False, widget=APISelect( api_url='/api/dcim/rack-groups/?site_id={{site}}', @@ -544,7 +547,9 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldForm): ) rack = ChainedModelChoiceField( queryset=Rack.objects.all(), - chains={'site': 'site'}, + chains=( + ('site', 'site'), + ), required=False, widget=APISelect( api_url='/api/dcim/racks/?site_id={{site}}', @@ -569,7 +574,9 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldForm): ) device_type = ChainedModelChoiceField( queryset=DeviceType.objects.all(), - chains={'manufacturer': 'manufacturer'}, + chains=( + ('manufacturer', 'manufacturer'), + ), label='Device type', widget=APISelect( api_url='/api/dcim/device-types/?manufacturer_id={{manufacturer}}', @@ -610,10 +617,10 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldForm): for family in [4, 6]: ip_choices = [] interface_ips = IPAddress.objects.filter(family=family, interface__device=self.instance) - ip_choices += [(ip.id, u'{} ({})'.format(ip.address, ip.interface)) for ip in interface_ips] + ip_choices += [(ip.id, '{} ({})'.format(ip.address, ip.interface)) for ip in interface_ips] nat_ips = IPAddress.objects.filter(family=family, nat_inside__interface__device=self.instance)\ .select_related('nat_inside__interface') - ip_choices += [(ip.id, u'{} ({} NAT)'.format(ip.address, ip.nat_inside.interface)) for ip in nat_ips] + ip_choices += [(ip.id, '{} ({} NAT)'.format(ip.address, ip.nat_inside.interface)) for ip in nat_ips] self.fields['primary_ip{}'.format(family)].choices = [(None, '---------')] + ip_choices # If editing an existing device, exclude it from the list of occupied rack units. This ensures that a device @@ -804,7 +811,7 @@ def device_status_choices(): status_counts = {} for status in Device.objects.values('status').annotate(count=Count('status')).order_by('status'): status_counts[status['status']] = status['count'] - return [(s[0], u'{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in STATUS_CHOICES] + return [(s[0], '{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in STATUS_CHOICES] class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm): @@ -956,20 +963,29 @@ class ConsoleConnectionImportForm(BootstrapMixin, BulkImportForm): class ConsolePortConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm): site = forms.ModelChoiceField( queryset=Site.objects.all(), - widget=forms.HiddenInput(), + required=False, + widget=forms.Select( + attrs={'filter-for': 'rack'} + ) ) rack = ChainedModelChoiceField( queryset=Rack.objects.all(), - chains={'site': 'site'}, + chains=( + ('site', 'site'), + ), label='Rack', required=False, - widget=forms.Select( + widget=APISelect( + api_url='/api/dcim/racks/?site_id={{site}}', attrs={'filter-for': 'console_server', 'nullable': 'true'} ) ) console_server = ChainedModelChoiceField( queryset=Device.objects.filter(device_type__is_console_server=True), - chains={'site': 'site', 'rack': 'rack'}, + chains=( + ('site', 'site'), + ('rack', 'rack'), + ), label='Console Server', required=False, widget=APISelect( @@ -989,7 +1005,9 @@ class ConsolePortConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelF ) cs_port = ChainedModelChoiceField( queryset=ConsoleServerPort.objects.all(), - chains={'device': 'console_server'}, + chains=( + ('device', 'console_server'), + ), label='Port', widget=APISelect( api_url='/api/dcim/console-server-ports/?device_id={{console_server}}', @@ -1034,20 +1052,29 @@ class ConsoleServerPortCreateForm(DeviceComponentForm): class ConsoleServerPortConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.Form): site = forms.ModelChoiceField( queryset=Site.objects.all(), - widget=forms.HiddenInput(), + required=False, + widget=forms.Select( + attrs={'filter-for': 'rack'} + ) ) rack = ChainedModelChoiceField( queryset=Rack.objects.all(), - chains={'site': 'site'}, + chains=( + ('site', 'site'), + ), label='Rack', required=False, - widget=forms.Select( + widget=APISelect( + api_url='/api/dcim/racks/?site_id={{site}}', attrs={'filter-for': 'device', 'nullable': 'true'} ) ) device = ChainedModelChoiceField( queryset=Device.objects.all(), - chains={'site': 'site', 'rack': 'rack'}, + chains=( + ('site', 'site'), + ('rack', 'rack'), + ), label='Device', required=False, widget=APISelect( @@ -1067,7 +1094,9 @@ class ConsoleServerPortConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms. ) port = ChainedModelChoiceField( queryset=ConsolePort.objects.all(), - chains={'device': 'device'}, + chains=( + ('device', 'device'), + ), label='Port', widget=APISelect( api_url='/api/dcim/console-ports/?device_id={{device}}', @@ -1181,19 +1210,31 @@ class PowerConnectionImportForm(BootstrapMixin, BulkImportForm): class PowerPortConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm): - site = forms.ModelChoiceField(queryset=Site.objects.all(), widget=forms.HiddenInput()) - rack = ChainedModelChoiceField( - queryset=Rack.objects.all(), - chains={'site': 'site'}, - label='Rack', + site = forms.ModelChoiceField( + queryset=Site.objects.all(), required=False, widget=forms.Select( + attrs={'filter-for': 'rack'} + ) + ) + rack = ChainedModelChoiceField( + queryset=Rack.objects.all(), + chains=( + ('site', 'site'), + ), + label='Rack', + required=False, + widget=APISelect( + api_url='/api/dcim/racks/?site_id={{site}}', attrs={'filter-for': 'pdu', 'nullable': 'true'} ) ) pdu = ChainedModelChoiceField( queryset=Device.objects.all(), - chains={'site': 'site', 'rack': 'rack'}, + chains=( + ('site', 'site'), + ('rack', 'rack'), + ), label='PDU', required=False, widget=APISelect( @@ -1213,7 +1254,9 @@ class PowerPortConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelFor ) power_outlet = ChainedModelChoiceField( queryset=PowerOutlet.objects.all(), - chains={'device': 'pdu'}, + chains=( + ('device', 'pdu'), + ), label='Outlet', widget=APISelect( api_url='/api/dcim/power-outlets/?device_id={{pdu}}', @@ -1258,20 +1301,29 @@ class PowerOutletCreateForm(DeviceComponentForm): class PowerOutletConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.Form): site = forms.ModelChoiceField( queryset=Site.objects.all(), - widget=forms.HiddenInput() + required=False, + widget=forms.Select( + attrs={'filter-for': 'rack'} + ) ) rack = ChainedModelChoiceField( queryset=Rack.objects.all(), - chains={'site': 'site'}, + chains=( + ('site', 'site'), + ), label='Rack', required=False, - widget=forms.Select( + widget=APISelect( + api_url='/api/dcim/racks/?site_id={{site}}', attrs={'filter-for': 'device', 'nullable': 'true'} ) ) device = ChainedModelChoiceField( queryset=Device.objects.all(), - chains={'site': 'site', 'rack': 'rack'}, + chains=( + ('site', 'site'), + ('rack', 'rack'), + ), label='Device', required=False, widget=APISelect( @@ -1291,7 +1343,9 @@ class PowerOutletConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.Form): ) port = ChainedModelChoiceField( queryset=PowerPort.objects.all(), - chains={'device': 'device'}, + chains=( + ('device', 'device'), + ), label='Port', widget=APISelect( api_url='/api/dcim/power-ports/?device_id={{device}}', @@ -1411,7 +1465,9 @@ class InterfaceConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelFor ) rack_b = ChainedModelChoiceField( queryset=Rack.objects.all(), - chains={'site': 'site_b'}, + chains=( + ('site', 'site_b'), + ), label='Rack', required=False, widget=APISelect( @@ -1421,7 +1477,10 @@ class InterfaceConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelFor ) device_b = ChainedModelChoiceField( queryset=Device.objects.all(), - chains={'site': 'site_b', 'rack': 'rack_b'}, + chains=( + ('site', 'site_b'), + ('rack', 'rack_b'), + ), label='Device', required=False, widget=APISelect( @@ -1443,7 +1502,9 @@ class InterfaceConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelFor queryset=Interface.objects.exclude(form_factor__in=VIRTUAL_IFACE_TYPES).select_related( 'circuit_termination', 'connected_as_a', 'connected_as_b' ), - chains={'device': 'device_b'}, + chains=( + ('device', 'device_b'), + ), label='Interface', widget=APISelect( api_url='/api/dcim/interfaces/?device_id={{device_b}}&type=physical', diff --git a/netbox/dcim/migrations/0037_unicode_literals.py b/netbox/dcim/migrations/0037_unicode_literals.py new file mode 100644 index 000000000..cba05becc --- /dev/null +++ b/netbox/dcim/migrations/0037_unicode_literals.py @@ -0,0 +1,209 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2017-05-24 15:34 +from __future__ import unicode_literals + +import dcim.fields +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion +import utilities.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('dcim', '0036_add_ff_juniper_vcp'), + ] + + operations = [ + migrations.AlterField( + model_name='consoleport', + name='connection_status', + field=models.NullBooleanField(choices=[[False, 'Planned'], [True, 'Connected']], default=True), + ), + migrations.AlterField( + model_name='consoleport', + name='cs_port', + field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='connected_console', to='dcim.ConsoleServerPort', verbose_name='Console server port'), + ), + migrations.AlterField( + model_name='device', + name='asset_tag', + field=utilities.fields.NullableCharField(blank=True, help_text='A unique tag used to identify this device', max_length=50, null=True, unique=True, verbose_name='Asset tag'), + ), + migrations.AlterField( + model_name='device', + name='face', + field=models.PositiveSmallIntegerField(blank=True, choices=[[0, 'Front'], [1, 'Rear']], null=True, verbose_name='Rack face'), + ), + migrations.AlterField( + model_name='device', + name='position', + field=models.PositiveSmallIntegerField(blank=True, help_text='The lowest-numbered unit occupied by the device', null=True, validators=[django.core.validators.MinValueValidator(1)], verbose_name='Position (U)'), + ), + migrations.AlterField( + model_name='device', + name='primary_ip4', + field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='primary_ip4_for', to='ipam.IPAddress', verbose_name='Primary IPv4'), + ), + migrations.AlterField( + model_name='device', + name='primary_ip6', + field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='primary_ip6_for', to='ipam.IPAddress', verbose_name='Primary IPv6'), + ), + migrations.AlterField( + model_name='device', + name='serial', + field=models.CharField(blank=True, max_length=50, verbose_name='Serial number'), + ), + migrations.AlterField( + model_name='device', + name='status', + field=models.PositiveSmallIntegerField(choices=[[1, 'Active'], [0, 'Offline'], [2, 'Planned'], [3, 'Staged'], [4, 'Failed'], [5, 'Inventory']], default=1, verbose_name='Status'), + ), + migrations.AlterField( + model_name='devicebay', + name='name', + field=models.CharField(max_length=50, verbose_name='Name'), + ), + migrations.AlterField( + model_name='devicetype', + name='interface_ordering', + field=models.PositiveSmallIntegerField(choices=[[1, 'Slot/position'], [2, 'Name (alphabetically)']], default=1), + ), + migrations.AlterField( + model_name='devicetype', + name='is_console_server', + field=models.BooleanField(default=False, help_text='This type of device has console server ports', verbose_name='Is a console server'), + ), + migrations.AlterField( + model_name='devicetype', + name='is_full_depth', + field=models.BooleanField(default=True, help_text='Device consumes both front and rear rack faces', verbose_name='Is full depth'), + ), + migrations.AlterField( + model_name='devicetype', + name='is_network_device', + field=models.BooleanField(default=True, help_text='This type of device has network interfaces', verbose_name='Is a network device'), + ), + migrations.AlterField( + model_name='devicetype', + name='is_pdu', + field=models.BooleanField(default=False, help_text='This type of device has power outlets', verbose_name='Is a PDU'), + ), + migrations.AlterField( + model_name='devicetype', + name='part_number', + field=models.CharField(blank=True, help_text='Discrete part number (optional)', max_length=50), + ), + migrations.AlterField( + model_name='devicetype', + name='subdevice_role', + field=models.NullBooleanField(choices=[(None, 'None'), (True, 'Parent'), (False, 'Child')], default=None, help_text='Parent devices house child devices in device bays. Select "None" if this device type is neither a parent nor a child.', verbose_name='Parent/child status'), + ), + migrations.AlterField( + model_name='devicetype', + name='u_height', + field=models.PositiveSmallIntegerField(default=1, verbose_name='Height (U)'), + ), + migrations.AlterField( + model_name='interface', + name='form_factor', + field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1600, 'QSFP28 (100GE)']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[5000, 'Cisco StackWise'], [5050, 'Cisco StackWise Plus'], [5100, 'Cisco FlexStack'], [5150, 'Cisco FlexStack Plus'], [5200, 'Juniper VCP']]], ['Other', [[32767, 'Other']]]], default=1200), + ), + migrations.AlterField( + model_name='interface', + name='lag', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='member_interfaces', to='dcim.Interface', verbose_name='Parent LAG'), + ), + migrations.AlterField( + model_name='interface', + name='mac_address', + field=dcim.fields.MACAddressField(blank=True, null=True, verbose_name='MAC Address'), + ), + migrations.AlterField( + model_name='interface', + name='mgmt_only', + field=models.BooleanField(default=False, help_text='This interface is used only for out-of-band management', verbose_name='OOB Management'), + ), + migrations.AlterField( + model_name='interfaceconnection', + name='connection_status', + field=models.BooleanField(choices=[[False, 'Planned'], [True, 'Connected']], default=True, verbose_name='Status'), + ), + migrations.AlterField( + model_name='interfacetemplate', + name='form_factor', + field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1600, 'QSFP28 (100GE)']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[5000, 'Cisco StackWise'], [5050, 'Cisco StackWise Plus'], [5100, 'Cisco FlexStack'], [5150, 'Cisco FlexStack Plus'], [5200, 'Juniper VCP']]], ['Other', [[32767, 'Other']]]], default=1200), + ), + migrations.AlterField( + model_name='interfacetemplate', + name='mgmt_only', + field=models.BooleanField(default=False, verbose_name='Management only'), + ), + migrations.AlterField( + model_name='inventoryitem', + name='discovered', + field=models.BooleanField(default=False, verbose_name='Discovered'), + ), + migrations.AlterField( + model_name='inventoryitem', + name='name', + field=models.CharField(max_length=50, verbose_name='Name'), + ), + migrations.AlterField( + model_name='inventoryitem', + name='part_id', + field=models.CharField(blank=True, max_length=50, verbose_name='Part ID'), + ), + migrations.AlterField( + model_name='inventoryitem', + name='serial', + field=models.CharField(blank=True, max_length=50, verbose_name='Serial number'), + ), + migrations.AlterField( + model_name='platform', + name='rpc_client', + field=models.CharField(blank=True, choices=[['juniper-junos', 'Juniper Junos (NETCONF)'], ['cisco-ios', 'Cisco IOS (SSH)'], ['opengear', 'Opengear (SSH)']], max_length=30, verbose_name='RPC client'), + ), + migrations.AlterField( + model_name='powerport', + name='connection_status', + field=models.NullBooleanField(choices=[[False, 'Planned'], [True, 'Connected']], default=True), + ), + migrations.AlterField( + model_name='rack', + name='desc_units', + field=models.BooleanField(default=False, help_text='Units are numbered top-to-bottom', verbose_name='Descending units'), + ), + migrations.AlterField( + model_name='rack', + name='facility_id', + field=utilities.fields.NullableCharField(blank=True, max_length=30, null=True, verbose_name='Facility ID'), + ), + migrations.AlterField( + model_name='rack', + name='type', + field=models.PositiveSmallIntegerField(blank=True, choices=[(100, '2-post frame'), (200, '4-post frame'), (300, '4-post cabinet'), (1000, 'Wall-mounted frame'), (1100, 'Wall-mounted cabinet')], null=True, verbose_name='Type'), + ), + migrations.AlterField( + model_name='rack', + name='u_height', + field=models.PositiveSmallIntegerField(default=42, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)], verbose_name='Height (U)'), + ), + migrations.AlterField( + model_name='rack', + name='width', + field=models.PositiveSmallIntegerField(choices=[(19, '19 inches'), (23, '23 inches')], default=19, help_text='Rail-to-rail width', verbose_name='Width'), + ), + migrations.AlterField( + model_name='site', + name='asn', + field=dcim.fields.ASNField(blank=True, null=True, verbose_name='ASN'), + ), + migrations.AlterField( + model_name='site', + name='contact_email', + field=models.EmailField(blank=True, max_length=254, verbose_name='Contact E-mail'), + ), + ] diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index 59d7a0ef2..043df10dc 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -1,3 +1,4 @@ +from __future__ import unicode_literals from collections import OrderedDict from itertools import count, groupby @@ -23,7 +24,6 @@ from utilities.fields import ColorField, NullableCharField from utilities.managers import NaturalOrderByManager from utilities.models import CreatedUpdatedModel from utilities.utils import csv_format - from .fields import ASNField, MACAddressField @@ -346,7 +346,7 @@ class RackGroup(models.Model): ] def __str__(self): - return u'{} - {}'.format(self.site.name, self.name) + return '{} - {}'.format(self.site.name, self.name) def get_absolute_url(self): return "{}?group_id={}".format(reverse('dcim:rack_list'), self.pk) @@ -466,10 +466,10 @@ class Rack(CreatedUpdatedModel, CustomFieldModel): @property def display_name(self): if self.facility_id: - return u"{} ({})".format(self.name, self.facility_id) + return "{} ({})".format(self.name, self.facility_id) elif self.name: return self.name - return u"" + return "" def get_rack_units(self, face=RACK_FACE_FRONT, exclude=None, remove_redundant=False): """ @@ -569,7 +569,7 @@ class RackReservation(models.Model): ordering = ['created'] def __str__(self): - return u"Reservation for rack {}".format(self.rack) + return "Reservation for rack {}".format(self.rack) def clean(self): @@ -579,7 +579,7 @@ class RackReservation(models.Model): invalid_units = [u for u in self.units if u not in self.rack.units] if invalid_units: raise ValidationError({ - 'units': u"Invalid unit(s) for {}U rack: {}".format( + 'units': "Invalid unit(s) for {}U rack: {}".format( self.rack.u_height, ', '.join([str(u) for u in invalid_units]), ), @@ -733,7 +733,7 @@ class DeviceType(models.Model, CustomFieldModel): @property def full_name(self): - return u'{} {}'.format(self.manufacturer.name, self.model) + return '{} {}'.format(self.manufacturer.name, self.model) @property def is_parent_device(self): @@ -1106,8 +1106,8 @@ class Device(CreatedUpdatedModel, CustomFieldModel): if self.name: return self.name elif hasattr(self, 'device_type'): - return u"{}".format(self.device_type) - return u"" + return "{}".format(self.device_type) + return "" @property def identifier(self): @@ -1320,7 +1320,7 @@ class Interface(models.Model): # An interface's LAG must belong to the same device if self.lag and self.lag.device != self.device: raise ValidationError({ - 'lag': u"The selected LAG interface ({}) belongs to a different device ({}).".format( + 'lag': "The selected LAG interface ({}) belongs to a different device ({}).".format( self.lag.name, self.lag.device.name ) }) @@ -1328,14 +1328,14 @@ class Interface(models.Model): # A virtual interface cannot have a parent LAG if self.form_factor in VIRTUAL_IFACE_TYPES and self.lag is not None: raise ValidationError({ - 'lag': u"{} interfaces cannot have a parent LAG interface.".format(self.get_form_factor_display()) + 'lag': "{} interfaces cannot have a parent LAG interface.".format(self.get_form_factor_display()) }) # Only a LAG can have LAG members if self.form_factor != IFACE_FF_LAG and self.member_interfaces.exists(): raise ValidationError({ 'form_factor': "Cannot change interface form factor; it has LAG members ({}).".format( - u", ".join([iface.name for iface in self.member_interfaces.all()]) + ", ".join([iface.name for iface in self.member_interfaces.all()]) ) }) @@ -1428,7 +1428,7 @@ class DeviceBay(models.Model): unique_together = ['device', 'name'] def __str__(self): - return u'{} - {}'.format(self.device.name, self.name) + return '{} - {}'.format(self.device.name, self.name) def clean(self): diff --git a/netbox/dcim/tables.py b/netbox/dcim/tables.py index d15e274c7..be80233e0 100644 --- a/netbox/dcim/tables.py +++ b/netbox/dcim/tables.py @@ -1,8 +1,9 @@ +from __future__ import unicode_literals + import django_tables2 as tables from django_tables2.utils import Accessor from utilities.tables import BaseTable, SearchTable, ToggleColumn - from .models import ( ConsolePort, ConsolePortTemplate, ConsoleServerPortTemplate, Device, DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceTemplate, Manufacturer, Platform, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, diff --git a/netbox/dcim/tests/test_api.py b/netbox/dcim/tests/test_api.py index 2f3b38449..9fe191cc7 100644 --- a/netbox/dcim/tests/test_api.py +++ b/netbox/dcim/tests/test_api.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from rest_framework import status from rest_framework.test import APITestCase diff --git a/netbox/dcim/tests/test_forms.py b/netbox/dcim/tests/test_forms.py index f859fe5e1..acf71411e 100644 --- a/netbox/dcim/tests/test_forms.py +++ b/netbox/dcim/tests/test_forms.py @@ -1,4 +1,7 @@ +from __future__ import unicode_literals + from django.test import TestCase + from dcim.forms import * from dcim.models import * diff --git a/netbox/dcim/tests/test_models.py b/netbox/dcim/tests/test_models.py index d1b721cb0..340c58092 100644 --- a/netbox/dcim/tests/test_models.py +++ b/netbox/dcim/tests/test_models.py @@ -1,4 +1,7 @@ +from __future__ import unicode_literals + from django.test import TestCase + from dcim.models import * diff --git a/netbox/dcim/urls.py b/netbox/dcim/urls.py index 8e666f406..775daeabf 100644 --- a/netbox/dcim/urls.py +++ b/netbox/dcim/urls.py @@ -1,9 +1,10 @@ +from __future__ import unicode_literals + from django.conf.urls import url +from extras.views import ImageAttachmentEditView from ipam.views import ServiceEditView from secrets.views import secret_add - -from extras.views import ImageAttachmentEditView from .models import Device, Rack, Site from . import views @@ -22,7 +23,7 @@ urlpatterns = [ 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-]+)/$', views.SiteView.as_view(), 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'), url(r'^sites/(?P\d+)/images/add/$', ImageAttachmentEditView.as_view(), name='site_add_image', kwargs={'model': Site}), @@ -52,7 +53,7 @@ urlpatterns = [ url(r'^racks/import/$', views.RackBulkImportView.as_view(), name='rack_import'), url(r'^racks/edit/$', views.RackBulkEditView.as_view(), name='rack_bulk_edit'), url(r'^racks/delete/$', views.RackBulkDeleteView.as_view(), name='rack_bulk_delete'), - url(r'^racks/(?P\d+)/$', views.rack, name='rack'), + url(r'^racks/(?P\d+)/$', views.RackView.as_view(), name='rack'), url(r'^racks/(?P\d+)/edit/$', views.RackEditView.as_view(), name='rack_edit'), url(r'^racks/(?P\d+)/delete/$', views.RackDeleteView.as_view(), name='rack_delete'), url(r'^racks/(?P\d+)/reservations/add/$', views.RackReservationEditView.as_view(), name='rack_add_reservation'), @@ -69,7 +70,7 @@ urlpatterns = [ url(r'^device-types/add/$', views.DeviceTypeEditView.as_view(), name='devicetype_add'), url(r'^device-types/edit/$', views.DeviceTypeBulkEditView.as_view(), name='devicetype_bulk_edit'), url(r'^device-types/delete/$', views.DeviceTypeBulkDeleteView.as_view(), name='devicetype_bulk_delete'), - url(r'^device-types/(?P\d+)/$', views.devicetype, name='devicetype'), + url(r'^device-types/(?P\d+)/$', views.DeviceTypeView.as_view(), name='devicetype'), url(r'^device-types/(?P\d+)/edit/$', views.DeviceTypeEditView.as_view(), name='devicetype_edit'), url(r'^device-types/(?P\d+)/delete/$', views.DeviceTypeDeleteView.as_view(), name='devicetype_delete'), @@ -117,11 +118,11 @@ urlpatterns = [ url(r'^devices/import/child-devices/$', views.ChildDeviceBulkImportView.as_view(), name='device_import_child'), url(r'^devices/edit/$', views.DeviceBulkEditView.as_view(), name='device_bulk_edit'), url(r'^devices/delete/$', views.DeviceBulkDeleteView.as_view(), name='device_bulk_delete'), - url(r'^devices/(?P\d+)/$', views.device, name='device'), + url(r'^devices/(?P\d+)/$', views.DeviceView.as_view(), name='device'), url(r'^devices/(?P\d+)/edit/$', views.DeviceEditView.as_view(), name='device_edit'), url(r'^devices/(?P\d+)/delete/$', views.DeviceDeleteView.as_view(), name='device_delete'), - url(r'^devices/(?P\d+)/inventory/$', views.device_inventory, name='device_inventory'), - url(r'^devices/(?P\d+)/lldp-neighbors/$', views.device_lldp_neighbors, name='device_lldp_neighbors'), + url(r'^devices/(?P\d+)/inventory/$', views.DeviceInventoryView.as_view(), name='device_inventory'), + url(r'^devices/(?P\d+)/lldp-neighbors/$', views.DeviceLLDPNeighborsView.as_view(), name='device_lldp_neighbors'), url(r'^devices/(?P\d+)/add-secret/$', secret_add, name='device_addsecret'), url(r'^devices/(?P\d+)/services/assign/$', ServiceEditView.as_view(), name='service_assign'), url(r'^devices/(?P\d+)/images/add/$', ImageAttachmentEditView.as_view(), name='device_add_image', kwargs={'model': Device}), diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 4ffe2d36b..f6e00be04 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -1,3 +1,4 @@ +from __future__ import unicode_literals from copy import deepcopy import re from natsort import natsorted @@ -24,7 +25,6 @@ from utilities.paginator import EnhancedPaginator from utilities.views import ( BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView, ) - from . import filters, forms, tables from .models import ( CONNECTION_STATUS_CONNECTED, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, @@ -109,11 +109,11 @@ class ComponentCreateView(View): if field == 'name': field = 'name_pattern' for e in errors: - form.add_error(field, u'{}: {}'.format(name, ', '.join(e))) + form.add_error(field, '{}: {}'.format(name, ', '.join(e))) if not form.errors: self.model.objects.bulk_create(new_components) - messages.success(request, u"Added {} {} to {}.".format( + messages.success(request, "Added {} {} to {}.".format( len(new_components), self.model._meta.verbose_name_plural, parent )) if '_addanother' in request.POST: @@ -178,27 +178,29 @@ class SiteListView(ObjectListView): template_name = 'dcim/site_list.html' -def site(request, slug): +class SiteView(View): - site = get_object_or_404(Site.objects.select_related('region', 'tenant__group'), slug=slug) - stats = { - 'rack_count': Rack.objects.filter(site=site).count(), - 'device_count': Device.objects.filter(site=site).count(), - 'prefix_count': Prefix.objects.filter(site=site).count(), - 'vlan_count': VLAN.objects.filter(site=site).count(), - 'circuit_count': Circuit.objects.filter(terminations__site=site).count(), - } - 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() + def get(self, request, slug): - return render(request, 'dcim/site.html', { - 'site': site, - 'stats': stats, - 'rack_groups': rack_groups, - 'topology_maps': topology_maps, - 'show_graphs': show_graphs, - }) + site = get_object_or_404(Site.objects.select_related('region', 'tenant__group'), slug=slug) + stats = { + 'rack_count': Rack.objects.filter(site=site).count(), + 'device_count': Device.objects.filter(site=site).count(), + 'prefix_count': Prefix.objects.filter(site=site).count(), + 'vlan_count': VLAN.objects.filter(site=site).count(), + 'circuit_count': Circuit.objects.filter(terminations__site=site).count(), + } + 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, + }) class SiteEditView(PermissionRequiredMixin, ObjectEditView): @@ -290,8 +292,13 @@ class RackRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): # class RackListView(ObjectListView): - queryset = Rack.objects.select_related('site', 'group', 'tenant', 'role').prefetch_related('devices__device_type')\ - .annotate(device_count=Count('devices', distinct=True)) + queryset = Rack.objects.select_related( + 'site', 'group', 'tenant', 'role' + ).prefetch_related( + 'devices__device_type' + ).annotate( + device_count=Count('devices', distinct=True) + ) filter = filters.RackFilter filter_form = forms.RackFilterForm table = tables.RackTable @@ -338,31 +345,33 @@ class RackElevationListView(View): }) -def rack(request, pk): +class RackView(View): - rack = get_object_or_404(Rack.objects.select_related('site__region', 'tenant__group', 'group', 'role'), pk=pk) + def get(self, request, pk): - 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() + rack = get_object_or_404(Rack.objects.select_related('site__region', 'tenant__group', 'group', 'role'), pk=pk) - reservations = RackReservation.objects.filter(rack=rack) - reserved_units = {} - for r in reservations: - for u in r.units: - reserved_units[u] = r + 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() - return render(request, 'dcim/rack.html', { - 'rack': rack, - 'reservations': reservations, - 'reserved_units': reserved_units, - 'nonracked_devices': nonracked_devices, - 'next_rack': next_rack, - 'prev_rack': prev_rack, - 'front_elevation': rack.get_front_elevation(), - 'rear_elevation': rack.get_rear_elevation(), - }) + reservations = RackReservation.objects.filter(rack=rack) + reserved_units = {} + for r in reservations: + for u in r.units: + reserved_units[u] = r + + return render(request, 'dcim/rack.html', { + 'rack': rack, + 'reservations': reservations, + 'reserved_units': reserved_units, + 'nonracked_devices': nonracked_devices, + 'next_rack': next_rack, + 'prev_rack': prev_rack, + 'front_elevation': rack.get_front_elevation(), + 'rear_elevation': rack.get_rear_elevation(), + }) class RackEditView(PermissionRequiredMixin, ObjectEditView): @@ -481,53 +490,57 @@ class DeviceTypeListView(ObjectListView): template_name = 'dcim/devicetype_list.html' -def devicetype(request, pk): +class DeviceTypeView(View): - devicetype = get_object_or_404(DeviceType, pk=pk) + def get(self, request, pk): - # Component tables - consoleport_table = tables.ConsolePortTemplateTable( - natsorted(ConsolePortTemplate.objects.filter(device_type=devicetype), key=attrgetter('name')) - ) - consoleserverport_table = tables.ConsoleServerPortTemplateTable( - natsorted(ConsoleServerPortTemplate.objects.filter(device_type=devicetype), key=attrgetter('name')) - ) - powerport_table = tables.PowerPortTemplateTable( - natsorted(PowerPortTemplate.objects.filter(device_type=devicetype), key=attrgetter('name')) - ) - poweroutlet_table = tables.PowerOutletTemplateTable( - natsorted(PowerOutletTemplate.objects.filter(device_type=devicetype), key=attrgetter('name')) - ) - mgmt_interface_table = tables.InterfaceTemplateTable( - list(InterfaceTemplate.objects.order_naturally(devicetype.interface_ordering).filter(device_type=devicetype, - mgmt_only=True)) - ) - interface_table = tables.InterfaceTemplateTable( - list(InterfaceTemplate.objects.order_naturally(devicetype.interface_ordering).filter(device_type=devicetype, - mgmt_only=False)) - ) - devicebay_table = tables.DeviceBayTemplateTable( - natsorted(DeviceBayTemplate.objects.filter(device_type=devicetype), key=attrgetter('name')) - ) - if request.user.has_perm('dcim.change_devicetype'): - consoleport_table.base_columns['pk'].visible = True - consoleserverport_table.base_columns['pk'].visible = True - powerport_table.base_columns['pk'].visible = True - poweroutlet_table.base_columns['pk'].visible = True - mgmt_interface_table.base_columns['pk'].visible = True - interface_table.base_columns['pk'].visible = True - devicebay_table.base_columns['pk'].visible = True + devicetype = get_object_or_404(DeviceType, pk=pk) - return render(request, 'dcim/devicetype.html', { - 'devicetype': devicetype, - 'consoleport_table': consoleport_table, - 'consoleserverport_table': consoleserverport_table, - 'powerport_table': powerport_table, - 'poweroutlet_table': poweroutlet_table, - 'mgmt_interface_table': mgmt_interface_table, - 'interface_table': interface_table, - 'devicebay_table': devicebay_table, - }) + # Component tables + consoleport_table = tables.ConsolePortTemplateTable( + natsorted(ConsolePortTemplate.objects.filter(device_type=devicetype), key=attrgetter('name')) + ) + consoleserverport_table = tables.ConsoleServerPortTemplateTable( + natsorted(ConsoleServerPortTemplate.objects.filter(device_type=devicetype), key=attrgetter('name')) + ) + powerport_table = tables.PowerPortTemplateTable( + natsorted(PowerPortTemplate.objects.filter(device_type=devicetype), key=attrgetter('name')) + ) + poweroutlet_table = tables.PowerOutletTemplateTable( + natsorted(PowerOutletTemplate.objects.filter(device_type=devicetype), key=attrgetter('name')) + ) + mgmt_interface_table = tables.InterfaceTemplateTable( + list(InterfaceTemplate.objects.order_naturally(devicetype.interface_ordering).filter( + device_type=devicetype, mgmt_only=True + )) + ) + interface_table = tables.InterfaceTemplateTable( + list(InterfaceTemplate.objects.order_naturally(devicetype.interface_ordering).filter( + device_type=devicetype, mgmt_only=False + )) + ) + devicebay_table = tables.DeviceBayTemplateTable( + natsorted(DeviceBayTemplate.objects.filter(device_type=devicetype), key=attrgetter('name')) + ) + if request.user.has_perm('dcim.change_devicetype'): + consoleport_table.base_columns['pk'].visible = True + consoleserverport_table.base_columns['pk'].visible = True + powerport_table.base_columns['pk'].visible = True + poweroutlet_table.base_columns['pk'].visible = True + mgmt_interface_table.base_columns['pk'].visible = True + interface_table.base_columns['pk'].visible = True + devicebay_table.base_columns['pk'].visible = True + + return render(request, 'dcim/devicetype.html', { + 'devicetype': devicetype, + 'consoleport_table': consoleport_table, + 'consoleserverport_table': consoleserverport_table, + 'powerport_table': powerport_table, + 'poweroutlet_table': poweroutlet_table, + 'mgmt_interface_table': mgmt_interface_table, + 'interface_table': interface_table, + 'devicebay_table': devicebay_table, + }) class DeviceTypeEditView(PermissionRequiredMixin, ObjectEditView): @@ -727,70 +740,114 @@ class DeviceListView(ObjectListView): template_name = 'dcim/device_list.html' -def device(request, pk): +class DeviceView(View): - device = get_object_or_404(Device.objects.select_related( - 'site__region', 'rack__group', 'tenant__group', 'device_role', 'platform' - ), pk=pk) - console_ports = natsorted( - ConsolePort.objects.filter(device=device).select_related('cs_port__device'), key=attrgetter('name') - ) - cs_ports = natsorted( - ConsoleServerPort.objects.filter(device=device).select_related('connected_console'), key=attrgetter('name') - ) - power_ports = natsorted( - PowerPort.objects.filter(device=device).select_related('power_outlet__device'), key=attrgetter('name') - ) - power_outlets = natsorted( - PowerOutlet.objects.filter(device=device).select_related('connected_port'), key=attrgetter('name') - ) - interfaces = Interface.objects.order_naturally(device.device_type.interface_ordering)\ - .filter(device=device, mgmt_only=False)\ - .select_related('connected_as_a__interface_b__device', 'connected_as_b__interface_a__device', - 'circuit_termination__circuit').prefetch_related('ip_addresses') - mgmt_interfaces = Interface.objects.order_naturally(device.device_type.interface_ordering)\ - .filter(device=device, mgmt_only=True)\ - .select_related('connected_as_a__interface_b__device', 'connected_as_b__interface_a__device', - 'circuit_termination__circuit').prefetch_related('ip_addresses') - device_bays = natsorted( - DeviceBay.objects.filter(device=device).select_related('installed_device__device_type__manufacturer'), - key=attrgetter('name') - ) - services = Service.objects.filter(device=device) - secrets = device.secrets.all() + def get(self, request, pk): - # Find any related devices for convenient linking in the UI - related_devices = [] - if device.name: - if re.match('.+[0-9]+$', device.name): - # Strip 1 or more trailing digits (e.g. core-switch1) - base_name = re.match('(.*?)[0-9]+$', device.name).group(1) - elif re.match('.+\d[a-z]$', device.name.lower()): - # Strip a trailing letter if preceded by a digit (e.g. dist-switch3a -> dist-switch3) - base_name = re.match('(.*\d+)[a-z]$', device.name.lower()).group(1) - else: - base_name = None - if base_name: - related_devices = Device.objects.filter(name__istartswith=base_name).exclude(pk=device.pk)\ - .select_related('rack', 'device_type__manufacturer')[:10] + device = get_object_or_404(Device.objects.select_related( + 'site__region', 'rack__group', 'tenant__group', 'device_role', 'platform' + ), pk=pk) + console_ports = natsorted( + ConsolePort.objects.filter(device=device).select_related('cs_port__device'), key=attrgetter('name') + ) + cs_ports = natsorted( + ConsoleServerPort.objects.filter(device=device).select_related('connected_console'), key=attrgetter('name') + ) + power_ports = natsorted( + PowerPort.objects.filter(device=device).select_related('power_outlet__device'), key=attrgetter('name') + ) + power_outlets = natsorted( + PowerOutlet.objects.filter(device=device).select_related('connected_port'), key=attrgetter('name') + ) + interfaces = Interface.objects.order_naturally(device.device_type.interface_ordering).filter( + device=device, mgmt_only=False + ).select_related( + 'connected_as_a__interface_b__device', 'connected_as_b__interface_a__device', + 'circuit_termination__circuit' + ).prefetch_related('ip_addresses') + mgmt_interfaces = Interface.objects.order_naturally(device.device_type.interface_ordering).filter( + device=device, mgmt_only=True + ).select_related( + 'connected_as_a__interface_b__device', 'connected_as_b__interface_a__device', + 'circuit_termination__circuit' + ).prefetch_related('ip_addresses') + device_bays = natsorted( + DeviceBay.objects.filter(device=device).select_related('installed_device__device_type__manufacturer'), + key=attrgetter('name') + ) + services = Service.objects.filter(device=device) + secrets = device.secrets.all() - # Show graph button on interfaces only if at least one graph has been created. - show_graphs = Graph.objects.filter(type=GRAPH_TYPE_INTERFACE).exists() + # Find any related devices for convenient linking in the UI + related_devices = [] + if device.name: + if re.match('.+[0-9]+$', device.name): + # Strip 1 or more trailing digits (e.g. core-switch1) + base_name = re.match('(.*?)[0-9]+$', device.name).group(1) + elif re.match('.+\d[a-z]$', device.name.lower()): + # Strip a trailing letter if preceded by a digit (e.g. dist-switch3a -> dist-switch3) + base_name = re.match('(.*\d+)[a-z]$', device.name.lower()).group(1) + else: + base_name = None + if base_name: + related_devices = Device.objects.filter(name__istartswith=base_name).exclude(pk=device.pk)\ + .select_related('rack', 'device_type__manufacturer')[:10] - return render(request, 'dcim/device.html', { - 'device': device, - 'console_ports': console_ports, - 'cs_ports': cs_ports, - 'power_ports': power_ports, - 'power_outlets': power_outlets, - 'interfaces': interfaces, - 'mgmt_interfaces': mgmt_interfaces, - 'device_bays': device_bays, - 'services': services, - 'secrets': secrets, - 'related_devices': related_devices, - 'show_graphs': show_graphs, - }) + # 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, + 'cs_ports': cs_ports, + 'power_ports': power_ports, + 'power_outlets': power_outlets, + 'interfaces': interfaces, + 'mgmt_interfaces': mgmt_interfaces, + 'device_bays': device_bays, + 'services': services, + 'secrets': secrets, + 'related_devices': related_devices, + 'show_graphs': show_graphs, + }) + + +class DeviceInventoryView(View): + + def get(self, request, pk): + + device = get_object_or_404(Device, pk=pk) + inventory_items = InventoryItem.objects.filter( + device=device, parent=None + ).select_related( + 'manufacturer' + ).prefetch_related( + 'child_items' + ) + + return render(request, 'dcim/device_inventory.html', { + 'device': device, + 'inventory_items': inventory_items, + }) + + +class DeviceLLDPNeighborsView(View): + + def get(self, request, pk): + + device = get_object_or_404(Device, pk=pk) + interfaces = Interface.objects.order_naturally( + device.device_type.interface_ordering + ).filter( + device=device + ).select_related( + 'connected_as_a', 'connected_as_b' + ) + + return render(request, 'dcim/device_lldp_neighbors.html', { + 'device': device, + 'interfaces': interfaces, + }) class DeviceEditView(PermissionRequiredMixin, ObjectEditView): @@ -851,30 +908,6 @@ class DeviceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): default_return_url = 'dcim:device_list' -def device_inventory(request, pk): - - device = get_object_or_404(Device, pk=pk) - inventory_items = InventoryItem.objects.filter(device=device, parent=None).select_related('manufacturer')\ - .prefetch_related('child_items') - - return render(request, 'dcim/device_inventory.html', { - 'device': device, - 'inventory_items': inventory_items, - }) - - -def device_lldp_neighbors(request, pk): - - device = get_object_or_404(Device, pk=pk) - interfaces = Interface.objects.order_naturally(device.device_type.interface_ordering).filter(device=device)\ - .select_related('connected_as_a', 'connected_as_b') - - return render(request, 'dcim/device_lldp_neighbors.html', { - 'device': device, - 'interfaces': interfaces, - }) - - # # Console ports # @@ -897,7 +930,7 @@ def consoleport_connect(request, pk): form = forms.ConsolePortConnectionForm(request.POST, instance=consoleport) if form.is_valid(): consoleport = form.save() - msg = u'Connected {} {} to {} {}'.format( + msg = 'Connected {} {} to {} {}'.format( consoleport.device.get_absolute_url(), escape(consoleport.device), escape(consoleport.name), @@ -911,9 +944,9 @@ def consoleport_connect(request, pk): else: form = forms.ConsolePortConnectionForm(instance=consoleport, initial={ - 'site': request.GET.get('site', consoleport.device.site), - 'rack': request.GET.get('rack', None), - 'console_server': request.GET.get('console_server', None), + 'site': request.GET.get('site'), + 'rack': request.GET.get('rack'), + 'console_server': request.GET.get('console_server'), 'connection_status': CONNECTION_STATUS_CONNECTED, }) @@ -931,7 +964,7 @@ def consoleport_disconnect(request, pk): if not consoleport.cs_port: messages.warning( - request, u"Cannot disconnect console port {}: It is not connected to anything.".format(consoleport) + request, "Cannot disconnect console port {}: It is not connected to anything.".format(consoleport) ) return redirect('dcim:device', pk=consoleport.device.pk) @@ -942,7 +975,7 @@ def consoleport_disconnect(request, pk): consoleport.cs_port = None consoleport.connection_status = None consoleport.save() - msg = u'Disconnected {} {} from {} {}'.format( + msg = 'Disconnected {} {} from {} {}'.format( consoleport.device.get_absolute_url(), escape(consoleport.device), escape(consoleport.name), @@ -1014,7 +1047,7 @@ def consoleserverport_connect(request, pk): consoleport.cs_port = consoleserverport consoleport.connection_status = form.cleaned_data['connection_status'] consoleport.save() - msg = u'Connected {} {} to {} {}'.format( + msg = 'Connected {} {} to {} {}'.format( consoleport.device.get_absolute_url(), escape(consoleport.device), escape(consoleport.name), @@ -1028,9 +1061,9 @@ def consoleserverport_connect(request, pk): else: form = forms.ConsoleServerPortConnectionForm(initial={ - 'site': request.GET.get('site', consoleserverport.device.site), - 'rack': request.GET.get('rack', None), - 'device': request.GET.get('device', None), + 'site': request.GET.get('site'), + 'rack': request.GET.get('rack'), + 'device': request.GET.get('device'), 'connection_status': CONNECTION_STATUS_CONNECTED, }) @@ -1048,7 +1081,7 @@ def consoleserverport_disconnect(request, pk): if not hasattr(consoleserverport, 'connected_console'): messages.warning( - request, u"Cannot disconnect console server port {}: Nothing is connected to it.".format(consoleserverport) + request, "Cannot disconnect console server port {}: Nothing is connected to it.".format(consoleserverport) ) return redirect('dcim:device', pk=consoleserverport.device.pk) @@ -1059,7 +1092,7 @@ def consoleserverport_disconnect(request, pk): consoleport.cs_port = None consoleport.connection_status = None consoleport.save() - msg = u'Disconnected {} {} from {} {}'.format( + msg = 'Disconnected {} {} from {} {}'.format( consoleport.device.get_absolute_url(), escape(consoleport.device), escape(consoleport.name), @@ -1120,7 +1153,7 @@ def powerport_connect(request, pk): form = forms.PowerPortConnectionForm(request.POST, instance=powerport) if form.is_valid(): powerport = form.save() - msg = u'Connected {} {} to {} {}'.format( + msg = 'Connected {} {} to {} {}'.format( powerport.device.get_absolute_url(), escape(powerport.device), escape(powerport.name), @@ -1134,9 +1167,9 @@ def powerport_connect(request, pk): else: form = forms.PowerPortConnectionForm(instance=powerport, initial={ - 'site': request.GET.get('site', powerport.device.site), - 'rack': request.GET.get('rack', None), - 'pdu': request.GET.get('pdu', None), + 'site': request.GET.get('site'), + 'rack': request.GET.get('rack'), + 'pdu': request.GET.get('pdu'), 'connection_status': CONNECTION_STATUS_CONNECTED, }) @@ -1154,7 +1187,7 @@ def powerport_disconnect(request, pk): if not powerport.power_outlet: messages.warning( - request, u"Cannot disconnect power port {}: It is not connected to an outlet.".format(powerport) + request, "Cannot disconnect power port {}: It is not connected to an outlet.".format(powerport) ) return redirect('dcim:device', pk=powerport.device.pk) @@ -1165,7 +1198,7 @@ def powerport_disconnect(request, pk): powerport.power_outlet = None powerport.connection_status = None powerport.save() - msg = u'Disconnected {} {} from {} {}'.format( + msg = 'Disconnected {} {} from {} {}'.format( powerport.device.get_absolute_url(), escape(powerport.device), escape(powerport.name), @@ -1237,7 +1270,7 @@ def poweroutlet_connect(request, pk): powerport.power_outlet = poweroutlet powerport.connection_status = form.cleaned_data['connection_status'] powerport.save() - msg = u'Connected {} {} to {} {}'.format( + msg = 'Connected {} {} to {} {}'.format( powerport.device.get_absolute_url(), escape(powerport.device), escape(powerport.name), @@ -1251,9 +1284,9 @@ def poweroutlet_connect(request, pk): else: form = forms.PowerOutletConnectionForm(initial={ - 'site': request.GET.get('site', poweroutlet.device.site), - 'rack': request.GET.get('rack', None), - 'device': request.GET.get('device', None), + 'site': request.GET.get('site'), + 'rack': request.GET.get('rack'), + 'device': request.GET.get('device'), 'connection_status': CONNECTION_STATUS_CONNECTED, }) @@ -1271,7 +1304,7 @@ def poweroutlet_disconnect(request, pk): if not hasattr(poweroutlet, 'connected_port'): messages.warning( - request, u"Cannot disconnect power outlet {}: Nothing is connected to it.".format(poweroutlet) + request, "Cannot disconnect power outlet {}: Nothing is connected to it.".format(poweroutlet) ) return redirect('dcim:device', pk=poweroutlet.device.pk) @@ -1282,7 +1315,7 @@ def poweroutlet_disconnect(request, pk): powerport.power_outlet = None powerport.connection_status = None powerport.save() - msg = u'Disconnected {} {} from {} {}'.format( + msg = 'Disconnected {} {} from {} {}'.format( powerport.device.get_absolute_url(), escape(powerport.device), escape(powerport.name), @@ -1396,7 +1429,7 @@ def devicebay_populate(request, pk): device_bay.save() if not form.errors: - messages.success(request, u"Added {} to {}.".format(device_bay.installed_device, device_bay)) + messages.success(request, "Added {} to {}.".format(device_bay.installed_device, device_bay)) return redirect('dcim:device', pk=device_bay.device.pk) else: @@ -1420,7 +1453,7 @@ def devicebay_depopulate(request, pk): removed_device = device_bay.installed_device device_bay.installed_device = None device_bay.save() - messages.success(request, u"{} has been removed from {}.".format(removed_device, device_bay)) + messages.success(request, "{} has been removed from {}.".format(removed_device, device_bay)) return redirect('dcim:device', pk=device_bay.device.pk) else: @@ -1483,11 +1516,11 @@ class DeviceBulkAddComponentView(View): else: for field, errors in component_form.errors.as_data().items(): for e in errors: - form.add_error(field, u'{} {}: {}'.format(device, name, ', '.join(e))) + form.add_error(field, '{} {}: {}'.format(device, name, ', '.join(e))) if not form.errors: self.model.objects.bulk_create(new_components) - messages.success(request, u"Added {} {} to {} devices.".format( + messages.success(request, "Added {} {} to {} devices.".format( len(new_components), self.model._meta.verbose_name_plural, len(form.cleaned_data['pk']) )) return redirect('dcim:device_list') @@ -1497,7 +1530,7 @@ class DeviceBulkAddComponentView(View): selected_devices = Device.objects.filter(pk__in=pk_list) if not selected_devices: - messages.warning(request, u"No devices were selected.") + messages.warning(request, "No devices were selected.") return redirect('dcim:device_list') return render(request, 'dcim/device_bulk_add_component.html', { @@ -1559,7 +1592,7 @@ def interfaceconnection_add(request, pk): if form.is_valid(): interfaceconnection = form.save() - msg = u'Connected {} {} to {} {}'.format( + msg = 'Connected {} {} to {} {}'.format( interfaceconnection.interface_a.device.get_absolute_url(), escape(interfaceconnection.interface_a.device), escape(interfaceconnection.interface_a.name), @@ -1583,11 +1616,11 @@ def interfaceconnection_add(request, pk): else: form = forms.InterfaceConnectionForm(device, initial={ - 'interface_a': request.GET.get('interface_a', None), - 'site_b': request.GET.get('site_b', device.site), - 'rack_b': request.GET.get('rack_b', None), - 'device_b': request.GET.get('device_b', None), - 'interface_b': request.GET.get('interface_b', None), + 'interface_a': request.GET.get('interface_a'), + 'site_b': request.GET.get('site_b'), + 'rack_b': request.GET.get('rack_b'), + 'device_b': request.GET.get('device_b'), + 'interface_b': request.GET.get('interface_b'), }) return render(request, 'dcim/interfaceconnection_edit.html', { @@ -1607,7 +1640,7 @@ def interfaceconnection_delete(request, pk): form = forms.InterfaceConnectionDeletionForm(request.POST) if form.is_valid(): interfaceconnection.delete() - msg = u'Disconnected {} {} from {} {}'.format( + msg = 'Disconnected {} {} from {} {}'.format( interfaceconnection.interface_a.device.get_absolute_url(), escape(interfaceconnection.interface_a.device), escape(interfaceconnection.interface_a.name), diff --git a/netbox/extras/admin.py b/netbox/extras/admin.py index 2a06b3f2f..9d396dd3d 100644 --- a/netbox/extras/admin.py +++ b/netbox/extras/admin.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from django import forms from django.contrib import admin from django.utils.safestring import mark_safe diff --git a/netbox/extras/api/customfields.py b/netbox/extras/api/customfields.py index dafed750b..5bd221893 100644 --- a/netbox/extras/api/customfields.py +++ b/netbox/extras/api/customfields.py @@ -1,9 +1,11 @@ -from django.contrib.contenttypes.models import ContentType -from django.db import transaction +from __future__ import unicode_literals from rest_framework import serializers from rest_framework.exceptions import ValidationError +from django.contrib.contenttypes.models import ContentType +from django.db import transaction + from extras.models import CF_TYPE_SELECT, CustomField, CustomFieldChoice, CustomFieldValue @@ -25,14 +27,14 @@ class CustomFieldsSerializer(serializers.BaseSerializer): # Validate custom field name if field_name not in custom_fields: - raise ValidationError(u"Invalid custom field for {} objects: {}".format(content_type, field_name)) + raise ValidationError("Invalid custom field for {} objects: {}".format(content_type, field_name)) # Validate selected choice cf = custom_fields[field_name] if cf.type == CF_TYPE_SELECT: valid_choices = [c.pk for c in cf.choices.all()] if value not in valid_choices: - raise ValidationError(u"Invalid choice ({}) for field {}".format(value, field_name)) + raise ValidationError("Invalid choice ({}) for field {}".format(value, field_name)) # Check for missing required fields missing_fields = [] @@ -40,7 +42,7 @@ class CustomFieldsSerializer(serializers.BaseSerializer): if field.required and field_name not in data: missing_fields.append(field_name) if missing_fields: - raise ValidationError(u"Missing required fields: {}".format(u", ".join(missing_fields))) + raise ValidationError("Missing required fields: {}".format(u", ".join(missing_fields))) return data diff --git a/netbox/extras/api/serializers.py b/netbox/extras/api/serializers.py index 08da93aa0..c8b3ff6f7 100644 --- a/netbox/extras/api/serializers.py +++ b/netbox/extras/api/serializers.py @@ -1,7 +1,9 @@ -from rest_framework import serializers +from __future__ import unicode_literals from django.core.exceptions import ObjectDoesNotExist +from rest_framework import serializers + from dcim.api.serializers import NestedDeviceSerializer, NestedRackSerializer, NestedSiteSerializer from dcim.models import Device, Rack, Site from extras.models import ( diff --git a/netbox/extras/api/urls.py b/netbox/extras/api/urls.py index 0f4bc2874..c5268318c 100644 --- a/netbox/extras/api/urls.py +++ b/netbox/extras/api/urls.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from rest_framework import routers from . import views diff --git a/netbox/extras/api/views.py b/netbox/extras/api/views.py index d8ef9090e..37112f2c6 100644 --- a/netbox/extras/api/views.py +++ b/netbox/extras/api/views.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from rest_framework.decorators import detail_route from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet diff --git a/netbox/extras/filters.py b/netbox/extras/filters.py index 9d9dc5f87..e44fb86e9 100644 --- a/netbox/extras/filters.py +++ b/netbox/extras/filters.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + import django_filters from django.contrib.auth.models import User diff --git a/netbox/extras/forms.py b/netbox/extras/forms.py index d85697c8d..d7a06fa5f 100644 --- a/netbox/extras/forms.py +++ b/netbox/extras/forms.py @@ -1,3 +1,4 @@ +from __future__ import unicode_literals from collections import OrderedDict from django import forms @@ -104,7 +105,7 @@ class CustomFieldForm(forms.ModelForm): obj_id=self.instance.pk) except CustomFieldValue.DoesNotExist: # Skip this field if none exists already and its value is empty - if self.cleaned_data[field_name] in [None, u'']: + if self.cleaned_data[field_name] in [None, '']: continue cfv = CustomFieldValue( field=self.fields[field_name].model, diff --git a/netbox/extras/management/commands/run_inventory.py b/netbox/extras/management/commands/run_inventory.py index c8008ed18..1e52b5c8f 100644 --- a/netbox/extras/management/commands/run_inventory.py +++ b/netbox/extras/management/commands/run_inventory.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from getpass import getpass from ncclient.transport.errors import AuthenticationError from paramiko import AuthenticationException diff --git a/netbox/extras/migrations/0007_unicode_literals.py b/netbox/extras/migrations/0007_unicode_literals.py new file mode 100644 index 000000000..c9a624510 --- /dev/null +++ b/netbox/extras/migrations/0007_unicode_literals.py @@ -0,0 +1,91 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2017-05-24 15:34 +from __future__ import unicode_literals + +from django.db import migrations, models +import extras.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('extras', '0006_add_imageattachments'), + ] + + operations = [ + migrations.AlterField( + model_name='customfield', + name='default', + field=models.CharField(blank=True, help_text='Default value for the field. Use "true" or "false" for booleans. N/A for selection fields.', max_length=100), + ), + migrations.AlterField( + model_name='customfield', + name='is_filterable', + field=models.BooleanField(default=True, help_text='This field can be used to filter objects.'), + ), + migrations.AlterField( + model_name='customfield', + name='label', + field=models.CharField(blank=True, help_text="Name of the field as displayed to users (if not provided, the field's name will be used)", max_length=50), + ), + migrations.AlterField( + model_name='customfield', + name='obj_type', + field=models.ManyToManyField(help_text='The object(s) to which this field applies.', related_name='custom_fields', to='contenttypes.ContentType', verbose_name='Object(s)'), + ), + migrations.AlterField( + model_name='customfield', + name='required', + field=models.BooleanField(default=False, help_text='Determines whether this field is required when creating new objects or editing an existing object.'), + ), + migrations.AlterField( + model_name='customfield', + name='type', + field=models.PositiveSmallIntegerField(choices=[(100, 'Text'), (200, 'Integer'), (300, 'Boolean (true/false)'), (400, 'Date'), (500, 'URL'), (600, 'Selection')], default=100), + ), + migrations.AlterField( + model_name='customfield', + name='weight', + field=models.PositiveSmallIntegerField(default=100, help_text='Fields with higher weights appear lower in a form'), + ), + migrations.AlterField( + model_name='customfieldchoice', + name='weight', + field=models.PositiveSmallIntegerField(default=100, help_text='Higher weights appear lower in the list'), + ), + migrations.AlterField( + model_name='graph', + name='link', + field=models.URLField(blank=True, verbose_name='Link URL'), + ), + migrations.AlterField( + model_name='graph', + name='name', + field=models.CharField(max_length=100, verbose_name='Name'), + ), + migrations.AlterField( + model_name='graph', + name='source', + field=models.CharField(max_length=500, verbose_name='Source URL'), + ), + migrations.AlterField( + model_name='graph', + name='type', + field=models.PositiveSmallIntegerField(choices=[(100, 'Interface'), (200, 'Provider'), (300, 'Site')]), + ), + migrations.AlterField( + model_name='imageattachment', + name='image', + field=models.ImageField(height_field='image_height', upload_to=extras.models.image_upload, width_field='image_width'), + ), + migrations.AlterField( + model_name='topologymap', + name='device_patterns', + field=models.TextField(help_text='Identify devices to include in the diagram using regular expressions, one per line. Each line will result in a new tier of the drawing. Separate multiple regexes within a line using semicolons. Devices will be rendered in the order they are defined.'), + ), + migrations.AlterField( + model_name='useraction', + name='action', + field=models.PositiveSmallIntegerField(choices=[(1, 'created'), (7, 'bulk created'), (2, 'imported'), (3, 'modified'), (4, 'bulk edited'), (5, 'deleted'), (6, 'bulk deleted')]), + ), + ] diff --git a/netbox/extras/models.py b/netbox/extras/models.py index 66d44d8a5..ea92fae0c 100644 --- a/netbox/extras/models.py +++ b/netbox/extras/models.py @@ -1,3 +1,4 @@ +from __future__ import unicode_literals from collections import OrderedDict from datetime import date import graphviz @@ -175,7 +176,7 @@ class CustomFieldValue(models.Model): unique_together = ['field', 'obj_type', 'obj_id'] def __str__(self): - return u'{} {}'.format(self.obj, self.field) + return '{} {}'.format(self.obj, self.field) @property def value(self): @@ -269,7 +270,7 @@ class ExportTemplate(models.Model): ] def __str__(self): - return u'{}: {}'.format(self.content_type, self.name) + return '{}: {}'.format(self.content_type, self.name) def to_response(self, context_dict, filename): """ @@ -387,7 +388,7 @@ def image_upload(instance, filename): elif instance.name: filename = instance.name - return u'{}{}_{}_{}'.format(path, instance.content_type.name, instance.object_id, filename) + return '{}{}_{}_{}'.format(path, instance.content_type.name, instance.object_id, filename) @python_2_unicode_compatible @@ -503,8 +504,8 @@ class UserAction(models.Model): def __str__(self): if self.message: - return u'{} {}'.format(self.user, self.message) - return u'{} {} {}'.format(self.user, self.get_action_display(), self.content_type) + return '{} {}'.format(self.user, self.message) + return '{} {} {}'.format(self.user, self.get_action_display(), self.content_type) def icon(self): if self.action in [ACTION_CREATE, ACTION_BULK_CREATE, ACTION_IMPORT]: diff --git a/netbox/extras/rpc.py b/netbox/extras/rpc.py index 208ec20dd..613bdb743 100644 --- a/netbox/extras/rpc.py +++ b/netbox/extras/rpc.py @@ -1,8 +1,10 @@ +from __future__ import unicode_literals +import re +import time + from ncclient import manager import paramiko -import re import xmltodict -import time CONNECT_TIMEOUT = 5 # seconds diff --git a/netbox/extras/tests/test_api.py b/netbox/extras/tests/test_api.py index 4c80ddee8..eddc6d71f 100644 --- a/netbox/extras/tests/test_api.py +++ b/netbox/extras/tests/test_api.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from rest_framework import status from rest_framework.test import APITestCase diff --git a/netbox/extras/tests/test_customfields.py b/netbox/extras/tests/test_customfields.py index 9e475fde8..5bbb407ce 100644 --- a/netbox/extras/tests/test_customfields.py +++ b/netbox/extras/tests/test_customfields.py @@ -1,3 +1,4 @@ +from __future__ import unicode_literals from datetime import date from rest_framework import status @@ -9,7 +10,6 @@ from django.test import TestCase from django.urls import reverse from dcim.models import Site - from extras.models import ( CustomField, CustomFieldValue, CustomFieldChoice, CF_TYPE_TEXT, CF_TYPE_INTEGER, CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_SELECT, CF_TYPE_URL, diff --git a/netbox/extras/urls.py b/netbox/extras/urls.py index 635c9edb4..f980158e8 100644 --- a/netbox/extras/urls.py +++ b/netbox/extras/urls.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from django.conf.urls import url from extras import views diff --git a/netbox/extras/views.py b/netbox/extras/views.py index 97968c62b..6469ba76a 100644 --- a/netbox/extras/views.py +++ b/netbox/extras/views.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from django.contrib.auth.mixins import PermissionRequiredMixin from django.shortcuts import get_object_or_404 diff --git a/netbox/ipam/api/serializers.py b/netbox/ipam/api/serializers.py index 7d9a5778c..096f1d232 100644 --- a/netbox/ipam/api/serializers.py +++ b/netbox/ipam/api/serializers.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from rest_framework import serializers from rest_framework.validators import UniqueTogetherValidator diff --git a/netbox/ipam/api/urls.py b/netbox/ipam/api/urls.py index b3e559675..e6b1bb13d 100644 --- a/netbox/ipam/api/urls.py +++ b/netbox/ipam/api/urls.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from rest_framework import routers from . import views diff --git a/netbox/ipam/api/views.py b/netbox/ipam/api/views.py index 611931edf..87c1996a1 100644 --- a/netbox/ipam/api/views.py +++ b/netbox/ipam/api/views.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from rest_framework.viewsets import ModelViewSet from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF diff --git a/netbox/ipam/apps.py b/netbox/ipam/apps.py index fd4af74b0..c944d1b2c 100644 --- a/netbox/ipam/apps.py +++ b/netbox/ipam/apps.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from django.apps import AppConfig diff --git a/netbox/ipam/fields.py b/netbox/ipam/fields.py index c44385b6d..a20a5dce2 100644 --- a/netbox/ipam/fields.py +++ b/netbox/ipam/fields.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from netaddr import IPNetwork from django.core.exceptions import ValidationError diff --git a/netbox/ipam/filters.py b/netbox/ipam/filters.py index 3229ad2b8..54146e91a 100644 --- a/netbox/ipam/filters.py +++ b/netbox/ipam/filters.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + import django_filters from netaddr import IPNetwork from netaddr.core import AddrFormatError @@ -8,7 +10,6 @@ from dcim.models import Site, Device, Interface from extras.filters import CustomFieldFilterSet from tenancy.models import Tenant from utilities.filters import NullableModelMultipleChoiceFilter, NumericInFilter - from .models import ( Aggregate, IPAddress, IPADDRESS_STATUS_CHOICES, Prefix, PREFIX_STATUS_CHOICES, RIR, Role, Service, VLAN, VLAN_STATUS_CHOICES, VLANGroup, VRF, diff --git a/netbox/ipam/formfields.py b/netbox/ipam/formfields.py index 914310be9..8d30e11e5 100644 --- a/netbox/ipam/formfields.py +++ b/netbox/ipam/formfields.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from netaddr import IPNetwork, AddrFormatError from django import forms diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index e45543479..3bc8124ea 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from django import forms from django.core.exceptions import ValidationError from django.db.models import Count @@ -10,7 +12,6 @@ from utilities.forms import ( APISelect, BootstrapMixin, BulkEditNullBooleanSelect, BulkImportForm, ChainedModelChoiceField, CSVDataField, ExpandableIPAddressField, FilterChoiceField, Livesearch, ReturnURLForm, SlugField, add_blank_choice, ) - from .models import ( Aggregate, IPAddress, IPADDRESS_STATUS_CHOICES, Prefix, PREFIX_STATUS_CHOICES, RIR, Role, Service, VLAN, VLANGroup, VLAN_STATUS_CHOICES, VRF, @@ -167,12 +168,21 @@ class RoleForm(BootstrapMixin, forms.ModelForm): class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldForm): site = forms.ModelChoiceField( - queryset=Site.objects.all(), required=False, label='Site', widget=forms.Select( + queryset=Site.objects.all(), + required=False, + label='Site', + widget=forms.Select( attrs={'filter-for': 'vlan', 'nullable': 'true'} ) ) vlan = ChainedModelChoiceField( - queryset=VLAN.objects.all(), chains={'site': 'site'}, required=False, label='VLAN', widget=APISelect( + queryset=VLAN.objects.all(), + chains=( + ('site', 'site'), + ), + required=False, + label='VLAN', + widget=APISelect( api_url='/api/ipam/vlans/?site_id={{site}}', display_field='display_name' ) ) @@ -270,7 +280,7 @@ def prefix_status_choices(): status_counts = {} for status in Prefix.objects.values('status').annotate(count=Count('status')).order_by('status'): status_counts[status['status']] = status['count'] - return [(s[0], u'{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in PREFIX_STATUS_CHOICES] + return [(s[0], '{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in PREFIX_STATUS_CHOICES] class PrefixFilterForm(BootstrapMixin, CustomFieldFilterForm): @@ -321,7 +331,9 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm) ) interface_rack = ChainedModelChoiceField( queryset=Rack.objects.all(), - chains={'site': 'interface_site'}, + chains=( + ('site', 'interface_site'), + ), required=False, label='Rack', widget=APISelect( @@ -332,7 +344,10 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm) ) interface_device = ChainedModelChoiceField( queryset=Device.objects.all(), - chains={'site': 'interface_site', 'rack': 'interface_rack'}, + chains=( + ('site', 'interface_site'), + ('rack', 'interface_rack'), + ), required=False, label='Device', widget=APISelect( @@ -343,7 +358,9 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm) ) interface = ChainedModelChoiceField( queryset=Interface.objects.all(), - chains={'device': 'interface_device'}, + chains=( + ('device', 'interface_device'), + ), required=False, widget=APISelect( api_url='/api/dcim/interfaces/?device_id={{interface_device}}' @@ -354,34 +371,41 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm) required=False, label='Site', widget=forms.Select( - attrs={'filter-for': 'nat_device'} + attrs={'filter-for': 'nat_rack'} ) ) nat_rack = ChainedModelChoiceField( queryset=Rack.objects.all(), - chains={'site': 'nat_site'}, + chains=( + ('site', 'nat_site'), + ), required=False, label='Rack', widget=APISelect( - api_url='/api/dcim/racks/?site_id={{interface_site}}', + api_url='/api/dcim/racks/?site_id={{nat_site}}', display_field='display_name', attrs={'filter-for': 'nat_device', 'nullable': 'true'} ) ) nat_device = ChainedModelChoiceField( queryset=Device.objects.all(), - chains={'site': 'nat_site'}, + chains=( + ('site', 'nat_site'), + ('rack', 'nat_rack'), + ), required=False, label='Device', widget=APISelect( - api_url='/api/dcim/devices/?site_id={{nat_site}}', + api_url='/api/dcim/devices/?site_id={{nat_site}}&rack_id={{nat_rack}}', display_field='display_name', attrs={'filter-for': 'nat_inside'} ) ) nat_inside = ChainedModelChoiceField( queryset=IPAddress.objects.all(), - chains={'interface__device': 'nat_device'}, + chains=( + ('interface__device', 'nat_device'), + ), required=False, label='IP Address', widget=APISelect( @@ -391,7 +415,7 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm) ) livesearch = forms.CharField( required=False, - label='IP Address', + label='Search', widget=Livesearch( query_key='q', query_url='ipam-api:ipaddress-list', @@ -404,8 +428,8 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm) class Meta: model = IPAddress fields = [ - 'address', 'vrf', 'status', 'description', 'interface', 'primary_for_device', 'nat_inside', 'tenant_group', - 'tenant', + 'address', 'vrf', 'status', 'description', 'interface', 'primary_for_device', 'nat_site', 'nat_rack', + 'nat_inside', 'tenant_group', 'tenant', ] def __init__(self, *args, **kwargs): @@ -567,7 +591,7 @@ def ipaddress_status_choices(): status_counts = {} for status in IPAddress.objects.values('status').annotate(count=Count('status')).order_by('status'): status_counts[status['status']] = status['count'] - return [(s[0], u'{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in IPADDRESS_STATUS_CHOICES] + return [(s[0], '{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in IPADDRESS_STATUS_CHOICES] class IPAddressFilterForm(BootstrapMixin, CustomFieldFilterForm): @@ -626,7 +650,9 @@ class VLANForm(BootstrapMixin, TenancyForm, CustomFieldForm): ) group = ChainedModelChoiceField( queryset=VLANGroup.objects.all(), - chains={'site': 'site'}, + chains=( + ('site', 'site'), + ), required=False, label='Group', widget=APISelect( @@ -720,7 +746,7 @@ def vlan_status_choices(): status_counts = {} for status in VLAN.objects.values('status').annotate(count=Count('status')).order_by('status'): status_counts[status['status']] = status['count'] - return [(s[0], u'{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in VLAN_STATUS_CHOICES] + return [(s[0], '{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in VLAN_STATUS_CHOICES] class VLANFilterForm(BootstrapMixin, CustomFieldFilterForm): diff --git a/netbox/ipam/lookups.py b/netbox/ipam/lookups.py index 05c69dfb2..ef5cf8327 100644 --- a/netbox/ipam/lookups.py +++ b/netbox/ipam/lookups.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from django.db.models import Lookup, Transform, IntegerField from django.db.models.lookups import BuiltinLookup diff --git a/netbox/ipam/migrations/0016_unicode_literals.py b/netbox/ipam/migrations/0016_unicode_literals.py new file mode 100644 index 000000000..bb29542ad --- /dev/null +++ b/netbox/ipam/migrations/0016_unicode_literals.py @@ -0,0 +1,133 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2017-05-24 15:34 +from __future__ import unicode_literals + +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion +import ipam.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('ipam', '0015_global_vlans'), + ] + + operations = [ + migrations.AlterField( + model_name='aggregate', + name='family', + field=models.PositiveSmallIntegerField(choices=[(4, 'IPv4'), (6, 'IPv6')]), + ), + migrations.AlterField( + model_name='aggregate', + name='rir', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='aggregates', to='ipam.RIR', verbose_name='RIR'), + ), + migrations.AlterField( + model_name='ipaddress', + name='address', + field=ipam.fields.IPAddressField(help_text='IPv4 or IPv6 address (with mask)'), + ), + migrations.AlterField( + model_name='ipaddress', + name='family', + field=models.PositiveSmallIntegerField(choices=[(4, 'IPv4'), (6, 'IPv6')], editable=False), + ), + migrations.AlterField( + model_name='ipaddress', + name='nat_inside', + field=models.OneToOneField(blank=True, help_text='The IP for which this address is the "outside" IP', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='nat_outside', to='ipam.IPAddress', verbose_name='NAT (Inside)'), + ), + migrations.AlterField( + model_name='ipaddress', + name='status', + field=models.PositiveSmallIntegerField(choices=[(1, 'Active'), (2, 'Reserved'), (3, 'Deprecated'), (5, 'DHCP')], default=1, verbose_name='Status'), + ), + migrations.AlterField( + model_name='ipaddress', + name='vrf', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='ip_addresses', to='ipam.VRF', verbose_name='VRF'), + ), + migrations.AlterField( + model_name='prefix', + name='family', + field=models.PositiveSmallIntegerField(choices=[(4, 'IPv4'), (6, 'IPv6')], editable=False), + ), + migrations.AlterField( + model_name='prefix', + name='is_pool', + field=models.BooleanField(default=False, help_text='All IP addresses within this prefix are considered usable', verbose_name='Is a pool'), + ), + migrations.AlterField( + model_name='prefix', + name='prefix', + field=ipam.fields.IPNetworkField(help_text='IPv4 or IPv6 network with mask'), + ), + migrations.AlterField( + model_name='prefix', + name='role', + field=models.ForeignKey(blank=True, help_text='The primary function of this prefix', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='prefixes', to='ipam.Role'), + ), + migrations.AlterField( + model_name='prefix', + name='status', + field=models.PositiveSmallIntegerField(choices=[(0, 'Container'), (1, 'Active'), (2, 'Reserved'), (3, 'Deprecated')], default=1, help_text='Operational status of this prefix', verbose_name='Status'), + ), + migrations.AlterField( + model_name='prefix', + name='vlan', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='prefixes', to='ipam.VLAN', verbose_name='VLAN'), + ), + migrations.AlterField( + model_name='prefix', + name='vrf', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='prefixes', to='ipam.VRF', verbose_name='VRF'), + ), + migrations.AlterField( + model_name='rir', + name='is_private', + field=models.BooleanField(default=False, help_text='IP space managed by this RIR is considered private', verbose_name='Private'), + ), + migrations.AlterField( + model_name='service', + name='device', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='services', to='dcim.Device', verbose_name='device'), + ), + migrations.AlterField( + model_name='service', + name='ipaddresses', + field=models.ManyToManyField(blank=True, related_name='services', to='ipam.IPAddress', verbose_name='IP addresses'), + ), + migrations.AlterField( + model_name='service', + name='port', + field=models.PositiveIntegerField(validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(65535)], verbose_name='Port number'), + ), + migrations.AlterField( + model_name='service', + name='protocol', + field=models.PositiveSmallIntegerField(choices=[(6, 'TCP'), (17, 'UDP')]), + ), + migrations.AlterField( + model_name='vlan', + name='status', + field=models.PositiveSmallIntegerField(choices=[(1, 'Active'), (2, 'Reserved'), (3, 'Deprecated')], default=1, verbose_name='Status'), + ), + migrations.AlterField( + model_name='vlan', + name='vid', + field=models.PositiveSmallIntegerField(validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(4094)], verbose_name='ID'), + ), + migrations.AlterField( + model_name='vrf', + name='enforce_unique', + field=models.BooleanField(default=True, help_text='Prevent duplicate prefixes/IP addresses within this VRF', verbose_name='Enforce unique space'), + ), + migrations.AlterField( + model_name='vrf', + name='rd', + field=models.CharField(max_length=21, unique=True, verbose_name='Route distinguisher'), + ), + ] diff --git a/netbox/ipam/models.py b/netbox/ipam/models.py index 980b17913..01cdd406d 100644 --- a/netbox/ipam/models.py +++ b/netbox/ipam/models.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from netaddr import IPNetwork, cidr_merge from django.conf import settings @@ -15,7 +17,6 @@ from tenancy.models import Tenant from utilities.models import CreatedUpdatedModel from utilities.sql import NullsFirstQuerySet from utilities.utils import csv_format - from .fields import IPNetworkField, IPAddressField @@ -499,7 +500,7 @@ class VLANGroup(models.Model): def __str__(self): if self.site is None: return self.name - return u'{} - {}'.format(self.site.name, self.name) + return '{} - {}'.format(self.site.name, self.name) def get_absolute_url(self): return "{}?group_id={}".format(reverse('ipam:vlan_list'), self.pk) @@ -566,7 +567,7 @@ class VLAN(CreatedUpdatedModel, CustomFieldModel): @property def display_name(self): if self.vid and self.name: - return u"{} ({})".format(self.vid, self.name) + return "{} ({})".format(self.vid, self.name) return None def get_status_class(self): @@ -593,4 +594,4 @@ class Service(CreatedUpdatedModel): unique_together = ['device', 'protocol', 'port'] def __str__(self): - return u'{} ({}/{})'.format(self.name, self.port, self.get_protocol_display()) + return '{} ({}/{})'.format(self.name, self.port, self.get_protocol_display()) diff --git a/netbox/ipam/tables.py b/netbox/ipam/tables.py index 562713f5b..64b7d62fa 100644 --- a/netbox/ipam/tables.py +++ b/netbox/ipam/tables.py @@ -1,8 +1,9 @@ +from __future__ import unicode_literals + import django_tables2 as tables from django_tables2.utils import Accessor from utilities.tables import BaseTable, SearchTable, ToggleColumn - from .models import Aggregate, IPAddress, Prefix, RIR, Role, VLAN, VLANGroup, VRF diff --git a/netbox/ipam/tests/test_api.py b/netbox/ipam/tests/test_api.py index cd58f865b..0b6814b4a 100644 --- a/netbox/ipam/tests/test_api.py +++ b/netbox/ipam/tests/test_api.py @@ -1,5 +1,6 @@ -from netaddr import IPNetwork +from __future__ import unicode_literals +from netaddr import IPNetwork from rest_framework import status from rest_framework.test import APITestCase diff --git a/netbox/ipam/tests/test_models.py b/netbox/ipam/tests/test_models.py index 3385c643f..0f75cc795 100644 --- a/netbox/ipam/tests/test_models.py +++ b/netbox/ipam/tests/test_models.py @@ -1,9 +1,11 @@ +from __future__ import unicode_literals + import netaddr +from django.core.exceptions import ValidationError from django.test import TestCase, override_settings from ipam.models import IPAddress, Prefix, VRF -from django.core.exceptions import ValidationError class TestPrefix(TestCase): diff --git a/netbox/ipam/urls.py b/netbox/ipam/urls.py index a8d7d4528..d28bf8a13 100644 --- a/netbox/ipam/urls.py +++ b/netbox/ipam/urls.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from django.conf.urls import url from . import views @@ -12,7 +14,7 @@ urlpatterns = [ url(r'^vrfs/import/$', views.VRFBulkImportView.as_view(), name='vrf_import'), url(r'^vrfs/edit/$', views.VRFBulkEditView.as_view(), name='vrf_bulk_edit'), url(r'^vrfs/delete/$', views.VRFBulkDeleteView.as_view(), name='vrf_bulk_delete'), - url(r'^vrfs/(?P\d+)/$', views.vrf, name='vrf'), + url(r'^vrfs/(?P\d+)/$', views.VRFView.as_view(), name='vrf'), url(r'^vrfs/(?P\d+)/edit/$', views.VRFEditView.as_view(), name='vrf_edit'), url(r'^vrfs/(?P\d+)/delete/$', views.VRFDeleteView.as_view(), name='vrf_delete'), @@ -28,7 +30,7 @@ urlpatterns = [ url(r'^aggregates/import/$', views.AggregateBulkImportView.as_view(), name='aggregate_import'), url(r'^aggregates/edit/$', views.AggregateBulkEditView.as_view(), name='aggregate_bulk_edit'), url(r'^aggregates/delete/$', views.AggregateBulkDeleteView.as_view(), name='aggregate_bulk_delete'), - url(r'^aggregates/(?P\d+)/$', views.aggregate, name='aggregate'), + url(r'^aggregates/(?P\d+)/$', views.AggregateView.as_view(), name='aggregate'), url(r'^aggregates/(?P\d+)/edit/$', views.AggregateEditView.as_view(), name='aggregate_edit'), url(r'^aggregates/(?P\d+)/delete/$', views.AggregateDeleteView.as_view(), name='aggregate_delete'), @@ -44,10 +46,10 @@ urlpatterns = [ url(r'^prefixes/import/$', views.PrefixBulkImportView.as_view(), name='prefix_import'), url(r'^prefixes/edit/$', views.PrefixBulkEditView.as_view(), name='prefix_bulk_edit'), url(r'^prefixes/delete/$', views.PrefixBulkDeleteView.as_view(), name='prefix_bulk_delete'), - url(r'^prefixes/(?P\d+)/$', views.prefix, name='prefix'), + url(r'^prefixes/(?P\d+)/$', views.PrefixView.as_view(), name='prefix'), url(r'^prefixes/(?P\d+)/edit/$', views.PrefixEditView.as_view(), name='prefix_edit'), url(r'^prefixes/(?P\d+)/delete/$', views.PrefixDeleteView.as_view(), name='prefix_delete'), - url(r'^prefixes/(?P\d+)/ip-addresses/$', views.prefix_ipaddresses, name='prefix_ipaddresses'), + url(r'^prefixes/(?P\d+)/ip-addresses/$', views.PrefixIPAddressesView.as_view(), name='prefix_ipaddresses'), # IP addresses url(r'^ip-addresses/$', views.IPAddressListView.as_view(), name='ipaddress_list'), @@ -56,7 +58,7 @@ urlpatterns = [ url(r'^ip-addresses/import/$', views.IPAddressBulkImportView.as_view(), name='ipaddress_import'), url(r'^ip-addresses/edit/$', views.IPAddressBulkEditView.as_view(), name='ipaddress_bulk_edit'), url(r'^ip-addresses/delete/$', views.IPAddressBulkDeleteView.as_view(), name='ipaddress_bulk_delete'), - url(r'^ip-addresses/(?P\d+)/$', views.ipaddress, name='ipaddress'), + url(r'^ip-addresses/(?P\d+)/$', views.IPAddressView.as_view(), name='ipaddress'), url(r'^ip-addresses/(?P\d+)/edit/$', views.IPAddressEditView.as_view(), name='ipaddress_edit'), url(r'^ip-addresses/(?P\d+)/delete/$', views.IPAddressDeleteView.as_view(), name='ipaddress_delete'), @@ -72,7 +74,7 @@ urlpatterns = [ url(r'^vlans/import/$', views.VLANBulkImportView.as_view(), name='vlan_import'), url(r'^vlans/edit/$', views.VLANBulkEditView.as_view(), name='vlan_bulk_edit'), url(r'^vlans/delete/$', views.VLANBulkDeleteView.as_view(), name='vlan_bulk_delete'), - url(r'^vlans/(?P\d+)/$', views.vlan, name='vlan'), + url(r'^vlans/(?P\d+)/$', views.VLANView.as_view(), name='vlan'), url(r'^vlans/(?P\d+)/edit/$', views.VLANEditView.as_view(), name='vlan_edit'), url(r'^vlans/(?P\d+)/delete/$', views.VLANDeleteView.as_view(), name='vlan_delete'), diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index 82e5f8331..a51f47b6e 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from django_tables2 import RequestConfig import netaddr @@ -6,13 +8,13 @@ from django.contrib.auth.mixins import PermissionRequiredMixin from django.db.models import Count, Q from django.shortcuts import get_object_or_404, render from django.urls import reverse +from django.views.generic import View from dcim.models import Device from utilities.paginator import EnhancedPaginator from utilities.views import ( BulkAddView, BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView, ) - from . import filters, forms, tables from .models import ( Aggregate, IPAddress, PREFIX_STATUS_ACTIVE, PREFIX_STATUS_DEPRECATED, PREFIX_STATUS_RESERVED, Prefix, RIR, Role, @@ -96,18 +98,20 @@ class VRFListView(ObjectListView): template_name = 'ipam/vrf_list.html' -def vrf(request, pk): +class VRFView(View): - vrf = get_object_or_404(VRF.objects.all(), pk=pk) - prefix_table = tables.PrefixBriefTable( - list(Prefix.objects.filter(vrf=vrf).select_related('site', 'role')) - ) - prefix_table.exclude = ('vrf',) + def get(self, request, pk): - return render(request, 'ipam/vrf.html', { - 'vrf': vrf, - 'prefix_table': prefix_table, - }) + vrf = get_object_or_404(VRF.objects.all(), pk=pk) + prefix_table = tables.PrefixBriefTable( + list(Prefix.objects.filter(vrf=vrf).select_related('site', 'role')) + ) + prefix_table.exclude = ('vrf',) + + return render(request, 'ipam/vrf.html', { + 'vrf': vrf, + 'prefix_table': prefix_table, + }) class VRFEditView(PermissionRequiredMixin, ObjectEditView): @@ -281,37 +285,44 @@ class AggregateListView(ObjectListView): } -def aggregate(request, pk): +class AggregateView(View): - aggregate = get_object_or_404(Aggregate, pk=pk) + def get(self, request, pk): - # Find all child prefixes contained by this aggregate - child_prefixes = Prefix.objects.filter(prefix__net_contained_or_equal=str(aggregate.prefix))\ - .select_related('site', 'role').annotate_depth(limit=0) - child_prefixes = add_available_prefixes(aggregate.prefix, child_prefixes) + aggregate = get_object_or_404(Aggregate, pk=pk) - prefix_table = tables.PrefixTable(child_prefixes) - if request.user.has_perm('ipam.change_prefix') or request.user.has_perm('ipam.delete_prefix'): - prefix_table.base_columns['pk'].visible = True + # Find all child prefixes contained by this aggregate + child_prefixes = Prefix.objects.filter( + prefix__net_contained_or_equal=str(aggregate.prefix) + ).select_related( + 'site', 'role' + ).annotate_depth( + limit=0 + ) + child_prefixes = add_available_prefixes(aggregate.prefix, child_prefixes) - paginate = { - 'klass': EnhancedPaginator, - 'per_page': request.GET.get('per_page', settings.PAGINATE_COUNT) - } - RequestConfig(request, paginate).configure(prefix_table) + prefix_table = tables.PrefixTable(child_prefixes) + if request.user.has_perm('ipam.change_prefix') or request.user.has_perm('ipam.delete_prefix'): + prefix_table.base_columns['pk'].visible = True - # Compile permissions list for rendering the object table - permissions = { - 'add': request.user.has_perm('ipam.add_prefix'), - 'change': request.user.has_perm('ipam.change_prefix'), - 'delete': request.user.has_perm('ipam.delete_prefix'), - } + paginate = { + 'klass': EnhancedPaginator, + 'per_page': request.GET.get('per_page', settings.PAGINATE_COUNT) + } + RequestConfig(request, paginate).configure(prefix_table) - return render(request, 'ipam/aggregate.html', { - 'aggregate': aggregate, - 'prefix_table': prefix_table, - 'permissions': permissions, - }) + # Compile permissions list for rendering the object table + permissions = { + 'add': request.user.has_perm('ipam.add_prefix'), + 'change': request.user.has_perm('ipam.change_prefix'), + 'delete': request.user.has_perm('ipam.delete_prefix'), + } + + return render(request, 'ipam/aggregate.html', { + 'aggregate': aggregate, + 'prefix_table': prefix_table, + 'permissions': permissions, + }) class AggregateEditView(PermissionRequiredMixin, ObjectEditView): @@ -394,66 +405,120 @@ class PrefixListView(ObjectListView): return self.queryset.annotate_depth(limit=limit) -def prefix(request, pk): +class PrefixView(View): - prefix = get_object_or_404(Prefix.objects.select_related( - 'vrf', 'site__region', 'tenant__group', 'vlan__group', 'role' - ), pk=pk) + def get(self, request, pk): - try: - aggregate = Aggregate.objects.get(prefix__net_contains_or_equals=str(prefix.prefix)) - except Aggregate.DoesNotExist: - aggregate = None + prefix = get_object_or_404(Prefix.objects.select_related( + 'vrf', 'site__region', 'tenant__group', 'vlan__group', 'role' + ), pk=pk) - # Count child IP addresses - ipaddress_count = IPAddress.objects.filter(vrf=prefix.vrf, address__net_host_contained=str(prefix.prefix))\ - .count() + try: + aggregate = Aggregate.objects.get(prefix__net_contains_or_equals=str(prefix.prefix)) + except Aggregate.DoesNotExist: + aggregate = None - # Parent prefixes table - parent_prefixes = Prefix.objects.filter(Q(vrf=prefix.vrf) | Q(vrf__isnull=True))\ - .filter(prefix__net_contains=str(prefix.prefix))\ - .select_related('site', 'role').annotate_depth() - parent_prefix_table = tables.PrefixBriefTable(parent_prefixes) - parent_prefix_table.exclude = ('vrf',) + # Count child IP addresses + ipaddress_count = IPAddress.objects.filter( + vrf=prefix.vrf, address__net_host_contained=str(prefix.prefix) + ).count() - # Duplicate prefixes table - duplicate_prefixes = Prefix.objects.filter(vrf=prefix.vrf, prefix=str(prefix.prefix)).exclude(pk=prefix.pk)\ - .select_related('site', 'role') - duplicate_prefix_table = tables.PrefixBriefTable(list(duplicate_prefixes)) - duplicate_prefix_table.exclude = ('vrf',) + # Parent prefixes table + parent_prefixes = Prefix.objects.filter( + Q(vrf=prefix.vrf) | Q(vrf__isnull=True) + ).filter( + prefix__net_contains=str(prefix.prefix) + ).select_related( + 'site', 'role' + ).annotate_depth() + parent_prefix_table = tables.PrefixBriefTable(parent_prefixes) + parent_prefix_table.exclude = ('vrf',) - # Child prefixes table - child_prefixes = Prefix.objects.filter(vrf=prefix.vrf, prefix__net_contained=str(prefix.prefix))\ - .select_related('site', 'role').annotate_depth(limit=0) - if child_prefixes: - child_prefixes = add_available_prefixes(prefix.prefix, child_prefixes) - child_prefix_table = tables.PrefixTable(child_prefixes) - if request.user.has_perm('ipam.change_prefix') or request.user.has_perm('ipam.delete_prefix'): - child_prefix_table.base_columns['pk'].visible = True + # Duplicate prefixes table + duplicate_prefixes = Prefix.objects.filter( + vrf=prefix.vrf, prefix=str(prefix.prefix) + ).exclude( + pk=prefix.pk + ).select_related( + 'site', 'role' + ) + duplicate_prefix_table = tables.PrefixBriefTable(list(duplicate_prefixes)) + duplicate_prefix_table.exclude = ('vrf',) - paginate = { - 'klass': EnhancedPaginator, - 'per_page': request.GET.get('per_page', settings.PAGINATE_COUNT) - } - RequestConfig(request, paginate).configure(child_prefix_table) + # Child prefixes table + child_prefixes = Prefix.objects.filter( + vrf=prefix.vrf, prefix__net_contained=str(prefix.prefix) + ).select_related( + 'site', 'role' + ).annotate_depth(limit=0) + if child_prefixes: + child_prefixes = add_available_prefixes(prefix.prefix, child_prefixes) + child_prefix_table = tables.PrefixTable(child_prefixes) + if request.user.has_perm('ipam.change_prefix') or request.user.has_perm('ipam.delete_prefix'): + child_prefix_table.base_columns['pk'].visible = True - # Compile permissions list for rendering the object table - permissions = { - 'add': request.user.has_perm('ipam.add_prefix'), - 'change': request.user.has_perm('ipam.change_prefix'), - 'delete': request.user.has_perm('ipam.delete_prefix'), - } + paginate = { + 'klass': EnhancedPaginator, + 'per_page': request.GET.get('per_page', settings.PAGINATE_COUNT) + } + RequestConfig(request, paginate).configure(child_prefix_table) - return render(request, 'ipam/prefix.html', { - 'prefix': prefix, - 'aggregate': aggregate, - 'ipaddress_count': ipaddress_count, - 'parent_prefix_table': parent_prefix_table, - 'child_prefix_table': child_prefix_table, - 'duplicate_prefix_table': duplicate_prefix_table, - 'permissions': permissions, - 'return_url': prefix.get_absolute_url(), - }) + # Compile permissions list for rendering the object table + permissions = { + 'add': request.user.has_perm('ipam.add_prefix'), + 'change': request.user.has_perm('ipam.change_prefix'), + 'delete': request.user.has_perm('ipam.delete_prefix'), + } + + return render(request, 'ipam/prefix.html', { + 'prefix': prefix, + 'aggregate': aggregate, + 'ipaddress_count': ipaddress_count, + 'parent_prefix_table': parent_prefix_table, + 'child_prefix_table': child_prefix_table, + 'duplicate_prefix_table': duplicate_prefix_table, + 'permissions': permissions, + 'return_url': prefix.get_absolute_url(), + }) + + +class PrefixIPAddressesView(View): + + def get(self, request, pk): + + prefix = get_object_or_404(Prefix.objects.all(), pk=pk) + + # Find all IPAddresses belonging to this Prefix + ipaddresses = IPAddress.objects.filter( + vrf=prefix.vrf, address__net_host_contained=str(prefix.prefix) + ).select_related( + 'vrf', 'interface__device', 'primary_ip4_for', 'primary_ip6_for' + ) + ipaddresses = add_available_ipaddresses(prefix.prefix, ipaddresses, prefix.is_pool) + + ip_table = tables.IPAddressTable(ipaddresses) + if request.user.has_perm('ipam.change_ipaddress') or request.user.has_perm('ipam.delete_ipaddress'): + ip_table.base_columns['pk'].visible = True + + paginate = { + 'klass': EnhancedPaginator, + 'per_page': request.GET.get('per_page', settings.PAGINATE_COUNT) + } + RequestConfig(request, paginate).configure(ip_table) + + # Compile permissions list for rendering the object table + permissions = { + 'add': request.user.has_perm('ipam.add_ipaddress'), + 'change': request.user.has_perm('ipam.change_ipaddress'), + 'delete': request.user.has_perm('ipam.delete_ipaddress'), + } + + return render(request, 'ipam/prefix_ipaddresses.html', { + 'prefix': prefix, + 'ip_table': ip_table, + 'permissions': permissions, + 'bulk_querystring': 'vrf_id={}&parent={}'.format(prefix.vrf or '0', prefix.prefix), + }) class PrefixEditView(PermissionRequiredMixin, ObjectEditView): @@ -495,40 +560,6 @@ class PrefixBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): default_return_url = 'ipam:prefix_list' -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(vrf=prefix.vrf, address__net_host_contained=str(prefix.prefix))\ - .select_related('vrf', 'interface__device', 'primary_ip4_for', 'primary_ip6_for') - ipaddresses = add_available_ipaddresses(prefix.prefix, ipaddresses, prefix.is_pool) - - ip_table = tables.IPAddressTable(ipaddresses) - if request.user.has_perm('ipam.change_ipaddress') or request.user.has_perm('ipam.delete_ipaddress'): - ip_table.base_columns['pk'].visible = True - - paginate = { - 'klass': EnhancedPaginator, - 'per_page': request.GET.get('per_page', settings.PAGINATE_COUNT) - } - RequestConfig(request, paginate).configure(ip_table) - - # Compile permissions list for rendering the object table - permissions = { - 'add': request.user.has_perm('ipam.add_ipaddress'), - 'change': request.user.has_perm('ipam.change_ipaddress'), - 'delete': request.user.has_perm('ipam.delete_ipaddress'), - } - - return render(request, 'ipam/prefix_ipaddresses.html', { - 'prefix': prefix, - 'ip_table': ip_table, - 'permissions': permissions, - 'bulk_querystring': 'vrf_id={}&parent={}'.format(prefix.vrf or '0', prefix.prefix), - }) - - # # IP addresses # @@ -541,32 +572,47 @@ class IPAddressListView(ObjectListView): template_name = 'ipam/ipaddress_list.html' -def ipaddress(request, pk): +class IPAddressView(View): - ipaddress = get_object_or_404(IPAddress.objects.select_related('interface__device'), pk=pk) + def get(self, request, pk): - # Parent prefixes table - parent_prefixes = Prefix.objects.filter(vrf=ipaddress.vrf, prefix__net_contains=str(ipaddress.address.ip))\ - .select_related('site', 'role') - parent_prefixes_table = tables.PrefixBriefTable(list(parent_prefixes)) - parent_prefixes_table.exclude = ('vrf',) + ipaddress = get_object_or_404(IPAddress.objects.select_related('interface__device'), pk=pk) - # Duplicate IPs table - duplicate_ips = IPAddress.objects.filter(vrf=ipaddress.vrf, address=str(ipaddress.address))\ - .exclude(pk=ipaddress.pk).select_related('interface__device', 'nat_inside') - duplicate_ips_table = tables.IPAddressBriefTable(list(duplicate_ips)) + # Parent prefixes table + parent_prefixes = Prefix.objects.filter( + vrf=ipaddress.vrf, prefix__net_contains=str(ipaddress.address.ip) + ).select_related( + 'site', 'role' + ) + parent_prefixes_table = tables.PrefixBriefTable(list(parent_prefixes)) + parent_prefixes_table.exclude = ('vrf',) - # Related IP table - related_ips = IPAddress.objects.select_related('interface__device').exclude(address=str(ipaddress.address))\ - .filter(vrf=ipaddress.vrf, address__net_contained_or_equal=str(ipaddress.address)) - related_ips_table = tables.IPAddressBriefTable(list(related_ips)) + # Duplicate IPs table + duplicate_ips = IPAddress.objects.filter( + vrf=ipaddress.vrf, address=str(ipaddress.address) + ).exclude( + pk=ipaddress.pk + ).select_related( + 'interface__device', 'nat_inside' + ) + duplicate_ips_table = tables.IPAddressBriefTable(list(duplicate_ips)) - return render(request, 'ipam/ipaddress.html', { - 'ipaddress': ipaddress, - 'parent_prefixes_table': parent_prefixes_table, - 'duplicate_ips_table': duplicate_ips_table, - 'related_ips_table': related_ips_table, - }) + # Related IP table + related_ips = IPAddress.objects.select_related( + 'interface__device' + ).exclude( + address=str(ipaddress.address) + ).filter( + vrf=ipaddress.vrf, address__net_contained_or_equal=str(ipaddress.address) + ) + related_ips_table = tables.IPAddressBriefTable(list(related_ips)) + + return render(request, 'ipam/ipaddress.html', { + 'ipaddress': ipaddress, + 'parent_prefixes_table': parent_prefixes_table, + 'duplicate_ips_table': duplicate_ips_table, + 'related_ips_table': related_ips_table, + }) class IPAddressEditView(PermissionRequiredMixin, ObjectEditView): @@ -669,17 +715,21 @@ class VLANListView(ObjectListView): template_name = 'ipam/vlan_list.html' -def vlan(request, pk): +class VLANView(View): - vlan = get_object_or_404(VLAN.objects.select_related('site__region', 'tenant__group', 'role'), pk=pk) - prefixes = Prefix.objects.filter(vlan=vlan).select_related('vrf', 'site', 'role') - prefix_table = tables.PrefixBriefTable(list(prefixes)) - prefix_table.exclude = ('vlan',) + def get(self, request, pk): - return render(request, 'ipam/vlan.html', { - 'vlan': vlan, - 'prefix_table': prefix_table, - }) + vlan = get_object_or_404(VLAN.objects.select_related( + 'site__region', 'tenant__group', 'role' + ), pk=pk) + prefixes = Prefix.objects.filter(vlan=vlan).select_related('vrf', 'site', 'role') + prefix_table = tables.PrefixBriefTable(list(prefixes)) + prefix_table.exclude = ('vlan',) + + return render(request, 'ipam/vlan.html', { + 'vlan': vlan, + 'prefix_table': prefix_table, + }) class VLANEditView(PermissionRequiredMixin, ObjectEditView): diff --git a/netbox/netbox/forms.py b/netbox/netbox/forms.py index 63af2e04b..85343ec77 100644 --- a/netbox/netbox/forms.py +++ b/netbox/netbox/forms.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from django import forms from utilities.forms import BootstrapMixin diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 82c4554f0..c6d48454a 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -13,7 +13,7 @@ except ImportError: ) -VERSION = '2.0.3' +VERSION = '2.0.4' # Import local configuration ALLOWED_HOSTS = DATABASE = SECRET_KEY = None @@ -112,6 +112,7 @@ INSTALLED_APPS = ( 'django.contrib.humanize', 'corsheaders', 'debug_toolbar', + 'django_filters', 'django_tables2', 'mptt', 'rest_framework', @@ -180,8 +181,8 @@ STATICFILES_DIRS = ( ) # Media -MEDIA_URL = '/media/' MEDIA_ROOT = os.path.join(BASE_DIR, 'media') +MEDIA_URL = '/{}media/'.format(BASE_PATH) # Disable default limit of 1000 fields per request. Needed for bulk deletion of objects. (Added in Django 1.10.) DATA_UPLOAD_MAX_NUMBER_FIELDS = None diff --git a/netbox/netbox/urls.py b/netbox/netbox/urls.py index 7e3ab8924..ddddf27a2 100644 --- a/netbox/netbox/urls.py +++ b/netbox/netbox/urls.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from rest_framework_swagger.views import get_swagger_view from django.conf import settings @@ -5,8 +7,8 @@ from django.conf.urls import include, url from django.contrib import admin from django.views.static import serve -from netbox.views import APIRootView, home, handle_500, SearchView, trigger_500 -from users.views import login, logout +from netbox.views import APIRootView, handle_500, HomeView, SearchView, trigger_500 +from users.views import LoginView, LogoutView handler500 = handle_500 @@ -15,12 +17,12 @@ swagger_view = get_swagger_view(title='NetBox API') _patterns = [ # Base views - url(r'^$', home, name='home'), + url(r'^$', HomeView.as_view(), name='home'), url(r'^search/$', SearchView.as_view(), name='search'), # Login/logout - url(r'^login/$', login, name='login'), - url(r'^logout/$', logout, name='logout'), + url(r'^login/$', LoginView.as_view(), name='login'), + url(r'^logout/$', LogoutView.as_view(), name='logout'), # Apps url(r'^circuits/', include('circuits.urls')), diff --git a/netbox/netbox/views.py b/netbox/netbox/views.py index f602a46f4..6f642063b 100644 --- a/netbox/netbox/views.py +++ b/netbox/netbox/views.py @@ -1,3 +1,4 @@ +from __future__ import unicode_literals from collections import OrderedDict import sys @@ -115,43 +116,46 @@ SEARCH_TYPES = OrderedDict(( )) -def home(request): +class HomeView(View): + template_name = 'home.html' - stats = { + def get(self, request): - # Organization - 'site_count': Site.objects.count(), - 'tenant_count': Tenant.objects.count(), + stats = { - # DCIM - 'rack_count': Rack.objects.count(), - 'device_count': Device.objects.count(), - 'interface_connections_count': InterfaceConnection.objects.count(), - 'console_connections_count': ConsolePort.objects.filter(cs_port__isnull=False).count(), - 'power_connections_count': PowerPort.objects.filter(power_outlet__isnull=False).count(), + # Organization + 'site_count': Site.objects.count(), + 'tenant_count': Tenant.objects.count(), - # IPAM - 'vrf_count': VRF.objects.count(), - 'aggregate_count': Aggregate.objects.count(), - 'prefix_count': Prefix.objects.count(), - 'ipaddress_count': IPAddress.objects.count(), - 'vlan_count': VLAN.objects.count(), + # DCIM + 'rack_count': Rack.objects.count(), + 'device_count': Device.objects.count(), + 'interface_connections_count': InterfaceConnection.objects.count(), + 'console_connections_count': ConsolePort.objects.filter(cs_port__isnull=False).count(), + 'power_connections_count': PowerPort.objects.filter(power_outlet__isnull=False).count(), - # Circuits - 'provider_count': Provider.objects.count(), - 'circuit_count': Circuit.objects.count(), + # IPAM + 'vrf_count': VRF.objects.count(), + 'aggregate_count': Aggregate.objects.count(), + 'prefix_count': Prefix.objects.count(), + 'ipaddress_count': IPAddress.objects.count(), + 'vlan_count': VLAN.objects.count(), - # Secrets - 'secret_count': Secret.objects.count(), + # Circuits + 'provider_count': Provider.objects.count(), + 'circuit_count': Circuit.objects.count(), - } + # Secrets + 'secret_count': Secret.objects.count(), - return render(request, 'home.html', { - 'search_form': SearchForm(), - 'stats': stats, - 'topology_maps': TopologyMap.objects.filter(site__isnull=True), - 'recent_activity': UserAction.objects.select_related('user')[:50] - }) + } + + return render(request, self.template_name, { + 'search_form': SearchForm(), + 'stats': stats, + 'topology_maps': TopologyMap.objects.filter(site__isnull=True), + 'recent_activity': UserAction.objects.select_related('user')[:50] + }) class SearchView(View): @@ -192,7 +196,7 @@ class SearchView(View): results.append({ 'name': queryset.model._meta.verbose_name_plural, 'table': table, - 'url': u'{}?q={}'.format(reverse(url), form.cleaned_data['q']) + 'url': '{}?q={}'.format(reverse(url), form.cleaned_data['q']) }) return render(request, 'search.html', { @@ -206,7 +210,7 @@ class APIRootView(APIView): exclude_from_schema = True def get_view_name(self): - return u"API Root" + return "API Root" def get(self, request, format=None): @@ -235,5 +239,6 @@ def trigger_500(request): """ Hot-wired method of triggering a server error to test reporting """ - raise Exception("Congratulations, you've triggered an exception! Go tell all your friends what an exceptional " - "person you are.") + raise Exception( + "Congratulations, you've triggered an exception! Go tell all your friends what an exceptional person you are." + ) diff --git a/netbox/netbox/wsgi.py b/netbox/netbox/wsgi.py index 7fac23c61..6d13ffe9d 100644 --- a/netbox/netbox/wsgi.py +++ b/netbox/netbox/wsgi.py @@ -11,6 +11,7 @@ import os from django.core.wsgi import get_wsgi_application + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "netbox.settings") application = get_wsgi_application() diff --git a/netbox/secrets/admin.py b/netbox/secrets/admin.py index 990f3ffc7..3780dccd2 100644 --- a/netbox/secrets/admin.py +++ b/netbox/secrets/admin.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from django.contrib import admin, messages from django.shortcuts import redirect, render @@ -34,8 +36,8 @@ class UserKeyAdmin(admin.ModelAdmin): try: my_userkey = UserKey.objects.get(user=request.user) except UserKey.DoesNotExist: - messages.error(request, u"You do not have an active User Key.") - return redirect('/admin/secrets/userkey/') + messages.error(request, "You do not have an active User Key.") + return redirect('admin:secrets_userkey_changelist') if 'activate' in request.POST: form = ActivateUserKeyForm(request.POST) @@ -44,9 +46,9 @@ class UserKeyAdmin(admin.ModelAdmin): master_key = my_userkey.get_master_key(form.cleaned_data['secret_key']) for uk in form.cleaned_data['_selected_action']: uk.activate(master_key) - return redirect('/admin/secrets/userkey/') + return redirect('admin:secrets_userkey_changelist') except ValueError: - messages.error(request, u"Invalid private key provided. Unable to retrieve master key.") + messages.error(request, "Invalid private key provided. Unable to retrieve master key.") else: form = ActivateUserKeyForm(initial={'_selected_action': request.POST.getlist(admin.ACTION_CHECKBOX_NAME)}) diff --git a/netbox/secrets/api/serializers.py b/netbox/secrets/api/serializers.py index 1dc52388a..3c7132d37 100644 --- a/netbox/secrets/api/serializers.py +++ b/netbox/secrets/api/serializers.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from rest_framework import serializers from rest_framework.validators import UniqueTogetherValidator diff --git a/netbox/secrets/api/urls.py b/netbox/secrets/api/urls.py index 5f60a8a06..3b1e7d3d0 100644 --- a/netbox/secrets/api/urls.py +++ b/netbox/secrets/api/urls.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from rest_framework import routers from . import views diff --git a/netbox/secrets/api/views.py b/netbox/secrets/api/views.py index 0e6314e17..edc165aa0 100644 --- a/netbox/secrets/api/views.py +++ b/netbox/secrets/api/views.py @@ -1,13 +1,14 @@ +from __future__ import unicode_literals import base64 + from Crypto.PublicKey import RSA - -from django.http import HttpResponseBadRequest - from rest_framework.exceptions import ValidationError from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.viewsets import ModelViewSet, ViewSet +from django.http import HttpResponseBadRequest + from secrets import filters from secrets.exceptions import InvalidKey from secrets.models import Secret, SecretRole, SessionKey, UserKey diff --git a/netbox/secrets/decorators.py b/netbox/secrets/decorators.py index 683805124..0b9ebc16e 100644 --- a/netbox/secrets/decorators.py +++ b/netbox/secrets/decorators.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from django.contrib import messages from django.shortcuts import redirect @@ -14,10 +16,10 @@ def userkey_required(): try: uk = UserKey.objects.get(user=request.user) except UserKey.DoesNotExist: - messages.warning(request, u"This operation requires an active user key, but you don't have one.") + messages.warning(request, "This operation requires an active user key, but you don't have one.") return redirect('user:userkey') if not uk.is_active(): - messages.warning(request, u"This operation is not available. Your user key has not been activated.") + messages.warning(request, "This operation is not available. Your user key has not been activated.") return redirect('user:userkey') return view(request, *args, **kwargs) return wrapped_view diff --git a/netbox/secrets/exceptions.py b/netbox/secrets/exceptions.py index c163e5907..f014d8a14 100644 --- a/netbox/secrets/exceptions.py +++ b/netbox/secrets/exceptions.py @@ -1,3 +1,6 @@ +from __future__ import unicode_literals + + class InvalidKey(Exception): """ Raised when a provided key is invalid. diff --git a/netbox/secrets/filters.py b/netbox/secrets/filters.py index 14ebd1616..4bc7b56cd 100644 --- a/netbox/secrets/filters.py +++ b/netbox/secrets/filters.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + import django_filters from django.db.models import Q diff --git a/netbox/secrets/forms.py b/netbox/secrets/forms.py index 65e369376..b8e165804 100644 --- a/netbox/secrets/forms.py +++ b/netbox/secrets/forms.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from Crypto.Cipher import PKCS1_OAEP from Crypto.PublicKey import RSA @@ -6,7 +8,6 @@ from django.db.models import Count from dcim.models import Device from utilities.forms import BootstrapMixin, BulkEditForm, BulkImportForm, CSVDataField, FilterChoiceField, SlugField - from .models import Secret, SecretRole, UserKey diff --git a/netbox/secrets/hashers.py b/netbox/secrets/hashers.py index fc5066fc6..49da1605d 100644 --- a/netbox/secrets/hashers.py +++ b/netbox/secrets/hashers.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from django.contrib.auth.hashers import PBKDF2PasswordHasher diff --git a/netbox/secrets/migrations/0003_unicode_literals.py b/netbox/secrets/migrations/0003_unicode_literals.py new file mode 100644 index 000000000..b8b7956d8 --- /dev/null +++ b/netbox/secrets/migrations/0003_unicode_literals.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2017-05-24 15:34 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('secrets', '0002_userkey_add_session_key'), + ] + + operations = [ + migrations.AlterField( + model_name='userkey', + name='public_key', + field=models.TextField(verbose_name='RSA public key'), + ), + ] diff --git a/netbox/secrets/models.py b/netbox/secrets/models.py index e39f1832b..bf423fdf6 100644 --- a/netbox/secrets/models.py +++ b/netbox/secrets/models.py @@ -1,4 +1,6 @@ +from __future__ import unicode_literals import os + from Crypto.Cipher import AES, PKCS1_OAEP, XOR from Crypto.PublicKey import RSA @@ -12,7 +14,6 @@ from django.utils.encoding import force_bytes, python_2_unicode_compatible from dcim.models import Device from utilities.models import CreatedUpdatedModel - from .exceptions import InvalidKey from .hashers import SecretValidationHasher @@ -301,8 +302,8 @@ class Secret(CreatedUpdatedModel): def __str__(self): if self.role and self.device: - return u'{} for {}'.format(self.role, self.device) - return u'Secret' + return '{} for {}'.format(self.role, self.device) + return 'Secret' def get_absolute_url(self): return reverse('secrets:secret', args=[self.pk]) diff --git a/netbox/secrets/tables.py b/netbox/secrets/tables.py index ca9b2fd96..980c093b7 100644 --- a/netbox/secrets/tables.py +++ b/netbox/secrets/tables.py @@ -1,5 +1,6 @@ +from __future__ import unicode_literals + import django_tables2 as tables -from django_tables2.utils import Accessor from utilities.tables import BaseTable, SearchTable, ToggleColumn @@ -22,8 +23,9 @@ class SecretRoleTable(BaseTable): name = tables.LinkColumn(verbose_name='Name') secret_count = tables.Column(verbose_name='Secrets') slug = tables.Column(verbose_name='Slug') - actions = tables.TemplateColumn(template_code=SECRETROLE_ACTIONS, attrs={'td': {'class': 'text-right'}}, - verbose_name='') + actions = tables.TemplateColumn( + template_code=SECRETROLE_ACTIONS, attrs={'td': {'class': 'text-right'}}, verbose_name='' + ) class Meta(BaseTable.Meta): model = SecretRole diff --git a/netbox/secrets/templatetags/secret_helpers.py b/netbox/secrets/templatetags/secret_helpers.py index 142c0d2cb..0e1ff554c 100644 --- a/netbox/secrets/templatetags/secret_helpers.py +++ b/netbox/secrets/templatetags/secret_helpers.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from django import template diff --git a/netbox/secrets/tests/test_api.py b/netbox/secrets/tests/test_api.py index 227478b99..4acda3eff 100644 --- a/netbox/secrets/tests/test_api.py +++ b/netbox/secrets/tests/test_api.py @@ -1,4 +1,6 @@ +from __future__ import unicode_literals import base64 + from rest_framework import status from rest_framework.test import APITestCase diff --git a/netbox/secrets/tests/test_models.py b/netbox/secrets/tests/test_models.py index e668f1185..4be37801d 100644 --- a/netbox/secrets/tests/test_models.py +++ b/netbox/secrets/tests/test_models.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from Crypto.PublicKey import RSA from django.conf import settings diff --git a/netbox/secrets/urls.py b/netbox/secrets/urls.py index a4d3b7a78..4961b2c82 100644 --- a/netbox/secrets/urls.py +++ b/netbox/secrets/urls.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from django.conf.urls import url from . import views @@ -17,7 +19,7 @@ urlpatterns = [ url(r'^secrets/import/$', views.secret_import, name='secret_import'), url(r'^secrets/edit/$', views.SecretBulkEditView.as_view(), name='secret_bulk_edit'), url(r'^secrets/delete/$', views.SecretBulkDeleteView.as_view(), name='secret_bulk_delete'), - url(r'^secrets/(?P\d+)/$', views.secret, name='secret'), + url(r'^secrets/(?P\d+)/$', views.SecretView.as_view(), name='secret'), url(r'^secrets/(?P\d+)/edit/$', views.secret_edit, name='secret_edit'), url(r'^secrets/(?P\d+)/delete/$', views.SecretDeleteView.as_view(), name='secret_delete'), diff --git a/netbox/secrets/views.py b/netbox/secrets/views.py index 0a132dca3..d2427dd73 100644 --- a/netbox/secrets/views.py +++ b/netbox/secrets/views.py @@ -1,3 +1,4 @@ +from __future__ import unicode_literals import base64 from django.contrib import messages @@ -8,10 +9,10 @@ from django.db.models import Count from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse from django.utils.decorators import method_decorator +from django.views.generic import View from dcim.models import Device from utilities.views import BulkDeleteView, BulkEditView, ObjectDeleteView, ObjectEditView, ObjectListView - from . import filters, forms, tables from .decorators import userkey_required from .models import SecretRole, Secret, SessionKey @@ -65,14 +66,16 @@ class SecretListView(ObjectListView): template_name = 'secrets/secret_list.html' -@login_required -def secret(request, pk): +@method_decorator(login_required, name='dispatch') +class SecretView(View): - secret = get_object_or_404(Secret, pk=pk) + def get(self, request, pk): - return render(request, 'secrets/secret.html', { - 'secret': secret, - }) + secret = get_object_or_404(Secret, pk=pk) + + return render(request, 'secrets/secret.html', { + 'secret': secret, + }) @permission_required('secrets.add_secret') @@ -107,7 +110,7 @@ def secret_add(request, pk): secret.plaintext = str(form.cleaned_data['plaintext']) secret.encrypt(master_key) secret.save() - messages.success(request, u"Added new secret: {}.".format(secret)) + messages.success(request, "Added new secret: {}.".format(secret)) if '_addanother' in request.POST: return redirect('dcim:device_addsecret', pk=device.pk) else: @@ -151,7 +154,7 @@ def secret_edit(request, pk): secret.plaintext = str(form.cleaned_data['plaintext']) secret.encrypt(master_key) secret.save() - messages.success(request, u"Modified secret {}.".format(secret)) + messages.success(request, "Modified secret {}.".format(secret)) return redirect('secrets:secret', pk=secret.pk) else: form.add_error(None, "Invalid session key. Unable to encrypt secret data.") @@ -163,7 +166,7 @@ def secret_edit(request, pk): # If no new plaintext was specified, a session key is not needed. else: secret = form.save() - messages.success(request, u"Modified secret {}.".format(secret)) + messages.success(request, "Modified secret {}.".format(secret)) return redirect('secrets:secret', pk=secret.pk) else: @@ -217,7 +220,7 @@ def secret_import(request): new_secrets.append(secret) table = tables.SecretTable(new_secrets) - messages.success(request, u"Imported {} new secrets.".format(len(new_secrets))) + messages.success(request, "Imported {} new secrets.".format(len(new_secrets))) return render(request, 'import_success.html', { 'table': table, diff --git a/netbox/templates/500.html b/netbox/templates/500.html index ba2ebf94f..575694b13 100644 --- a/netbox/templates/500.html +++ b/netbox/templates/500.html @@ -1,37 +1,40 @@ +{% load static from staticfiles %} Server Error - - + + -
-
-
-
- - - Server Error - -
-
-

There was a problem with your request. This error has been logged and administrative staff have - been notified. Please return to the home page and try again.

-

If you are responsible for this installation, please consider - filing a bug report. Additional - information is provided below:

-
{{ exception }}
+
+
+
+
+
+ + + Server Error + +
+
+

There was a problem with your request. This error has been logged and administrative staff have + been notified. Please return to the home page and try again.

+

If you are responsible for this installation, please consider + filing a bug report. Additional + information is provided below:

+
{{ exception }}
{{ error }}
-
-
+
diff --git a/netbox/templates/circuits/circuittermination_edit.html b/netbox/templates/circuits/circuittermination_edit.html index 22e02c4ee..13aa7e5b6 100644 --- a/netbox/templates/circuits/circuittermination_edit.html +++ b/netbox/templates/circuits/circuittermination_edit.html @@ -45,23 +45,8 @@
{% render_field form.site %} -
-
- -
-
-
-
- {% render_field form.rack %} - {% render_field form.device %} -
- -
+ {% render_field form.rack %} + {% render_field form.device %} {% render_field form.interface %}
diff --git a/netbox/templates/dcim/consoleport_connect.html b/netbox/templates/dcim/consoleport_connect.html index a04a4674b..e06bf45ec 100644 --- a/netbox/templates/dcim/consoleport_connect.html +++ b/netbox/templates/dcim/consoleport_connect.html @@ -32,12 +32,7 @@ {% render_field form.livesearch %}
-
- -
-

{{ consoleport.device.site }}

-
-
+ {% render_field form.site %} {% render_field form.rack %} {% render_field form.console_server %}
diff --git a/netbox/templates/dcim/consoleserverport_connect.html b/netbox/templates/dcim/consoleserverport_connect.html index 6ba944b59..82b80e3f7 100644 --- a/netbox/templates/dcim/consoleserverport_connect.html +++ b/netbox/templates/dcim/consoleserverport_connect.html @@ -32,12 +32,7 @@ {% render_field form.livesearch %}
-
- -
-

{{ consoleserverport.device.site }}

-
-
+ {% render_field form.site %} {% render_field form.rack %} {% render_field form.device %}
diff --git a/netbox/templates/dcim/device_import_child.html b/netbox/templates/dcim/device_import_child.html index 3fea2572a..668a9c810 100644 --- a/netbox/templates/dcim/device_import_child.html +++ b/netbox/templates/dcim/device_import_child.html @@ -69,6 +69,11 @@ Unique alphanumeric tag (optional) ABC123456 + + Status + Current status + Active + Parent device Parent device @@ -82,7 +87,7 @@

Example

-
Blade12,Blade Server,Pied Piper,Dell,BS2000T,Linux,CAB00577291,ABC123456,Server101,Slot4
+
Blade12,Blade Server,Pied Piper,Dell,BS2000T,Linux,CAB00577291,ABC123456,Active,Server101,Slot4
{% endblock %} diff --git a/netbox/templates/dcim/poweroutlet_connect.html b/netbox/templates/dcim/poweroutlet_connect.html index 6fcc3e858..839027db2 100644 --- a/netbox/templates/dcim/poweroutlet_connect.html +++ b/netbox/templates/dcim/poweroutlet_connect.html @@ -32,12 +32,7 @@ {% render_field form.livesearch %}
-
- -
-

{{ poweroutlet.device.site }}

-
-
+ {% render_field form.site %} {% render_field form.rack %} {% render_field form.device %}
diff --git a/netbox/templates/dcim/powerport_connect.html b/netbox/templates/dcim/powerport_connect.html index f77a0e352..a10fecc7b 100644 --- a/netbox/templates/dcim/powerport_connect.html +++ b/netbox/templates/dcim/powerport_connect.html @@ -32,12 +32,7 @@ {% render_field form.livesearch %}
-
- -
-

{{ powerport.device.site }}

-
-
+ {% render_field form.site %} {% render_field form.rack %} {% render_field form.pdu %}
diff --git a/netbox/templates/inc/table.html b/netbox/templates/inc/table.html index e8d84fbeb..0c046ff2f 100644 --- a/netbox/templates/inc/table.html +++ b/netbox/templates/inc/table.html @@ -6,7 +6,7 @@ {% for column in table.columns %} {% if column.orderable %} - {{ column.header }} + {{ column.header }} {% else %} {{ column.header }} {% endif %} diff --git a/netbox/templates/ipam/ipaddress_edit.html b/netbox/templates/ipam/ipaddress_edit.html index d7aef0fe4..64dc22353 100644 --- a/netbox/templates/ipam/ipaddress_edit.html +++ b/netbox/templates/ipam/ipaddress_edit.html @@ -47,6 +47,7 @@
{% render_field form.nat_site %} + {% render_field form.nat_rack %} {% render_field form.nat_device %}