Merge pull request #1230 from digitalocean/develop

Release v2.0.4
This commit is contained in:
Jeremy Stretch 2017-05-25 14:45:13 -04:00 committed by GitHub
commit f7b0d22f86
129 changed files with 1748 additions and 768 deletions

View File

@ -45,6 +45,10 @@ sure to include:
* Any error messages generated * Any error messages generated
* Screenshots (if applicable) * 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 * 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 much work is required to resolve them. It may take some time for someone
to address your issue. to address your issue.
@ -91,6 +95,10 @@ following:
* Any third-party libraries or other resources which would be * Any third-party libraries or other resources which would be
involved 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 ## Submitting Pull Requests
* Be sure to open an issue before starting work on a pull request, and * Be sure to open an issue before starting work on a pull request, and

View File

@ -58,6 +58,14 @@ This script:
* Applies any database migrations that were included in the release * Applies any database migrations that were included in the release
* Collects all static files to be served by the HTTP service * 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 # 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`: Finally, restart the WSGI service to run the new code. If you followed this guide for the initial installation, this is done using `supervisorctl`:

View File

@ -1,3 +1,5 @@
from __future__ import unicode_literals
from rest_framework import serializers from rest_framework import serializers
from circuits.models import Provider, Circuit, CircuitTermination, CircuitType from circuits.models import Provider, Circuit, CircuitTermination, CircuitType

View File

@ -1,3 +1,5 @@
from __future__ import unicode_literals
from rest_framework import routers from rest_framework import routers
from . import views from . import views

View File

@ -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.decorators import detail_route
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from django.shortcuts import get_object_or_404
from circuits import filters from circuits import filters
from circuits.models import Provider, CircuitTermination, CircuitType, Circuit from circuits.models import Provider, CircuitTermination, CircuitType, Circuit
from extras.models import Graph, GRAPH_TYPE_PROVIDER from extras.models import Graph, GRAPH_TYPE_PROVIDER

View File

@ -1,3 +1,5 @@
from __future__ import unicode_literals
from django.apps import AppConfig from django.apps import AppConfig

View File

@ -1,3 +1,5 @@
from __future__ import unicode_literals
import django_filters import django_filters
from django.db.models import Q from django.db.models import Q

View File

@ -1,3 +1,5 @@
from __future__ import unicode_literals
from django import forms from django import forms
from django.db.models import Count from django.db.models import Count
@ -165,7 +167,9 @@ class CircuitTerminationForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm
) )
rack = ChainedModelChoiceField( rack = ChainedModelChoiceField(
queryset=Rack.objects.all(), queryset=Rack.objects.all(),
chains={'site': 'site'}, chains=(
('site', 'site'),
),
required=False, required=False,
label='Rack', label='Rack',
widget=APISelect( widget=APISelect(
@ -175,7 +179,10 @@ class CircuitTerminationForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm
) )
device = ChainedModelChoiceField( device = ChainedModelChoiceField(
queryset=Device.objects.all(), queryset=Device.objects.all(),
chains={'site': 'site', 'rack': 'rack'}, chains=(
('site', 'site'),
('rack', 'rack'),
),
required=False, required=False,
label='Device', label='Device',
widget=APISelect( widget=APISelect(
@ -184,20 +191,13 @@ class CircuitTerminationForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm
attrs={'filter-for': 'interface'} 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( interface = ChainedModelChoiceField(
queryset=Interface.objects.exclude(form_factor__in=VIRTUAL_IFACE_TYPES).select_related( queryset=Interface.objects.exclude(form_factor__in=VIRTUAL_IFACE_TYPES).select_related(
'circuit_termination', 'connected_as_a', 'connected_as_b' 'circuit_termination', 'connected_as_a', 'connected_as_b'
), ),
chains={'device': 'device'}, chains=(
('device', 'device'),
),
required=False, required=False,
label='Interface', label='Interface',
widget=APISelect( widget=APISelect(
@ -208,8 +208,10 @@ class CircuitTerminationForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm
class Meta: class Meta:
model = CircuitTermination model = CircuitTermination
fields = ['term_side', 'site', 'rack', 'device', 'livesearch', 'interface', 'port_speed', 'upstream_speed', fields = [
'xconnect_id', 'pp_info'] 'term_side', 'site', 'rack', 'device', 'interface', 'port_speed', 'upstream_speed', 'xconnect_id',
'pp_info',
]
help_texts = { help_texts = {
'port_speed': "Physical circuit speed", 'port_speed': "Physical circuit speed",
'xconnect_id': "ID of the local cross-connect", 'xconnect_id': "ID of the local cross-connect",

View File

@ -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'),
),
]

View File

@ -1,3 +1,5 @@
from __future__ import unicode_literals
from django.contrib.contenttypes.fields import GenericRelation from django.contrib.contenttypes.fields import GenericRelation
from django.db import models from django.db import models
from django.urls import reverse from django.urls import reverse
@ -110,7 +112,7 @@ class Circuit(CreatedUpdatedModel, CustomFieldModel):
unique_together = ['provider', 'cid'] unique_together = ['provider', 'cid']
def __str__(self): def __str__(self):
return u'{} {}'.format(self.provider, self.cid) return '{} {}'.format(self.provider, self.cid)
def get_absolute_url(self): def get_absolute_url(self):
return reverse('circuits:circuit', args=[self.pk]) return reverse('circuits:circuit', args=[self.pk])
@ -166,7 +168,7 @@ class CircuitTermination(models.Model):
unique_together = ['circuit', 'term_side'] unique_together = ['circuit', 'term_side']
def __str__(self): 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): def get_peer_termination(self):
peer_side = 'Z' if self.term_side == 'A' else 'A' peer_side = 'Z' if self.term_side == 'A' else 'A'

View File

@ -1,3 +1,5 @@
from __future__ import unicode_literals
from django.db.models.signals import post_delete, post_save from django.db.models.signals import post_delete, post_save
from django.dispatch import receiver from django.dispatch import receiver
from django.utils import timezone from django.utils import timezone

View File

@ -1,8 +1,9 @@
from __future__ import unicode_literals
import django_tables2 as tables import django_tables2 as tables
from django_tables2.utils import Accessor from django_tables2.utils import Accessor
from utilities.tables import BaseTable, SearchTable, ToggleColumn from utilities.tables import BaseTable, SearchTable, ToggleColumn
from .models import Circuit, CircuitType, Provider from .models import Circuit, CircuitType, Provider

View File

@ -1,3 +1,5 @@
from __future__ import unicode_literals
from rest_framework import status from rest_framework import status
from rest_framework.test import APITestCase from rest_framework.test import APITestCase

View File

@ -1,3 +1,5 @@
from __future__ import unicode_literals
from django.conf.urls import url from django.conf.urls import url
from . import views from . import views
@ -12,7 +14,7 @@ urlpatterns = [
url(r'^providers/import/$', views.ProviderBulkImportView.as_view(), name='provider_import'), 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/edit/$', views.ProviderBulkEditView.as_view(), name='provider_bulk_edit'),
url(r'^providers/delete/$', views.ProviderBulkDeleteView.as_view(), name='provider_bulk_delete'), url(r'^providers/delete/$', views.ProviderBulkDeleteView.as_view(), name='provider_bulk_delete'),
url(r'^providers/(?P<slug>[\w-]+)/$', views.provider, name='provider'), url(r'^providers/(?P<slug>[\w-]+)/$', views.ProviderView.as_view(), name='provider'),
url(r'^providers/(?P<slug>[\w-]+)/edit/$', views.ProviderEditView.as_view(), name='provider_edit'), url(r'^providers/(?P<slug>[\w-]+)/edit/$', views.ProviderEditView.as_view(), name='provider_edit'),
url(r'^providers/(?P<slug>[\w-]+)/delete/$', views.ProviderDeleteView.as_view(), name='provider_delete'), url(r'^providers/(?P<slug>[\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/import/$', views.CircuitBulkImportView.as_view(), name='circuit_import'),
url(r'^circuits/edit/$', views.CircuitBulkEditView.as_view(), name='circuit_bulk_edit'), 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/delete/$', views.CircuitBulkDeleteView.as_view(), name='circuit_bulk_delete'),
url(r'^circuits/(?P<pk>\d+)/$', views.circuit, name='circuit'), url(r'^circuits/(?P<pk>\d+)/$', views.CircuitView.as_view(), name='circuit'),
url(r'^circuits/(?P<pk>\d+)/edit/$', views.CircuitEditView.as_view(), name='circuit_edit'), url(r'^circuits/(?P<pk>\d+)/edit/$', views.CircuitEditView.as_view(), name='circuit_edit'),
url(r'^circuits/(?P<pk>\d+)/delete/$', views.CircuitDeleteView.as_view(), name='circuit_delete'), url(r'^circuits/(?P<pk>\d+)/delete/$', views.CircuitDeleteView.as_view(), name='circuit_delete'),
url(r'^circuits/(?P<pk>\d+)/terminations/swap/$', views.circuit_terminations_swap, name='circuit_terminations_swap'), url(r'^circuits/(?P<pk>\d+)/terminations/swap/$', views.circuit_terminations_swap, name='circuit_terminations_swap'),

View File

@ -1,3 +1,5 @@
from __future__ import unicode_literals
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.decorators import permission_required from django.contrib.auth.decorators import permission_required
from django.contrib.auth.mixins import PermissionRequiredMixin from django.contrib.auth.mixins import PermissionRequiredMixin
@ -5,13 +7,13 @@ from django.db import transaction
from django.db.models import Count from django.db.models import Count
from django.shortcuts import get_object_or_404, redirect, render from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse from django.urls import reverse
from django.views.generic import View
from extras.models import Graph, GRAPH_TYPE_PROVIDER from extras.models import Graph, GRAPH_TYPE_PROVIDER
from utilities.forms import ConfirmationForm from utilities.forms import ConfirmationForm
from utilities.views import ( from utilities.views import (
BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView, BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
) )
from . import filters, forms, tables from . import filters, forms, tables
from .models import Circuit, CircuitTermination, CircuitType, Provider, TERM_SIDE_A, TERM_SIDE_Z from .models import Circuit, CircuitTermination, CircuitType, Provider, TERM_SIDE_A, TERM_SIDE_Z
@ -28,11 +30,16 @@ class ProviderListView(ObjectListView):
template_name = 'circuits/provider_list.html' template_name = 'circuits/provider_list.html'
def provider(request, slug): class ProviderView(View):
def get(self, request, slug):
provider = get_object_or_404(Provider, slug=slug) provider = get_object_or_404(Provider, slug=slug)
circuits = Circuit.objects.filter(provider=provider).select_related('type', 'tenant')\ circuits = Circuit.objects.filter(provider=provider).select_related(
.prefetch_related('terminations__site') 'type', 'tenant'
).prefetch_related(
'terminations__site'
)
show_graphs = Graph.objects.filter(type=GRAPH_TYPE_PROVIDER).exists() show_graphs = Graph.objects.filter(type=GRAPH_TYPE_PROVIDER).exists()
return render(request, 'circuits/provider.html', { return render(request, 'circuits/provider.html', {
@ -117,7 +124,9 @@ class CircuitListView(ObjectListView):
template_name = 'circuits/circuit_list.html' template_name = 'circuits/circuit_list.html'
def circuit(request, pk): class CircuitView(View):
def get(self, request, pk):
circuit = get_object_or_404(Circuit.objects.select_related('provider', 'type', 'tenant__group'), pk=pk) circuit = get_object_or_404(Circuit.objects.select_related('provider', 'type', 'tenant__group'), pk=pk)
termination_a = CircuitTermination.objects.select_related( termination_a = CircuitTermination.objects.select_related(

View File

@ -1,3 +1,5 @@
from __future__ import unicode_literals
from rest_framework.exceptions import APIException from rest_framework.exceptions import APIException

View File

@ -1,3 +1,5 @@
from __future__ import unicode_literals
from rest_framework import serializers from rest_framework import serializers
from rest_framework.validators import UniqueTogetherValidator from rest_framework.validators import UniqueTogetherValidator
@ -618,10 +620,11 @@ class PeerInterfaceSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail')
device = NestedDeviceSerializer() device = NestedDeviceSerializer()
form_factor = ChoiceFieldSerializer(choices=IFACE_FF_CHOICES) form_factor = ChoiceFieldSerializer(choices=IFACE_FF_CHOICES)
lag = NestedInterfaceSerializer()
class Meta: class Meta:
model = Interface 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): class WritableInterfaceSerializer(serializers.ModelSerializer):

View File

@ -1,3 +1,5 @@
from __future__ import unicode_literals
from rest_framework import routers from rest_framework import routers
from . import views from . import views

View File

@ -1,3 +1,5 @@
from __future__ import unicode_literals
from rest_framework.decorators import detail_route from rest_framework.decorators import detail_route
from rest_framework.mixins import ListModelMixin from rest_framework.mixins import ListModelMixin
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated

View File

@ -1,3 +1,5 @@
from __future__ import unicode_literals
from django.apps import AppConfig from django.apps import AppConfig

View File

@ -1,3 +1,5 @@
from __future__ import unicode_literals
from netaddr import EUI, mac_unix_expanded from netaddr import EUI, mac_unix_expanded
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError

View File

@ -1,3 +1,5 @@
from __future__ import unicode_literals
import django_filters import django_filters
from netaddr.core import AddrFormatError from netaddr.core import AddrFormatError

View File

@ -1,3 +1,5 @@
from __future__ import unicode_literals
from netaddr import EUI, AddrFormatError from netaddr import EUI, AddrFormatError
from django import forms from django import forms

View File

@ -1,3 +1,5 @@
from __future__ import unicode_literals
from mptt.forms import TreeNodeChoiceField from mptt.forms import TreeNodeChoiceField
import re import re
@ -16,7 +18,6 @@ from utilities.forms import (
FilterChoiceField, FlexibleModelChoiceField, Livesearch, SelectWithDisabled, SmallTextarea, SlugField, FilterChoiceField, FlexibleModelChoiceField, Livesearch, SelectWithDisabled, SmallTextarea, SlugField,
FilterTreeNodeMultipleChoiceField, FilterTreeNodeMultipleChoiceField,
) )
from .formfields import MACAddressFormField from .formfields import MACAddressFormField
from .models import ( from .models import (
DeviceBay, DeviceBayTemplate, CONNECTION_STATUS_CHOICES, CONNECTION_STATUS_PLANNED, CONNECTION_STATUS_CONNECTED, 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): class RackForm(BootstrapMixin, TenancyForm, CustomFieldForm):
group = ChainedModelChoiceField( group = ChainedModelChoiceField(
queryset=RackGroup.objects.all(), queryset=RackGroup.objects.all(),
chains={'site': 'site'}, chains=(
('site', 'site'),
),
required=False, required=False,
widget=APISelect( widget=APISelect(
api_url='/api/dcim/rack-groups/?site_id={{site}}', api_url='/api/dcim/rack-groups/?site_id={{site}}',
@ -544,7 +547,9 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldForm):
) )
rack = ChainedModelChoiceField( rack = ChainedModelChoiceField(
queryset=Rack.objects.all(), queryset=Rack.objects.all(),
chains={'site': 'site'}, chains=(
('site', 'site'),
),
required=False, required=False,
widget=APISelect( widget=APISelect(
api_url='/api/dcim/racks/?site_id={{site}}', api_url='/api/dcim/racks/?site_id={{site}}',
@ -569,7 +574,9 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldForm):
) )
device_type = ChainedModelChoiceField( device_type = ChainedModelChoiceField(
queryset=DeviceType.objects.all(), queryset=DeviceType.objects.all(),
chains={'manufacturer': 'manufacturer'}, chains=(
('manufacturer', 'manufacturer'),
),
label='Device type', label='Device type',
widget=APISelect( widget=APISelect(
api_url='/api/dcim/device-types/?manufacturer_id={{manufacturer}}', api_url='/api/dcim/device-types/?manufacturer_id={{manufacturer}}',
@ -610,10 +617,10 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldForm):
for family in [4, 6]: for family in [4, 6]:
ip_choices = [] ip_choices = []
interface_ips = IPAddress.objects.filter(family=family, interface__device=self.instance) 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)\ nat_ips = IPAddress.objects.filter(family=family, nat_inside__interface__device=self.instance)\
.select_related('nat_inside__interface') .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 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 # 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 = {} status_counts = {}
for status in Device.objects.values('status').annotate(count=Count('status')).order_by('status'): for status in Device.objects.values('status').annotate(count=Count('status')).order_by('status'):
status_counts[status['status']] = status['count'] 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): class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
@ -956,20 +963,29 @@ class ConsoleConnectionImportForm(BootstrapMixin, BulkImportForm):
class ConsolePortConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm): class ConsolePortConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm):
site = forms.ModelChoiceField( site = forms.ModelChoiceField(
queryset=Site.objects.all(), queryset=Site.objects.all(),
widget=forms.HiddenInput(), required=False,
widget=forms.Select(
attrs={'filter-for': 'rack'}
)
) )
rack = ChainedModelChoiceField( rack = ChainedModelChoiceField(
queryset=Rack.objects.all(), queryset=Rack.objects.all(),
chains={'site': 'site'}, chains=(
('site', 'site'),
),
label='Rack', label='Rack',
required=False, required=False,
widget=forms.Select( widget=APISelect(
api_url='/api/dcim/racks/?site_id={{site}}',
attrs={'filter-for': 'console_server', 'nullable': 'true'} attrs={'filter-for': 'console_server', 'nullable': 'true'}
) )
) )
console_server = ChainedModelChoiceField( console_server = ChainedModelChoiceField(
queryset=Device.objects.filter(device_type__is_console_server=True), queryset=Device.objects.filter(device_type__is_console_server=True),
chains={'site': 'site', 'rack': 'rack'}, chains=(
('site', 'site'),
('rack', 'rack'),
),
label='Console Server', label='Console Server',
required=False, required=False,
widget=APISelect( widget=APISelect(
@ -989,7 +1005,9 @@ class ConsolePortConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelF
) )
cs_port = ChainedModelChoiceField( cs_port = ChainedModelChoiceField(
queryset=ConsoleServerPort.objects.all(), queryset=ConsoleServerPort.objects.all(),
chains={'device': 'console_server'}, chains=(
('device', 'console_server'),
),
label='Port', label='Port',
widget=APISelect( widget=APISelect(
api_url='/api/dcim/console-server-ports/?device_id={{console_server}}', api_url='/api/dcim/console-server-ports/?device_id={{console_server}}',
@ -1034,20 +1052,29 @@ class ConsoleServerPortCreateForm(DeviceComponentForm):
class ConsoleServerPortConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.Form): class ConsoleServerPortConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.Form):
site = forms.ModelChoiceField( site = forms.ModelChoiceField(
queryset=Site.objects.all(), queryset=Site.objects.all(),
widget=forms.HiddenInput(), required=False,
widget=forms.Select(
attrs={'filter-for': 'rack'}
)
) )
rack = ChainedModelChoiceField( rack = ChainedModelChoiceField(
queryset=Rack.objects.all(), queryset=Rack.objects.all(),
chains={'site': 'site'}, chains=(
('site', 'site'),
),
label='Rack', label='Rack',
required=False, required=False,
widget=forms.Select( widget=APISelect(
api_url='/api/dcim/racks/?site_id={{site}}',
attrs={'filter-for': 'device', 'nullable': 'true'} attrs={'filter-for': 'device', 'nullable': 'true'}
) )
) )
device = ChainedModelChoiceField( device = ChainedModelChoiceField(
queryset=Device.objects.all(), queryset=Device.objects.all(),
chains={'site': 'site', 'rack': 'rack'}, chains=(
('site', 'site'),
('rack', 'rack'),
),
label='Device', label='Device',
required=False, required=False,
widget=APISelect( widget=APISelect(
@ -1067,7 +1094,9 @@ class ConsoleServerPortConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.
) )
port = ChainedModelChoiceField( port = ChainedModelChoiceField(
queryset=ConsolePort.objects.all(), queryset=ConsolePort.objects.all(),
chains={'device': 'device'}, chains=(
('device', 'device'),
),
label='Port', label='Port',
widget=APISelect( widget=APISelect(
api_url='/api/dcim/console-ports/?device_id={{device}}', api_url='/api/dcim/console-ports/?device_id={{device}}',
@ -1181,19 +1210,31 @@ class PowerConnectionImportForm(BootstrapMixin, BulkImportForm):
class PowerPortConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm): class PowerPortConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm):
site = forms.ModelChoiceField(queryset=Site.objects.all(), widget=forms.HiddenInput()) site = forms.ModelChoiceField(
rack = ChainedModelChoiceField( queryset=Site.objects.all(),
queryset=Rack.objects.all(),
chains={'site': 'site'},
label='Rack',
required=False, required=False,
widget=forms.Select( 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'} attrs={'filter-for': 'pdu', 'nullable': 'true'}
) )
) )
pdu = ChainedModelChoiceField( pdu = ChainedModelChoiceField(
queryset=Device.objects.all(), queryset=Device.objects.all(),
chains={'site': 'site', 'rack': 'rack'}, chains=(
('site', 'site'),
('rack', 'rack'),
),
label='PDU', label='PDU',
required=False, required=False,
widget=APISelect( widget=APISelect(
@ -1213,7 +1254,9 @@ class PowerPortConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelFor
) )
power_outlet = ChainedModelChoiceField( power_outlet = ChainedModelChoiceField(
queryset=PowerOutlet.objects.all(), queryset=PowerOutlet.objects.all(),
chains={'device': 'pdu'}, chains=(
('device', 'pdu'),
),
label='Outlet', label='Outlet',
widget=APISelect( widget=APISelect(
api_url='/api/dcim/power-outlets/?device_id={{pdu}}', api_url='/api/dcim/power-outlets/?device_id={{pdu}}',
@ -1258,20 +1301,29 @@ class PowerOutletCreateForm(DeviceComponentForm):
class PowerOutletConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.Form): class PowerOutletConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.Form):
site = forms.ModelChoiceField( site = forms.ModelChoiceField(
queryset=Site.objects.all(), queryset=Site.objects.all(),
widget=forms.HiddenInput() required=False,
widget=forms.Select(
attrs={'filter-for': 'rack'}
)
) )
rack = ChainedModelChoiceField( rack = ChainedModelChoiceField(
queryset=Rack.objects.all(), queryset=Rack.objects.all(),
chains={'site': 'site'}, chains=(
('site', 'site'),
),
label='Rack', label='Rack',
required=False, required=False,
widget=forms.Select( widget=APISelect(
api_url='/api/dcim/racks/?site_id={{site}}',
attrs={'filter-for': 'device', 'nullable': 'true'} attrs={'filter-for': 'device', 'nullable': 'true'}
) )
) )
device = ChainedModelChoiceField( device = ChainedModelChoiceField(
queryset=Device.objects.all(), queryset=Device.objects.all(),
chains={'site': 'site', 'rack': 'rack'}, chains=(
('site', 'site'),
('rack', 'rack'),
),
label='Device', label='Device',
required=False, required=False,
widget=APISelect( widget=APISelect(
@ -1291,7 +1343,9 @@ class PowerOutletConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.Form):
) )
port = ChainedModelChoiceField( port = ChainedModelChoiceField(
queryset=PowerPort.objects.all(), queryset=PowerPort.objects.all(),
chains={'device': 'device'}, chains=(
('device', 'device'),
),
label='Port', label='Port',
widget=APISelect( widget=APISelect(
api_url='/api/dcim/power-ports/?device_id={{device}}', api_url='/api/dcim/power-ports/?device_id={{device}}',
@ -1411,7 +1465,9 @@ class InterfaceConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelFor
) )
rack_b = ChainedModelChoiceField( rack_b = ChainedModelChoiceField(
queryset=Rack.objects.all(), queryset=Rack.objects.all(),
chains={'site': 'site_b'}, chains=(
('site', 'site_b'),
),
label='Rack', label='Rack',
required=False, required=False,
widget=APISelect( widget=APISelect(
@ -1421,7 +1477,10 @@ class InterfaceConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelFor
) )
device_b = ChainedModelChoiceField( device_b = ChainedModelChoiceField(
queryset=Device.objects.all(), queryset=Device.objects.all(),
chains={'site': 'site_b', 'rack': 'rack_b'}, chains=(
('site', 'site_b'),
('rack', 'rack_b'),
),
label='Device', label='Device',
required=False, required=False,
widget=APISelect( widget=APISelect(
@ -1443,7 +1502,9 @@ class InterfaceConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelFor
queryset=Interface.objects.exclude(form_factor__in=VIRTUAL_IFACE_TYPES).select_related( queryset=Interface.objects.exclude(form_factor__in=VIRTUAL_IFACE_TYPES).select_related(
'circuit_termination', 'connected_as_a', 'connected_as_b' 'circuit_termination', 'connected_as_a', 'connected_as_b'
), ),
chains={'device': 'device_b'}, chains=(
('device', 'device_b'),
),
label='Interface', label='Interface',
widget=APISelect( widget=APISelect(
api_url='/api/dcim/interfaces/?device_id={{device_b}}&type=physical', api_url='/api/dcim/interfaces/?device_id={{device_b}}&type=physical',

View File

@ -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'),
),
]

View File

@ -1,3 +1,4 @@
from __future__ import unicode_literals
from collections import OrderedDict from collections import OrderedDict
from itertools import count, groupby from itertools import count, groupby
@ -23,7 +24,6 @@ from utilities.fields import ColorField, NullableCharField
from utilities.managers import NaturalOrderByManager from utilities.managers import NaturalOrderByManager
from utilities.models import CreatedUpdatedModel from utilities.models import CreatedUpdatedModel
from utilities.utils import csv_format from utilities.utils import csv_format
from .fields import ASNField, MACAddressField from .fields import ASNField, MACAddressField
@ -346,7 +346,7 @@ class RackGroup(models.Model):
] ]
def __str__(self): def __str__(self):
return u'{} - {}'.format(self.site.name, self.name) return '{} - {}'.format(self.site.name, self.name)
def get_absolute_url(self): def get_absolute_url(self):
return "{}?group_id={}".format(reverse('dcim:rack_list'), self.pk) return "{}?group_id={}".format(reverse('dcim:rack_list'), self.pk)
@ -466,10 +466,10 @@ class Rack(CreatedUpdatedModel, CustomFieldModel):
@property @property
def display_name(self): def display_name(self):
if self.facility_id: if self.facility_id:
return u"{} ({})".format(self.name, self.facility_id) return "{} ({})".format(self.name, self.facility_id)
elif self.name: elif self.name:
return self.name return self.name
return u"" return ""
def get_rack_units(self, face=RACK_FACE_FRONT, exclude=None, remove_redundant=False): def get_rack_units(self, face=RACK_FACE_FRONT, exclude=None, remove_redundant=False):
""" """
@ -569,7 +569,7 @@ class RackReservation(models.Model):
ordering = ['created'] ordering = ['created']
def __str__(self): def __str__(self):
return u"Reservation for rack {}".format(self.rack) return "Reservation for rack {}".format(self.rack)
def clean(self): 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] invalid_units = [u for u in self.units if u not in self.rack.units]
if invalid_units: if invalid_units:
raise ValidationError({ raise ValidationError({
'units': u"Invalid unit(s) for {}U rack: {}".format( 'units': "Invalid unit(s) for {}U rack: {}".format(
self.rack.u_height, self.rack.u_height,
', '.join([str(u) for u in invalid_units]), ', '.join([str(u) for u in invalid_units]),
), ),
@ -733,7 +733,7 @@ class DeviceType(models.Model, CustomFieldModel):
@property @property
def full_name(self): def full_name(self):
return u'{} {}'.format(self.manufacturer.name, self.model) return '{} {}'.format(self.manufacturer.name, self.model)
@property @property
def is_parent_device(self): def is_parent_device(self):
@ -1106,8 +1106,8 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
if self.name: if self.name:
return self.name return self.name
elif hasattr(self, 'device_type'): elif hasattr(self, 'device_type'):
return u"{}".format(self.device_type) return "{}".format(self.device_type)
return u"" return ""
@property @property
def identifier(self): def identifier(self):
@ -1320,7 +1320,7 @@ class Interface(models.Model):
# An interface's LAG must belong to the same device # An interface's LAG must belong to the same device
if self.lag and self.lag.device != self.device: if self.lag and self.lag.device != self.device:
raise ValidationError({ 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 self.lag.name, self.lag.device.name
) )
}) })
@ -1328,14 +1328,14 @@ class Interface(models.Model):
# A virtual interface cannot have a parent LAG # A virtual interface cannot have a parent LAG
if self.form_factor in VIRTUAL_IFACE_TYPES and self.lag is not None: if self.form_factor in VIRTUAL_IFACE_TYPES and self.lag is not None:
raise ValidationError({ 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 # Only a LAG can have LAG members
if self.form_factor != IFACE_FF_LAG and self.member_interfaces.exists(): if self.form_factor != IFACE_FF_LAG and self.member_interfaces.exists():
raise ValidationError({ raise ValidationError({
'form_factor': "Cannot change interface form factor; it has LAG members ({}).".format( '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'] unique_together = ['device', 'name']
def __str__(self): def __str__(self):
return u'{} - {}'.format(self.device.name, self.name) return '{} - {}'.format(self.device.name, self.name)
def clean(self): def clean(self):

View File

@ -1,8 +1,9 @@
from __future__ import unicode_literals
import django_tables2 as tables import django_tables2 as tables
from django_tables2.utils import Accessor from django_tables2.utils import Accessor
from utilities.tables import BaseTable, SearchTable, ToggleColumn from utilities.tables import BaseTable, SearchTable, ToggleColumn
from .models import ( from .models import (
ConsolePort, ConsolePortTemplate, ConsoleServerPortTemplate, Device, DeviceBayTemplate, DeviceRole, DeviceType, ConsolePort, ConsolePortTemplate, ConsoleServerPortTemplate, Device, DeviceBayTemplate, DeviceRole, DeviceType,
Interface, InterfaceTemplate, Manufacturer, Platform, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, Interface, InterfaceTemplate, Manufacturer, Platform, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack,

View File

@ -1,3 +1,5 @@
from __future__ import unicode_literals
from rest_framework import status from rest_framework import status
from rest_framework.test import APITestCase from rest_framework.test import APITestCase

View File

@ -1,4 +1,7 @@
from __future__ import unicode_literals
from django.test import TestCase from django.test import TestCase
from dcim.forms import * from dcim.forms import *
from dcim.models import * from dcim.models import *

View File

@ -1,4 +1,7 @@
from __future__ import unicode_literals
from django.test import TestCase from django.test import TestCase
from dcim.models import * from dcim.models import *

View File

@ -1,9 +1,10 @@
from __future__ import unicode_literals
from django.conf.urls import url from django.conf.urls import url
from extras.views import ImageAttachmentEditView
from ipam.views import ServiceEditView from ipam.views import ServiceEditView
from secrets.views import secret_add from secrets.views import secret_add
from extras.views import ImageAttachmentEditView
from .models import Device, Rack, Site from .models import Device, Rack, Site
from . import views from . import views
@ -22,7 +23,7 @@ urlpatterns = [
url(r'^sites/add/$', views.SiteEditView.as_view(), name='site_add'), url(r'^sites/add/$', views.SiteEditView.as_view(), name='site_add'),
url(r'^sites/import/$', views.SiteBulkImportView.as_view(), name='site_import'), url(r'^sites/import/$', views.SiteBulkImportView.as_view(), name='site_import'),
url(r'^sites/edit/$', views.SiteBulkEditView.as_view(), name='site_bulk_edit'), url(r'^sites/edit/$', views.SiteBulkEditView.as_view(), name='site_bulk_edit'),
url(r'^sites/(?P<slug>[\w-]+)/$', views.site, name='site'), url(r'^sites/(?P<slug>[\w-]+)/$', views.SiteView.as_view(), name='site'),
url(r'^sites/(?P<slug>[\w-]+)/edit/$', views.SiteEditView.as_view(), name='site_edit'), url(r'^sites/(?P<slug>[\w-]+)/edit/$', views.SiteEditView.as_view(), name='site_edit'),
url(r'^sites/(?P<slug>[\w-]+)/delete/$', views.SiteDeleteView.as_view(), name='site_delete'), url(r'^sites/(?P<slug>[\w-]+)/delete/$', views.SiteDeleteView.as_view(), name='site_delete'),
url(r'^sites/(?P<object_id>\d+)/images/add/$', ImageAttachmentEditView.as_view(), name='site_add_image', kwargs={'model': Site}), url(r'^sites/(?P<object_id>\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/import/$', views.RackBulkImportView.as_view(), name='rack_import'),
url(r'^racks/edit/$', views.RackBulkEditView.as_view(), name='rack_bulk_edit'), 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/delete/$', views.RackBulkDeleteView.as_view(), name='rack_bulk_delete'),
url(r'^racks/(?P<pk>\d+)/$', views.rack, name='rack'), url(r'^racks/(?P<pk>\d+)/$', views.RackView.as_view(), name='rack'),
url(r'^racks/(?P<pk>\d+)/edit/$', views.RackEditView.as_view(), name='rack_edit'), url(r'^racks/(?P<pk>\d+)/edit/$', views.RackEditView.as_view(), name='rack_edit'),
url(r'^racks/(?P<pk>\d+)/delete/$', views.RackDeleteView.as_view(), name='rack_delete'), url(r'^racks/(?P<pk>\d+)/delete/$', views.RackDeleteView.as_view(), name='rack_delete'),
url(r'^racks/(?P<rack>\d+)/reservations/add/$', views.RackReservationEditView.as_view(), name='rack_add_reservation'), url(r'^racks/(?P<rack>\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/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/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/delete/$', views.DeviceTypeBulkDeleteView.as_view(), name='devicetype_bulk_delete'),
url(r'^device-types/(?P<pk>\d+)/$', views.devicetype, name='devicetype'), url(r'^device-types/(?P<pk>\d+)/$', views.DeviceTypeView.as_view(), name='devicetype'),
url(r'^device-types/(?P<pk>\d+)/edit/$', views.DeviceTypeEditView.as_view(), name='devicetype_edit'), url(r'^device-types/(?P<pk>\d+)/edit/$', views.DeviceTypeEditView.as_view(), name='devicetype_edit'),
url(r'^device-types/(?P<pk>\d+)/delete/$', views.DeviceTypeDeleteView.as_view(), name='devicetype_delete'), url(r'^device-types/(?P<pk>\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/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/edit/$', views.DeviceBulkEditView.as_view(), name='device_bulk_edit'),
url(r'^devices/delete/$', views.DeviceBulkDeleteView.as_view(), name='device_bulk_delete'), url(r'^devices/delete/$', views.DeviceBulkDeleteView.as_view(), name='device_bulk_delete'),
url(r'^devices/(?P<pk>\d+)/$', views.device, name='device'), url(r'^devices/(?P<pk>\d+)/$', views.DeviceView.as_view(), name='device'),
url(r'^devices/(?P<pk>\d+)/edit/$', views.DeviceEditView.as_view(), name='device_edit'), url(r'^devices/(?P<pk>\d+)/edit/$', views.DeviceEditView.as_view(), name='device_edit'),
url(r'^devices/(?P<pk>\d+)/delete/$', views.DeviceDeleteView.as_view(), name='device_delete'), url(r'^devices/(?P<pk>\d+)/delete/$', views.DeviceDeleteView.as_view(), name='device_delete'),
url(r'^devices/(?P<pk>\d+)/inventory/$', views.device_inventory, name='device_inventory'), url(r'^devices/(?P<pk>\d+)/inventory/$', views.DeviceInventoryView.as_view(), name='device_inventory'),
url(r'^devices/(?P<pk>\d+)/lldp-neighbors/$', views.device_lldp_neighbors, name='device_lldp_neighbors'), url(r'^devices/(?P<pk>\d+)/lldp-neighbors/$', views.DeviceLLDPNeighborsView.as_view(), name='device_lldp_neighbors'),
url(r'^devices/(?P<pk>\d+)/add-secret/$', secret_add, name='device_addsecret'), url(r'^devices/(?P<pk>\d+)/add-secret/$', secret_add, name='device_addsecret'),
url(r'^devices/(?P<device>\d+)/services/assign/$', ServiceEditView.as_view(), name='service_assign'), url(r'^devices/(?P<device>\d+)/services/assign/$', ServiceEditView.as_view(), name='service_assign'),
url(r'^devices/(?P<object_id>\d+)/images/add/$', ImageAttachmentEditView.as_view(), name='device_add_image', kwargs={'model': Device}), url(r'^devices/(?P<object_id>\d+)/images/add/$', ImageAttachmentEditView.as_view(), name='device_add_image', kwargs={'model': Device}),

View File

@ -1,3 +1,4 @@
from __future__ import unicode_literals
from copy import deepcopy from copy import deepcopy
import re import re
from natsort import natsorted from natsort import natsorted
@ -24,7 +25,6 @@ from utilities.paginator import EnhancedPaginator
from utilities.views import ( from utilities.views import (
BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView, BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
) )
from . import filters, forms, tables from . import filters, forms, tables
from .models import ( from .models import (
CONNECTION_STATUS_CONNECTED, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, CONNECTION_STATUS_CONNECTED, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device,
@ -109,11 +109,11 @@ class ComponentCreateView(View):
if field == 'name': if field == 'name':
field = 'name_pattern' field = 'name_pattern'
for e in errors: 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: if not form.errors:
self.model.objects.bulk_create(new_components) 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 len(new_components), self.model._meta.verbose_name_plural, parent
)) ))
if '_addanother' in request.POST: if '_addanother' in request.POST:
@ -178,7 +178,9 @@ class SiteListView(ObjectListView):
template_name = 'dcim/site_list.html' template_name = 'dcim/site_list.html'
def site(request, slug): class SiteView(View):
def get(self, request, slug):
site = get_object_or_404(Site.objects.select_related('region', 'tenant__group'), slug=slug) site = get_object_or_404(Site.objects.select_related('region', 'tenant__group'), slug=slug)
stats = { stats = {
@ -290,8 +292,13 @@ class RackRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
# #
class RackListView(ObjectListView): class RackListView(ObjectListView):
queryset = Rack.objects.select_related('site', 'group', 'tenant', 'role').prefetch_related('devices__device_type')\ queryset = Rack.objects.select_related(
.annotate(device_count=Count('devices', distinct=True)) 'site', 'group', 'tenant', 'role'
).prefetch_related(
'devices__device_type'
).annotate(
device_count=Count('devices', distinct=True)
)
filter = filters.RackFilter filter = filters.RackFilter
filter_form = forms.RackFilterForm filter_form = forms.RackFilterForm
table = tables.RackTable table = tables.RackTable
@ -338,7 +345,9 @@ class RackElevationListView(View):
}) })
def rack(request, pk): class RackView(View):
def get(self, request, pk):
rack = get_object_or_404(Rack.objects.select_related('site__region', 'tenant__group', 'group', 'role'), pk=pk) rack = get_object_or_404(Rack.objects.select_related('site__region', 'tenant__group', 'group', 'role'), pk=pk)
@ -481,7 +490,9 @@ class DeviceTypeListView(ObjectListView):
template_name = 'dcim/devicetype_list.html' template_name = 'dcim/devicetype_list.html'
def devicetype(request, pk): class DeviceTypeView(View):
def get(self, request, pk):
devicetype = get_object_or_404(DeviceType, pk=pk) devicetype = get_object_or_404(DeviceType, pk=pk)
@ -499,12 +510,14 @@ def devicetype(request, pk):
natsorted(PowerOutletTemplate.objects.filter(device_type=devicetype), key=attrgetter('name')) natsorted(PowerOutletTemplate.objects.filter(device_type=devicetype), key=attrgetter('name'))
) )
mgmt_interface_table = tables.InterfaceTemplateTable( mgmt_interface_table = tables.InterfaceTemplateTable(
list(InterfaceTemplate.objects.order_naturally(devicetype.interface_ordering).filter(device_type=devicetype, list(InterfaceTemplate.objects.order_naturally(devicetype.interface_ordering).filter(
mgmt_only=True)) device_type=devicetype, mgmt_only=True
))
) )
interface_table = tables.InterfaceTemplateTable( interface_table = tables.InterfaceTemplateTable(
list(InterfaceTemplate.objects.order_naturally(devicetype.interface_ordering).filter(device_type=devicetype, list(InterfaceTemplate.objects.order_naturally(devicetype.interface_ordering).filter(
mgmt_only=False)) device_type=devicetype, mgmt_only=False
))
) )
devicebay_table = tables.DeviceBayTemplateTable( devicebay_table = tables.DeviceBayTemplateTable(
natsorted(DeviceBayTemplate.objects.filter(device_type=devicetype), key=attrgetter('name')) natsorted(DeviceBayTemplate.objects.filter(device_type=devicetype), key=attrgetter('name'))
@ -727,7 +740,9 @@ class DeviceListView(ObjectListView):
template_name = 'dcim/device_list.html' template_name = 'dcim/device_list.html'
def device(request, pk): class DeviceView(View):
def get(self, request, pk):
device = get_object_or_404(Device.objects.select_related( device = get_object_or_404(Device.objects.select_related(
'site__region', 'rack__group', 'tenant__group', 'device_role', 'platform' 'site__region', 'rack__group', 'tenant__group', 'device_role', 'platform'
@ -744,14 +759,18 @@ def device(request, pk):
power_outlets = natsorted( power_outlets = natsorted(
PowerOutlet.objects.filter(device=device).select_related('connected_port'), key=attrgetter('name') PowerOutlet.objects.filter(device=device).select_related('connected_port'), key=attrgetter('name')
) )
interfaces = Interface.objects.order_naturally(device.device_type.interface_ordering)\ interfaces = Interface.objects.order_naturally(device.device_type.interface_ordering).filter(
.filter(device=device, mgmt_only=False)\ device=device, mgmt_only=False
.select_related('connected_as_a__interface_b__device', 'connected_as_b__interface_a__device', ).select_related(
'circuit_termination__circuit').prefetch_related('ip_addresses') 'connected_as_a__interface_b__device', 'connected_as_b__interface_a__device',
mgmt_interfaces = Interface.objects.order_naturally(device.device_type.interface_ordering)\ 'circuit_termination__circuit'
.filter(device=device, mgmt_only=True)\ ).prefetch_related('ip_addresses')
.select_related('connected_as_a__interface_b__device', 'connected_as_b__interface_a__device', mgmt_interfaces = Interface.objects.order_naturally(device.device_type.interface_ordering).filter(
'circuit_termination__circuit').prefetch_related('ip_addresses') 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( device_bays = natsorted(
DeviceBay.objects.filter(device=device).select_related('installed_device__device_type__manufacturer'), DeviceBay.objects.filter(device=device).select_related('installed_device__device_type__manufacturer'),
key=attrgetter('name') key=attrgetter('name')
@ -793,6 +812,44 @@ def device(request, pk):
}) })
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): class DeviceEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.change_device' permission_required = 'dcim.change_device'
model = Device model = Device
@ -851,30 +908,6 @@ class DeviceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
default_return_url = 'dcim:device_list' 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 # Console ports
# #
@ -897,7 +930,7 @@ def consoleport_connect(request, pk):
form = forms.ConsolePortConnectionForm(request.POST, instance=consoleport) form = forms.ConsolePortConnectionForm(request.POST, instance=consoleport)
if form.is_valid(): if form.is_valid():
consoleport = form.save() consoleport = form.save()
msg = u'Connected <a href="{}">{}</a> {} to <a href="{}">{}</a> {}'.format( msg = 'Connected <a href="{}">{}</a> {} to <a href="{}">{}</a> {}'.format(
consoleport.device.get_absolute_url(), consoleport.device.get_absolute_url(),
escape(consoleport.device), escape(consoleport.device),
escape(consoleport.name), escape(consoleport.name),
@ -911,9 +944,9 @@ def consoleport_connect(request, pk):
else: else:
form = forms.ConsolePortConnectionForm(instance=consoleport, initial={ form = forms.ConsolePortConnectionForm(instance=consoleport, initial={
'site': request.GET.get('site', consoleport.device.site), 'site': request.GET.get('site'),
'rack': request.GET.get('rack', None), 'rack': request.GET.get('rack'),
'console_server': request.GET.get('console_server', None), 'console_server': request.GET.get('console_server'),
'connection_status': CONNECTION_STATUS_CONNECTED, 'connection_status': CONNECTION_STATUS_CONNECTED,
}) })
@ -931,7 +964,7 @@ def consoleport_disconnect(request, pk):
if not consoleport.cs_port: if not consoleport.cs_port:
messages.warning( 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) return redirect('dcim:device', pk=consoleport.device.pk)
@ -942,7 +975,7 @@ def consoleport_disconnect(request, pk):
consoleport.cs_port = None consoleport.cs_port = None
consoleport.connection_status = None consoleport.connection_status = None
consoleport.save() consoleport.save()
msg = u'Disconnected <a href="{}">{}</a> {} from <a href="{}">{}</a> {}'.format( msg = 'Disconnected <a href="{}">{}</a> {} from <a href="{}">{}</a> {}'.format(
consoleport.device.get_absolute_url(), consoleport.device.get_absolute_url(),
escape(consoleport.device), escape(consoleport.device),
escape(consoleport.name), escape(consoleport.name),
@ -1014,7 +1047,7 @@ def consoleserverport_connect(request, pk):
consoleport.cs_port = consoleserverport consoleport.cs_port = consoleserverport
consoleport.connection_status = form.cleaned_data['connection_status'] consoleport.connection_status = form.cleaned_data['connection_status']
consoleport.save() consoleport.save()
msg = u'Connected <a href="{}">{}</a> {} to <a href="{}">{}</a> {}'.format( msg = 'Connected <a href="{}">{}</a> {} to <a href="{}">{}</a> {}'.format(
consoleport.device.get_absolute_url(), consoleport.device.get_absolute_url(),
escape(consoleport.device), escape(consoleport.device),
escape(consoleport.name), escape(consoleport.name),
@ -1028,9 +1061,9 @@ def consoleserverport_connect(request, pk):
else: else:
form = forms.ConsoleServerPortConnectionForm(initial={ form = forms.ConsoleServerPortConnectionForm(initial={
'site': request.GET.get('site', consoleserverport.device.site), 'site': request.GET.get('site'),
'rack': request.GET.get('rack', None), 'rack': request.GET.get('rack'),
'device': request.GET.get('device', None), 'device': request.GET.get('device'),
'connection_status': CONNECTION_STATUS_CONNECTED, 'connection_status': CONNECTION_STATUS_CONNECTED,
}) })
@ -1048,7 +1081,7 @@ def consoleserverport_disconnect(request, pk):
if not hasattr(consoleserverport, 'connected_console'): if not hasattr(consoleserverport, 'connected_console'):
messages.warning( 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) return redirect('dcim:device', pk=consoleserverport.device.pk)
@ -1059,7 +1092,7 @@ def consoleserverport_disconnect(request, pk):
consoleport.cs_port = None consoleport.cs_port = None
consoleport.connection_status = None consoleport.connection_status = None
consoleport.save() consoleport.save()
msg = u'Disconnected <a href="{}">{}</a> {} from <a href="{}">{}</a> {}'.format( msg = 'Disconnected <a href="{}">{}</a> {} from <a href="{}">{}</a> {}'.format(
consoleport.device.get_absolute_url(), consoleport.device.get_absolute_url(),
escape(consoleport.device), escape(consoleport.device),
escape(consoleport.name), escape(consoleport.name),
@ -1120,7 +1153,7 @@ def powerport_connect(request, pk):
form = forms.PowerPortConnectionForm(request.POST, instance=powerport) form = forms.PowerPortConnectionForm(request.POST, instance=powerport)
if form.is_valid(): if form.is_valid():
powerport = form.save() powerport = form.save()
msg = u'Connected <a href="{}">{}</a> {} to <a href="{}">{}</a> {}'.format( msg = 'Connected <a href="{}">{}</a> {} to <a href="{}">{}</a> {}'.format(
powerport.device.get_absolute_url(), powerport.device.get_absolute_url(),
escape(powerport.device), escape(powerport.device),
escape(powerport.name), escape(powerport.name),
@ -1134,9 +1167,9 @@ def powerport_connect(request, pk):
else: else:
form = forms.PowerPortConnectionForm(instance=powerport, initial={ form = forms.PowerPortConnectionForm(instance=powerport, initial={
'site': request.GET.get('site', powerport.device.site), 'site': request.GET.get('site'),
'rack': request.GET.get('rack', None), 'rack': request.GET.get('rack'),
'pdu': request.GET.get('pdu', None), 'pdu': request.GET.get('pdu'),
'connection_status': CONNECTION_STATUS_CONNECTED, 'connection_status': CONNECTION_STATUS_CONNECTED,
}) })
@ -1154,7 +1187,7 @@ def powerport_disconnect(request, pk):
if not powerport.power_outlet: if not powerport.power_outlet:
messages.warning( 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) return redirect('dcim:device', pk=powerport.device.pk)
@ -1165,7 +1198,7 @@ def powerport_disconnect(request, pk):
powerport.power_outlet = None powerport.power_outlet = None
powerport.connection_status = None powerport.connection_status = None
powerport.save() powerport.save()
msg = u'Disconnected <a href="{}">{}</a> {} from <a href="{}">{}</a> {}'.format( msg = 'Disconnected <a href="{}">{}</a> {} from <a href="{}">{}</a> {}'.format(
powerport.device.get_absolute_url(), powerport.device.get_absolute_url(),
escape(powerport.device), escape(powerport.device),
escape(powerport.name), escape(powerport.name),
@ -1237,7 +1270,7 @@ def poweroutlet_connect(request, pk):
powerport.power_outlet = poweroutlet powerport.power_outlet = poweroutlet
powerport.connection_status = form.cleaned_data['connection_status'] powerport.connection_status = form.cleaned_data['connection_status']
powerport.save() powerport.save()
msg = u'Connected <a href="{}">{}</a> {} to <a href="{}">{}</a> {}'.format( msg = 'Connected <a href="{}">{}</a> {} to <a href="{}">{}</a> {}'.format(
powerport.device.get_absolute_url(), powerport.device.get_absolute_url(),
escape(powerport.device), escape(powerport.device),
escape(powerport.name), escape(powerport.name),
@ -1251,9 +1284,9 @@ def poweroutlet_connect(request, pk):
else: else:
form = forms.PowerOutletConnectionForm(initial={ form = forms.PowerOutletConnectionForm(initial={
'site': request.GET.get('site', poweroutlet.device.site), 'site': request.GET.get('site'),
'rack': request.GET.get('rack', None), 'rack': request.GET.get('rack'),
'device': request.GET.get('device', None), 'device': request.GET.get('device'),
'connection_status': CONNECTION_STATUS_CONNECTED, 'connection_status': CONNECTION_STATUS_CONNECTED,
}) })
@ -1271,7 +1304,7 @@ def poweroutlet_disconnect(request, pk):
if not hasattr(poweroutlet, 'connected_port'): if not hasattr(poweroutlet, 'connected_port'):
messages.warning( 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) return redirect('dcim:device', pk=poweroutlet.device.pk)
@ -1282,7 +1315,7 @@ def poweroutlet_disconnect(request, pk):
powerport.power_outlet = None powerport.power_outlet = None
powerport.connection_status = None powerport.connection_status = None
powerport.save() powerport.save()
msg = u'Disconnected <a href="{}">{}</a> {} from <a href="{}">{}</a> {}'.format( msg = 'Disconnected <a href="{}">{}</a> {} from <a href="{}">{}</a> {}'.format(
powerport.device.get_absolute_url(), powerport.device.get_absolute_url(),
escape(powerport.device), escape(powerport.device),
escape(powerport.name), escape(powerport.name),
@ -1396,7 +1429,7 @@ def devicebay_populate(request, pk):
device_bay.save() device_bay.save()
if not form.errors: 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) return redirect('dcim:device', pk=device_bay.device.pk)
else: else:
@ -1420,7 +1453,7 @@ def devicebay_depopulate(request, pk):
removed_device = device_bay.installed_device removed_device = device_bay.installed_device
device_bay.installed_device = None device_bay.installed_device = None
device_bay.save() 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) return redirect('dcim:device', pk=device_bay.device.pk)
else: else:
@ -1483,11 +1516,11 @@ class DeviceBulkAddComponentView(View):
else: else:
for field, errors in component_form.errors.as_data().items(): for field, errors in component_form.errors.as_data().items():
for e in errors: 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: if not form.errors:
self.model.objects.bulk_create(new_components) 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']) len(new_components), self.model._meta.verbose_name_plural, len(form.cleaned_data['pk'])
)) ))
return redirect('dcim:device_list') return redirect('dcim:device_list')
@ -1497,7 +1530,7 @@ class DeviceBulkAddComponentView(View):
selected_devices = Device.objects.filter(pk__in=pk_list) selected_devices = Device.objects.filter(pk__in=pk_list)
if not selected_devices: 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 redirect('dcim:device_list')
return render(request, 'dcim/device_bulk_add_component.html', { return render(request, 'dcim/device_bulk_add_component.html', {
@ -1559,7 +1592,7 @@ def interfaceconnection_add(request, pk):
if form.is_valid(): if form.is_valid():
interfaceconnection = form.save() interfaceconnection = form.save()
msg = u'Connected <a href="{}">{}</a> {} to <a href="{}">{}</a> {}'.format( msg = 'Connected <a href="{}">{}</a> {} to <a href="{}">{}</a> {}'.format(
interfaceconnection.interface_a.device.get_absolute_url(), interfaceconnection.interface_a.device.get_absolute_url(),
escape(interfaceconnection.interface_a.device), escape(interfaceconnection.interface_a.device),
escape(interfaceconnection.interface_a.name), escape(interfaceconnection.interface_a.name),
@ -1583,11 +1616,11 @@ def interfaceconnection_add(request, pk):
else: else:
form = forms.InterfaceConnectionForm(device, initial={ form = forms.InterfaceConnectionForm(device, initial={
'interface_a': request.GET.get('interface_a', None), 'interface_a': request.GET.get('interface_a'),
'site_b': request.GET.get('site_b', device.site), 'site_b': request.GET.get('site_b'),
'rack_b': request.GET.get('rack_b', None), 'rack_b': request.GET.get('rack_b'),
'device_b': request.GET.get('device_b', None), 'device_b': request.GET.get('device_b'),
'interface_b': request.GET.get('interface_b', None), 'interface_b': request.GET.get('interface_b'),
}) })
return render(request, 'dcim/interfaceconnection_edit.html', { return render(request, 'dcim/interfaceconnection_edit.html', {
@ -1607,7 +1640,7 @@ def interfaceconnection_delete(request, pk):
form = forms.InterfaceConnectionDeletionForm(request.POST) form = forms.InterfaceConnectionDeletionForm(request.POST)
if form.is_valid(): if form.is_valid():
interfaceconnection.delete() interfaceconnection.delete()
msg = u'Disconnected <a href="{}">{}</a> {} from <a href="{}">{}</a> {}'.format( msg = 'Disconnected <a href="{}">{}</a> {} from <a href="{}">{}</a> {}'.format(
interfaceconnection.interface_a.device.get_absolute_url(), interfaceconnection.interface_a.device.get_absolute_url(),
escape(interfaceconnection.interface_a.device), escape(interfaceconnection.interface_a.device),
escape(interfaceconnection.interface_a.name), escape(interfaceconnection.interface_a.name),

View File

@ -1,3 +1,5 @@
from __future__ import unicode_literals
from django import forms from django import forms
from django.contrib import admin from django.contrib import admin
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe

View File

@ -1,9 +1,11 @@
from django.contrib.contenttypes.models import ContentType from __future__ import unicode_literals
from django.db import transaction
from rest_framework import serializers from rest_framework import serializers
from rest_framework.exceptions import ValidationError 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 from extras.models import CF_TYPE_SELECT, CustomField, CustomFieldChoice, CustomFieldValue
@ -25,14 +27,14 @@ class CustomFieldsSerializer(serializers.BaseSerializer):
# Validate custom field name # Validate custom field name
if field_name not in custom_fields: 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 # Validate selected choice
cf = custom_fields[field_name] cf = custom_fields[field_name]
if cf.type == CF_TYPE_SELECT: if cf.type == CF_TYPE_SELECT:
valid_choices = [c.pk for c in cf.choices.all()] valid_choices = [c.pk for c in cf.choices.all()]
if value not in valid_choices: 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 # Check for missing required fields
missing_fields = [] missing_fields = []
@ -40,7 +42,7 @@ class CustomFieldsSerializer(serializers.BaseSerializer):
if field.required and field_name not in data: if field.required and field_name not in data:
missing_fields.append(field_name) missing_fields.append(field_name)
if missing_fields: 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 return data

View File

@ -1,7 +1,9 @@
from rest_framework import serializers from __future__ import unicode_literals
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from rest_framework import serializers
from dcim.api.serializers import NestedDeviceSerializer, NestedRackSerializer, NestedSiteSerializer from dcim.api.serializers import NestedDeviceSerializer, NestedRackSerializer, NestedSiteSerializer
from dcim.models import Device, Rack, Site from dcim.models import Device, Rack, Site
from extras.models import ( from extras.models import (

View File

@ -1,3 +1,5 @@
from __future__ import unicode_literals
from rest_framework import routers from rest_framework import routers
from . import views from . import views

View File

@ -1,3 +1,5 @@
from __future__ import unicode_literals
from rest_framework.decorators import detail_route from rest_framework.decorators import detail_route
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet

View File

@ -1,3 +1,5 @@
from __future__ import unicode_literals
import django_filters import django_filters
from django.contrib.auth.models import User from django.contrib.auth.models import User

View File

@ -1,3 +1,4 @@
from __future__ import unicode_literals
from collections import OrderedDict from collections import OrderedDict
from django import forms from django import forms
@ -104,7 +105,7 @@ class CustomFieldForm(forms.ModelForm):
obj_id=self.instance.pk) obj_id=self.instance.pk)
except CustomFieldValue.DoesNotExist: except CustomFieldValue.DoesNotExist:
# Skip this field if none exists already and its value is empty # 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 continue
cfv = CustomFieldValue( cfv = CustomFieldValue(
field=self.fields[field_name].model, field=self.fields[field_name].model,

View File

@ -1,3 +1,5 @@
from __future__ import unicode_literals
from getpass import getpass from getpass import getpass
from ncclient.transport.errors import AuthenticationError from ncclient.transport.errors import AuthenticationError
from paramiko import AuthenticationException from paramiko import AuthenticationException

View File

@ -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')]),
),
]

View File

@ -1,3 +1,4 @@
from __future__ import unicode_literals
from collections import OrderedDict from collections import OrderedDict
from datetime import date from datetime import date
import graphviz import graphviz
@ -175,7 +176,7 @@ class CustomFieldValue(models.Model):
unique_together = ['field', 'obj_type', 'obj_id'] unique_together = ['field', 'obj_type', 'obj_id']
def __str__(self): def __str__(self):
return u'{} {}'.format(self.obj, self.field) return '{} {}'.format(self.obj, self.field)
@property @property
def value(self): def value(self):
@ -269,7 +270,7 @@ class ExportTemplate(models.Model):
] ]
def __str__(self): 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): def to_response(self, context_dict, filename):
""" """
@ -387,7 +388,7 @@ def image_upload(instance, filename):
elif instance.name: elif instance.name:
filename = 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 @python_2_unicode_compatible
@ -503,8 +504,8 @@ class UserAction(models.Model):
def __str__(self): def __str__(self):
if self.message: if self.message:
return u'{} {}'.format(self.user, self.message) return '{} {}'.format(self.user, self.message)
return u'{} {} {}'.format(self.user, self.get_action_display(), self.content_type) return '{} {} {}'.format(self.user, self.get_action_display(), self.content_type)
def icon(self): def icon(self):
if self.action in [ACTION_CREATE, ACTION_BULK_CREATE, ACTION_IMPORT]: if self.action in [ACTION_CREATE, ACTION_BULK_CREATE, ACTION_IMPORT]:

View File

@ -1,8 +1,10 @@
from __future__ import unicode_literals
import re
import time
from ncclient import manager from ncclient import manager
import paramiko import paramiko
import re
import xmltodict import xmltodict
import time
CONNECT_TIMEOUT = 5 # seconds CONNECT_TIMEOUT = 5 # seconds

View File

@ -1,3 +1,5 @@
from __future__ import unicode_literals
from rest_framework import status from rest_framework import status
from rest_framework.test import APITestCase from rest_framework.test import APITestCase

View File

@ -1,3 +1,4 @@
from __future__ import unicode_literals
from datetime import date from datetime import date
from rest_framework import status from rest_framework import status
@ -9,7 +10,6 @@ from django.test import TestCase
from django.urls import reverse from django.urls import reverse
from dcim.models import Site from dcim.models import Site
from extras.models import ( from extras.models import (
CustomField, CustomFieldValue, CustomFieldChoice, CF_TYPE_TEXT, CF_TYPE_INTEGER, CF_TYPE_BOOLEAN, CF_TYPE_DATE, CustomField, CustomFieldValue, CustomFieldChoice, CF_TYPE_TEXT, CF_TYPE_INTEGER, CF_TYPE_BOOLEAN, CF_TYPE_DATE,
CF_TYPE_SELECT, CF_TYPE_URL, CF_TYPE_SELECT, CF_TYPE_URL,

View File

@ -1,3 +1,5 @@
from __future__ import unicode_literals
from django.conf.urls import url from django.conf.urls import url
from extras import views from extras import views

View File

@ -1,3 +1,5 @@
from __future__ import unicode_literals
from django.contrib.auth.mixins import PermissionRequiredMixin from django.contrib.auth.mixins import PermissionRequiredMixin
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404

View File

@ -1,3 +1,5 @@
from __future__ import unicode_literals
from rest_framework import serializers from rest_framework import serializers
from rest_framework.validators import UniqueTogetherValidator from rest_framework.validators import UniqueTogetherValidator

View File

@ -1,3 +1,5 @@
from __future__ import unicode_literals
from rest_framework import routers from rest_framework import routers
from . import views from . import views

View File

@ -1,3 +1,5 @@
from __future__ import unicode_literals
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF

View File

@ -1,3 +1,5 @@
from __future__ import unicode_literals
from django.apps import AppConfig from django.apps import AppConfig

View File

@ -1,3 +1,5 @@
from __future__ import unicode_literals
from netaddr import IPNetwork from netaddr import IPNetwork
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError

View File

@ -1,3 +1,5 @@
from __future__ import unicode_literals
import django_filters import django_filters
from netaddr import IPNetwork from netaddr import IPNetwork
from netaddr.core import AddrFormatError from netaddr.core import AddrFormatError
@ -8,7 +10,6 @@ from dcim.models import Site, Device, Interface
from extras.filters import CustomFieldFilterSet from extras.filters import CustomFieldFilterSet
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.filters import NullableModelMultipleChoiceFilter, NumericInFilter from utilities.filters import NullableModelMultipleChoiceFilter, NumericInFilter
from .models import ( from .models import (
Aggregate, IPAddress, IPADDRESS_STATUS_CHOICES, Prefix, PREFIX_STATUS_CHOICES, RIR, Role, Service, VLAN, Aggregate, IPAddress, IPADDRESS_STATUS_CHOICES, Prefix, PREFIX_STATUS_CHOICES, RIR, Role, Service, VLAN,
VLAN_STATUS_CHOICES, VLANGroup, VRF, VLAN_STATUS_CHOICES, VLANGroup, VRF,

View File

@ -1,3 +1,5 @@
from __future__ import unicode_literals
from netaddr import IPNetwork, AddrFormatError from netaddr import IPNetwork, AddrFormatError
from django import forms from django import forms

View File

@ -1,3 +1,5 @@
from __future__ import unicode_literals
from django import forms from django import forms
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db.models import Count from django.db.models import Count
@ -10,7 +12,6 @@ from utilities.forms import (
APISelect, BootstrapMixin, BulkEditNullBooleanSelect, BulkImportForm, ChainedModelChoiceField, CSVDataField, APISelect, BootstrapMixin, BulkEditNullBooleanSelect, BulkImportForm, ChainedModelChoiceField, CSVDataField,
ExpandableIPAddressField, FilterChoiceField, Livesearch, ReturnURLForm, SlugField, add_blank_choice, ExpandableIPAddressField, FilterChoiceField, Livesearch, ReturnURLForm, SlugField, add_blank_choice,
) )
from .models import ( from .models import (
Aggregate, IPAddress, IPADDRESS_STATUS_CHOICES, Prefix, PREFIX_STATUS_CHOICES, RIR, Role, Service, VLAN, Aggregate, IPAddress, IPADDRESS_STATUS_CHOICES, Prefix, PREFIX_STATUS_CHOICES, RIR, Role, Service, VLAN,
VLANGroup, VLAN_STATUS_CHOICES, VRF, VLANGroup, VLAN_STATUS_CHOICES, VRF,
@ -167,12 +168,21 @@ class RoleForm(BootstrapMixin, forms.ModelForm):
class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldForm): class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldForm):
site = forms.ModelChoiceField( 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'} attrs={'filter-for': 'vlan', 'nullable': 'true'}
) )
) )
vlan = ChainedModelChoiceField( 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' api_url='/api/ipam/vlans/?site_id={{site}}', display_field='display_name'
) )
) )
@ -270,7 +280,7 @@ def prefix_status_choices():
status_counts = {} status_counts = {}
for status in Prefix.objects.values('status').annotate(count=Count('status')).order_by('status'): for status in Prefix.objects.values('status').annotate(count=Count('status')).order_by('status'):
status_counts[status['status']] = status['count'] 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): class PrefixFilterForm(BootstrapMixin, CustomFieldFilterForm):
@ -321,7 +331,9 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm)
) )
interface_rack = ChainedModelChoiceField( interface_rack = ChainedModelChoiceField(
queryset=Rack.objects.all(), queryset=Rack.objects.all(),
chains={'site': 'interface_site'}, chains=(
('site', 'interface_site'),
),
required=False, required=False,
label='Rack', label='Rack',
widget=APISelect( widget=APISelect(
@ -332,7 +344,10 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm)
) )
interface_device = ChainedModelChoiceField( interface_device = ChainedModelChoiceField(
queryset=Device.objects.all(), queryset=Device.objects.all(),
chains={'site': 'interface_site', 'rack': 'interface_rack'}, chains=(
('site', 'interface_site'),
('rack', 'interface_rack'),
),
required=False, required=False,
label='Device', label='Device',
widget=APISelect( widget=APISelect(
@ -343,7 +358,9 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm)
) )
interface = ChainedModelChoiceField( interface = ChainedModelChoiceField(
queryset=Interface.objects.all(), queryset=Interface.objects.all(),
chains={'device': 'interface_device'}, chains=(
('device', 'interface_device'),
),
required=False, required=False,
widget=APISelect( widget=APISelect(
api_url='/api/dcim/interfaces/?device_id={{interface_device}}' api_url='/api/dcim/interfaces/?device_id={{interface_device}}'
@ -354,34 +371,41 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm)
required=False, required=False,
label='Site', label='Site',
widget=forms.Select( widget=forms.Select(
attrs={'filter-for': 'nat_device'} attrs={'filter-for': 'nat_rack'}
) )
) )
nat_rack = ChainedModelChoiceField( nat_rack = ChainedModelChoiceField(
queryset=Rack.objects.all(), queryset=Rack.objects.all(),
chains={'site': 'nat_site'}, chains=(
('site', 'nat_site'),
),
required=False, required=False,
label='Rack', label='Rack',
widget=APISelect( widget=APISelect(
api_url='/api/dcim/racks/?site_id={{interface_site}}', api_url='/api/dcim/racks/?site_id={{nat_site}}',
display_field='display_name', display_field='display_name',
attrs={'filter-for': 'nat_device', 'nullable': 'true'} attrs={'filter-for': 'nat_device', 'nullable': 'true'}
) )
) )
nat_device = ChainedModelChoiceField( nat_device = ChainedModelChoiceField(
queryset=Device.objects.all(), queryset=Device.objects.all(),
chains={'site': 'nat_site'}, chains=(
('site', 'nat_site'),
('rack', 'nat_rack'),
),
required=False, required=False,
label='Device', label='Device',
widget=APISelect( 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', display_field='display_name',
attrs={'filter-for': 'nat_inside'} attrs={'filter-for': 'nat_inside'}
) )
) )
nat_inside = ChainedModelChoiceField( nat_inside = ChainedModelChoiceField(
queryset=IPAddress.objects.all(), queryset=IPAddress.objects.all(),
chains={'interface__device': 'nat_device'}, chains=(
('interface__device', 'nat_device'),
),
required=False, required=False,
label='IP Address', label='IP Address',
widget=APISelect( widget=APISelect(
@ -391,7 +415,7 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm)
) )
livesearch = forms.CharField( livesearch = forms.CharField(
required=False, required=False,
label='IP Address', label='Search',
widget=Livesearch( widget=Livesearch(
query_key='q', query_key='q',
query_url='ipam-api:ipaddress-list', query_url='ipam-api:ipaddress-list',
@ -404,8 +428,8 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm)
class Meta: class Meta:
model = IPAddress model = IPAddress
fields = [ fields = [
'address', 'vrf', 'status', 'description', 'interface', 'primary_for_device', 'nat_inside', 'tenant_group', 'address', 'vrf', 'status', 'description', 'interface', 'primary_for_device', 'nat_site', 'nat_rack',
'tenant', 'nat_inside', 'tenant_group', 'tenant',
] ]
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -567,7 +591,7 @@ def ipaddress_status_choices():
status_counts = {} status_counts = {}
for status in IPAddress.objects.values('status').annotate(count=Count('status')).order_by('status'): for status in IPAddress.objects.values('status').annotate(count=Count('status')).order_by('status'):
status_counts[status['status']] = status['count'] 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): class IPAddressFilterForm(BootstrapMixin, CustomFieldFilterForm):
@ -626,7 +650,9 @@ class VLANForm(BootstrapMixin, TenancyForm, CustomFieldForm):
) )
group = ChainedModelChoiceField( group = ChainedModelChoiceField(
queryset=VLANGroup.objects.all(), queryset=VLANGroup.objects.all(),
chains={'site': 'site'}, chains=(
('site', 'site'),
),
required=False, required=False,
label='Group', label='Group',
widget=APISelect( widget=APISelect(
@ -720,7 +746,7 @@ def vlan_status_choices():
status_counts = {} status_counts = {}
for status in VLAN.objects.values('status').annotate(count=Count('status')).order_by('status'): for status in VLAN.objects.values('status').annotate(count=Count('status')).order_by('status'):
status_counts[status['status']] = status['count'] 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): class VLANFilterForm(BootstrapMixin, CustomFieldFilterForm):

View File

@ -1,3 +1,5 @@
from __future__ import unicode_literals
from django.db.models import Lookup, Transform, IntegerField from django.db.models import Lookup, Transform, IntegerField
from django.db.models.lookups import BuiltinLookup from django.db.models.lookups import BuiltinLookup

View File

@ -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'),
),
]

View File

@ -1,3 +1,5 @@
from __future__ import unicode_literals
from netaddr import IPNetwork, cidr_merge from netaddr import IPNetwork, cidr_merge
from django.conf import settings from django.conf import settings
@ -15,7 +17,6 @@ from tenancy.models import Tenant
from utilities.models import CreatedUpdatedModel from utilities.models import CreatedUpdatedModel
from utilities.sql import NullsFirstQuerySet from utilities.sql import NullsFirstQuerySet
from utilities.utils import csv_format from utilities.utils import csv_format
from .fields import IPNetworkField, IPAddressField from .fields import IPNetworkField, IPAddressField
@ -499,7 +500,7 @@ class VLANGroup(models.Model):
def __str__(self): def __str__(self):
if self.site is None: if self.site is None:
return self.name return self.name
return u'{} - {}'.format(self.site.name, self.name) return '{} - {}'.format(self.site.name, self.name)
def get_absolute_url(self): def get_absolute_url(self):
return "{}?group_id={}".format(reverse('ipam:vlan_list'), self.pk) return "{}?group_id={}".format(reverse('ipam:vlan_list'), self.pk)
@ -566,7 +567,7 @@ class VLAN(CreatedUpdatedModel, CustomFieldModel):
@property @property
def display_name(self): def display_name(self):
if self.vid and self.name: if self.vid and self.name:
return u"{} ({})".format(self.vid, self.name) return "{} ({})".format(self.vid, self.name)
return None return None
def get_status_class(self): def get_status_class(self):
@ -593,4 +594,4 @@ class Service(CreatedUpdatedModel):
unique_together = ['device', 'protocol', 'port'] unique_together = ['device', 'protocol', 'port']
def __str__(self): def __str__(self):
return u'{} ({}/{})'.format(self.name, self.port, self.get_protocol_display()) return '{} ({}/{})'.format(self.name, self.port, self.get_protocol_display())

View File

@ -1,8 +1,9 @@
from __future__ import unicode_literals
import django_tables2 as tables import django_tables2 as tables
from django_tables2.utils import Accessor from django_tables2.utils import Accessor
from utilities.tables import BaseTable, SearchTable, ToggleColumn from utilities.tables import BaseTable, SearchTable, ToggleColumn
from .models import Aggregate, IPAddress, Prefix, RIR, Role, VLAN, VLANGroup, VRF from .models import Aggregate, IPAddress, Prefix, RIR, Role, VLAN, VLANGroup, VRF

View File

@ -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 import status
from rest_framework.test import APITestCase from rest_framework.test import APITestCase

View File

@ -1,9 +1,11 @@
from __future__ import unicode_literals
import netaddr import netaddr
from django.core.exceptions import ValidationError
from django.test import TestCase, override_settings from django.test import TestCase, override_settings
from ipam.models import IPAddress, Prefix, VRF from ipam.models import IPAddress, Prefix, VRF
from django.core.exceptions import ValidationError
class TestPrefix(TestCase): class TestPrefix(TestCase):

View File

@ -1,3 +1,5 @@
from __future__ import unicode_literals
from django.conf.urls import url from django.conf.urls import url
from . import views from . import views
@ -12,7 +14,7 @@ urlpatterns = [
url(r'^vrfs/import/$', views.VRFBulkImportView.as_view(), name='vrf_import'), 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/edit/$', views.VRFBulkEditView.as_view(), name='vrf_bulk_edit'),
url(r'^vrfs/delete/$', views.VRFBulkDeleteView.as_view(), name='vrf_bulk_delete'), url(r'^vrfs/delete/$', views.VRFBulkDeleteView.as_view(), name='vrf_bulk_delete'),
url(r'^vrfs/(?P<pk>\d+)/$', views.vrf, name='vrf'), url(r'^vrfs/(?P<pk>\d+)/$', views.VRFView.as_view(), name='vrf'),
url(r'^vrfs/(?P<pk>\d+)/edit/$', views.VRFEditView.as_view(), name='vrf_edit'), url(r'^vrfs/(?P<pk>\d+)/edit/$', views.VRFEditView.as_view(), name='vrf_edit'),
url(r'^vrfs/(?P<pk>\d+)/delete/$', views.VRFDeleteView.as_view(), name='vrf_delete'), url(r'^vrfs/(?P<pk>\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/import/$', views.AggregateBulkImportView.as_view(), name='aggregate_import'),
url(r'^aggregates/edit/$', views.AggregateBulkEditView.as_view(), name='aggregate_bulk_edit'), 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/delete/$', views.AggregateBulkDeleteView.as_view(), name='aggregate_bulk_delete'),
url(r'^aggregates/(?P<pk>\d+)/$', views.aggregate, name='aggregate'), url(r'^aggregates/(?P<pk>\d+)/$', views.AggregateView.as_view(), name='aggregate'),
url(r'^aggregates/(?P<pk>\d+)/edit/$', views.AggregateEditView.as_view(), name='aggregate_edit'), url(r'^aggregates/(?P<pk>\d+)/edit/$', views.AggregateEditView.as_view(), name='aggregate_edit'),
url(r'^aggregates/(?P<pk>\d+)/delete/$', views.AggregateDeleteView.as_view(), name='aggregate_delete'), url(r'^aggregates/(?P<pk>\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/import/$', views.PrefixBulkImportView.as_view(), name='prefix_import'),
url(r'^prefixes/edit/$', views.PrefixBulkEditView.as_view(), name='prefix_bulk_edit'), 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/delete/$', views.PrefixBulkDeleteView.as_view(), name='prefix_bulk_delete'),
url(r'^prefixes/(?P<pk>\d+)/$', views.prefix, name='prefix'), url(r'^prefixes/(?P<pk>\d+)/$', views.PrefixView.as_view(), name='prefix'),
url(r'^prefixes/(?P<pk>\d+)/edit/$', views.PrefixEditView.as_view(), name='prefix_edit'), url(r'^prefixes/(?P<pk>\d+)/edit/$', views.PrefixEditView.as_view(), name='prefix_edit'),
url(r'^prefixes/(?P<pk>\d+)/delete/$', views.PrefixDeleteView.as_view(), name='prefix_delete'), url(r'^prefixes/(?P<pk>\d+)/delete/$', views.PrefixDeleteView.as_view(), name='prefix_delete'),
url(r'^prefixes/(?P<pk>\d+)/ip-addresses/$', views.prefix_ipaddresses, name='prefix_ipaddresses'), url(r'^prefixes/(?P<pk>\d+)/ip-addresses/$', views.PrefixIPAddressesView.as_view(), name='prefix_ipaddresses'),
# IP addresses # IP addresses
url(r'^ip-addresses/$', views.IPAddressListView.as_view(), name='ipaddress_list'), 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/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/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/delete/$', views.IPAddressBulkDeleteView.as_view(), name='ipaddress_bulk_delete'),
url(r'^ip-addresses/(?P<pk>\d+)/$', views.ipaddress, name='ipaddress'), url(r'^ip-addresses/(?P<pk>\d+)/$', views.IPAddressView.as_view(), name='ipaddress'),
url(r'^ip-addresses/(?P<pk>\d+)/edit/$', views.IPAddressEditView.as_view(), name='ipaddress_edit'), url(r'^ip-addresses/(?P<pk>\d+)/edit/$', views.IPAddressEditView.as_view(), name='ipaddress_edit'),
url(r'^ip-addresses/(?P<pk>\d+)/delete/$', views.IPAddressDeleteView.as_view(), name='ipaddress_delete'), url(r'^ip-addresses/(?P<pk>\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/import/$', views.VLANBulkImportView.as_view(), name='vlan_import'),
url(r'^vlans/edit/$', views.VLANBulkEditView.as_view(), name='vlan_bulk_edit'), 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/delete/$', views.VLANBulkDeleteView.as_view(), name='vlan_bulk_delete'),
url(r'^vlans/(?P<pk>\d+)/$', views.vlan, name='vlan'), url(r'^vlans/(?P<pk>\d+)/$', views.VLANView.as_view(), name='vlan'),
url(r'^vlans/(?P<pk>\d+)/edit/$', views.VLANEditView.as_view(), name='vlan_edit'), url(r'^vlans/(?P<pk>\d+)/edit/$', views.VLANEditView.as_view(), name='vlan_edit'),
url(r'^vlans/(?P<pk>\d+)/delete/$', views.VLANDeleteView.as_view(), name='vlan_delete'), url(r'^vlans/(?P<pk>\d+)/delete/$', views.VLANDeleteView.as_view(), name='vlan_delete'),

View File

@ -1,3 +1,5 @@
from __future__ import unicode_literals
from django_tables2 import RequestConfig from django_tables2 import RequestConfig
import netaddr import netaddr
@ -6,13 +8,13 @@ from django.contrib.auth.mixins import PermissionRequiredMixin
from django.db.models import Count, Q from django.db.models import Count, Q
from django.shortcuts import get_object_or_404, render from django.shortcuts import get_object_or_404, render
from django.urls import reverse from django.urls import reverse
from django.views.generic import View
from dcim.models import Device from dcim.models import Device
from utilities.paginator import EnhancedPaginator from utilities.paginator import EnhancedPaginator
from utilities.views import ( from utilities.views import (
BulkAddView, BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView, BulkAddView, BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
) )
from . import filters, forms, tables from . import filters, forms, tables
from .models import ( from .models import (
Aggregate, IPAddress, PREFIX_STATUS_ACTIVE, PREFIX_STATUS_DEPRECATED, PREFIX_STATUS_RESERVED, Prefix, RIR, Role, Aggregate, IPAddress, PREFIX_STATUS_ACTIVE, PREFIX_STATUS_DEPRECATED, PREFIX_STATUS_RESERVED, Prefix, RIR, Role,
@ -96,7 +98,9 @@ class VRFListView(ObjectListView):
template_name = 'ipam/vrf_list.html' template_name = 'ipam/vrf_list.html'
def vrf(request, pk): class VRFView(View):
def get(self, request, pk):
vrf = get_object_or_404(VRF.objects.all(), pk=pk) vrf = get_object_or_404(VRF.objects.all(), pk=pk)
prefix_table = tables.PrefixBriefTable( prefix_table = tables.PrefixBriefTable(
@ -281,13 +285,20 @@ class AggregateListView(ObjectListView):
} }
def aggregate(request, pk): class AggregateView(View):
def get(self, request, pk):
aggregate = get_object_or_404(Aggregate, pk=pk) aggregate = get_object_or_404(Aggregate, pk=pk)
# Find all child prefixes contained by this aggregate # Find all child prefixes contained by this aggregate
child_prefixes = Prefix.objects.filter(prefix__net_contained_or_equal=str(aggregate.prefix))\ child_prefixes = Prefix.objects.filter(
.select_related('site', 'role').annotate_depth(limit=0) 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) child_prefixes = add_available_prefixes(aggregate.prefix, child_prefixes)
prefix_table = tables.PrefixTable(child_prefixes) prefix_table = tables.PrefixTable(child_prefixes)
@ -394,7 +405,9 @@ class PrefixListView(ObjectListView):
return self.queryset.annotate_depth(limit=limit) return self.queryset.annotate_depth(limit=limit)
def prefix(request, pk): class PrefixView(View):
def get(self, request, pk):
prefix = get_object_or_404(Prefix.objects.select_related( prefix = get_object_or_404(Prefix.objects.select_related(
'vrf', 'site__region', 'tenant__group', 'vlan__group', 'role' 'vrf', 'site__region', 'tenant__group', 'vlan__group', 'role'
@ -406,25 +419,38 @@ def prefix(request, pk):
aggregate = None aggregate = None
# Count child IP addresses # Count child IP addresses
ipaddress_count = IPAddress.objects.filter(vrf=prefix.vrf, address__net_host_contained=str(prefix.prefix))\ ipaddress_count = IPAddress.objects.filter(
.count() vrf=prefix.vrf, address__net_host_contained=str(prefix.prefix)
).count()
# Parent prefixes table # Parent prefixes table
parent_prefixes = Prefix.objects.filter(Q(vrf=prefix.vrf) | Q(vrf__isnull=True))\ parent_prefixes = Prefix.objects.filter(
.filter(prefix__net_contains=str(prefix.prefix))\ Q(vrf=prefix.vrf) | Q(vrf__isnull=True)
.select_related('site', 'role').annotate_depth() ).filter(
prefix__net_contains=str(prefix.prefix)
).select_related(
'site', 'role'
).annotate_depth()
parent_prefix_table = tables.PrefixBriefTable(parent_prefixes) parent_prefix_table = tables.PrefixBriefTable(parent_prefixes)
parent_prefix_table.exclude = ('vrf',) parent_prefix_table.exclude = ('vrf',)
# Duplicate prefixes table # Duplicate prefixes table
duplicate_prefixes = Prefix.objects.filter(vrf=prefix.vrf, prefix=str(prefix.prefix)).exclude(pk=prefix.pk)\ duplicate_prefixes = Prefix.objects.filter(
.select_related('site', 'role') 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 = tables.PrefixBriefTable(list(duplicate_prefixes))
duplicate_prefix_table.exclude = ('vrf',) duplicate_prefix_table.exclude = ('vrf',)
# Child prefixes table # Child prefixes table
child_prefixes = Prefix.objects.filter(vrf=prefix.vrf, prefix__net_contained=str(prefix.prefix))\ child_prefixes = Prefix.objects.filter(
.select_related('site', 'role').annotate_depth(limit=0) vrf=prefix.vrf, prefix__net_contained=str(prefix.prefix)
).select_related(
'site', 'role'
).annotate_depth(limit=0)
if child_prefixes: if child_prefixes:
child_prefixes = add_available_prefixes(prefix.prefix, child_prefixes) child_prefixes = add_available_prefixes(prefix.prefix, child_prefixes)
child_prefix_table = tables.PrefixTable(child_prefixes) child_prefix_table = tables.PrefixTable(child_prefixes)
@ -456,6 +482,45 @@ def prefix(request, pk):
}) })
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): class PrefixEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.change_prefix' permission_required = 'ipam.change_prefix'
model = Prefix model = Prefix
@ -495,40 +560,6 @@ class PrefixBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
default_return_url = 'ipam:prefix_list' 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 # IP addresses
# #
@ -541,24 +572,39 @@ class IPAddressListView(ObjectListView):
template_name = 'ipam/ipaddress_list.html' template_name = 'ipam/ipaddress_list.html'
def ipaddress(request, pk): class IPAddressView(View):
def get(self, request, pk):
ipaddress = get_object_or_404(IPAddress.objects.select_related('interface__device'), pk=pk) ipaddress = get_object_or_404(IPAddress.objects.select_related('interface__device'), pk=pk)
# Parent prefixes table # Parent prefixes table
parent_prefixes = Prefix.objects.filter(vrf=ipaddress.vrf, prefix__net_contains=str(ipaddress.address.ip))\ parent_prefixes = Prefix.objects.filter(
.select_related('site', 'role') 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 = tables.PrefixBriefTable(list(parent_prefixes))
parent_prefixes_table.exclude = ('vrf',) parent_prefixes_table.exclude = ('vrf',)
# Duplicate IPs table # Duplicate IPs table
duplicate_ips = IPAddress.objects.filter(vrf=ipaddress.vrf, address=str(ipaddress.address))\ duplicate_ips = IPAddress.objects.filter(
.exclude(pk=ipaddress.pk).select_related('interface__device', 'nat_inside') 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)) duplicate_ips_table = tables.IPAddressBriefTable(list(duplicate_ips))
# Related IP table # Related IP table
related_ips = IPAddress.objects.select_related('interface__device').exclude(address=str(ipaddress.address))\ related_ips = IPAddress.objects.select_related(
.filter(vrf=ipaddress.vrf, address__net_contained_or_equal=str(ipaddress.address)) '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)) related_ips_table = tables.IPAddressBriefTable(list(related_ips))
return render(request, 'ipam/ipaddress.html', { return render(request, 'ipam/ipaddress.html', {
@ -669,9 +715,13 @@ class VLANListView(ObjectListView):
template_name = 'ipam/vlan_list.html' 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) def get(self, request, pk):
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') prefixes = Prefix.objects.filter(vlan=vlan).select_related('vrf', 'site', 'role')
prefix_table = tables.PrefixBriefTable(list(prefixes)) prefix_table = tables.PrefixBriefTable(list(prefixes))
prefix_table.exclude = ('vlan',) prefix_table.exclude = ('vlan',)

View File

@ -1,3 +1,5 @@
from __future__ import unicode_literals
from django import forms from django import forms
from utilities.forms import BootstrapMixin from utilities.forms import BootstrapMixin

View File

@ -13,7 +13,7 @@ except ImportError:
) )
VERSION = '2.0.3' VERSION = '2.0.4'
# Import local configuration # Import local configuration
ALLOWED_HOSTS = DATABASE = SECRET_KEY = None ALLOWED_HOSTS = DATABASE = SECRET_KEY = None
@ -112,6 +112,7 @@ INSTALLED_APPS = (
'django.contrib.humanize', 'django.contrib.humanize',
'corsheaders', 'corsheaders',
'debug_toolbar', 'debug_toolbar',
'django_filters',
'django_tables2', 'django_tables2',
'mptt', 'mptt',
'rest_framework', 'rest_framework',
@ -180,8 +181,8 @@ STATICFILES_DIRS = (
) )
# Media # Media
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, '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.) # 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 DATA_UPLOAD_MAX_NUMBER_FIELDS = None

View File

@ -1,3 +1,5 @@
from __future__ import unicode_literals
from rest_framework_swagger.views import get_swagger_view from rest_framework_swagger.views import get_swagger_view
from django.conf import settings from django.conf import settings
@ -5,8 +7,8 @@ from django.conf.urls import include, url
from django.contrib import admin from django.contrib import admin
from django.views.static import serve from django.views.static import serve
from netbox.views import APIRootView, home, handle_500, SearchView, trigger_500 from netbox.views import APIRootView, handle_500, HomeView, SearchView, trigger_500
from users.views import login, logout from users.views import LoginView, LogoutView
handler500 = handle_500 handler500 = handle_500
@ -15,12 +17,12 @@ swagger_view = get_swagger_view(title='NetBox API')
_patterns = [ _patterns = [
# Base views # Base views
url(r'^$', home, name='home'), url(r'^$', HomeView.as_view(), name='home'),
url(r'^search/$', SearchView.as_view(), name='search'), url(r'^search/$', SearchView.as_view(), name='search'),
# Login/logout # Login/logout
url(r'^login/$', login, name='login'), url(r'^login/$', LoginView.as_view(), name='login'),
url(r'^logout/$', logout, name='logout'), url(r'^logout/$', LogoutView.as_view(), name='logout'),
# Apps # Apps
url(r'^circuits/', include('circuits.urls')), url(r'^circuits/', include('circuits.urls')),

View File

@ -1,3 +1,4 @@
from __future__ import unicode_literals
from collections import OrderedDict from collections import OrderedDict
import sys import sys
@ -115,7 +116,10 @@ SEARCH_TYPES = OrderedDict((
)) ))
def home(request): class HomeView(View):
template_name = 'home.html'
def get(self, request):
stats = { stats = {
@ -146,7 +150,7 @@ def home(request):
} }
return render(request, 'home.html', { return render(request, self.template_name, {
'search_form': SearchForm(), 'search_form': SearchForm(),
'stats': stats, 'stats': stats,
'topology_maps': TopologyMap.objects.filter(site__isnull=True), 'topology_maps': TopologyMap.objects.filter(site__isnull=True),
@ -192,7 +196,7 @@ class SearchView(View):
results.append({ results.append({
'name': queryset.model._meta.verbose_name_plural, 'name': queryset.model._meta.verbose_name_plural,
'table': table, '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', { return render(request, 'search.html', {
@ -206,7 +210,7 @@ class APIRootView(APIView):
exclude_from_schema = True exclude_from_schema = True
def get_view_name(self): def get_view_name(self):
return u"API Root" return "API Root"
def get(self, request, format=None): 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 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 " raise Exception(
"person you are.") "Congratulations, you've triggered an exception! Go tell all your friends what an exceptional person you are."
)

View File

@ -11,6 +11,7 @@ import os
from django.core.wsgi import get_wsgi_application from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "netbox.settings") os.environ.setdefault("DJANGO_SETTINGS_MODULE", "netbox.settings")
application = get_wsgi_application() application = get_wsgi_application()

View File

@ -1,3 +1,5 @@
from __future__ import unicode_literals
from django.contrib import admin, messages from django.contrib import admin, messages
from django.shortcuts import redirect, render from django.shortcuts import redirect, render
@ -34,8 +36,8 @@ class UserKeyAdmin(admin.ModelAdmin):
try: try:
my_userkey = UserKey.objects.get(user=request.user) my_userkey = UserKey.objects.get(user=request.user)
except UserKey.DoesNotExist: except UserKey.DoesNotExist:
messages.error(request, u"You do not have an active User Key.") messages.error(request, "You do not have an active User Key.")
return redirect('/admin/secrets/userkey/') return redirect('admin:secrets_userkey_changelist')
if 'activate' in request.POST: if 'activate' in request.POST:
form = ActivateUserKeyForm(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']) master_key = my_userkey.get_master_key(form.cleaned_data['secret_key'])
for uk in form.cleaned_data['_selected_action']: for uk in form.cleaned_data['_selected_action']:
uk.activate(master_key) uk.activate(master_key)
return redirect('/admin/secrets/userkey/') return redirect('admin:secrets_userkey_changelist')
except ValueError: 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: else:
form = ActivateUserKeyForm(initial={'_selected_action': request.POST.getlist(admin.ACTION_CHECKBOX_NAME)}) form = ActivateUserKeyForm(initial={'_selected_action': request.POST.getlist(admin.ACTION_CHECKBOX_NAME)})

View File

@ -1,3 +1,5 @@
from __future__ import unicode_literals
from rest_framework import serializers from rest_framework import serializers
from rest_framework.validators import UniqueTogetherValidator from rest_framework.validators import UniqueTogetherValidator

View File

@ -1,3 +1,5 @@
from __future__ import unicode_literals
from rest_framework import routers from rest_framework import routers
from . import views from . import views

View File

@ -1,13 +1,14 @@
from __future__ import unicode_literals
import base64 import base64
from Crypto.PublicKey import RSA from Crypto.PublicKey import RSA
from django.http import HttpResponseBadRequest
from rest_framework.exceptions import ValidationError from rest_framework.exceptions import ValidationError
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet, ViewSet from rest_framework.viewsets import ModelViewSet, ViewSet
from django.http import HttpResponseBadRequest
from secrets import filters from secrets import filters
from secrets.exceptions import InvalidKey from secrets.exceptions import InvalidKey
from secrets.models import Secret, SecretRole, SessionKey, UserKey from secrets.models import Secret, SecretRole, SessionKey, UserKey

View File

@ -1,3 +1,5 @@
from __future__ import unicode_literals
from django.contrib import messages from django.contrib import messages
from django.shortcuts import redirect from django.shortcuts import redirect
@ -14,10 +16,10 @@ def userkey_required():
try: try:
uk = UserKey.objects.get(user=request.user) uk = UserKey.objects.get(user=request.user)
except UserKey.DoesNotExist: 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') return redirect('user:userkey')
if not uk.is_active(): 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 redirect('user:userkey')
return view(request, *args, **kwargs) return view(request, *args, **kwargs)
return wrapped_view return wrapped_view

View File

@ -1,3 +1,6 @@
from __future__ import unicode_literals
class InvalidKey(Exception): class InvalidKey(Exception):
""" """
Raised when a provided key is invalid. Raised when a provided key is invalid.

View File

@ -1,3 +1,5 @@
from __future__ import unicode_literals
import django_filters import django_filters
from django.db.models import Q from django.db.models import Q

View File

@ -1,3 +1,5 @@
from __future__ import unicode_literals
from Crypto.Cipher import PKCS1_OAEP from Crypto.Cipher import PKCS1_OAEP
from Crypto.PublicKey import RSA from Crypto.PublicKey import RSA
@ -6,7 +8,6 @@ from django.db.models import Count
from dcim.models import Device from dcim.models import Device
from utilities.forms import BootstrapMixin, BulkEditForm, BulkImportForm, CSVDataField, FilterChoiceField, SlugField from utilities.forms import BootstrapMixin, BulkEditForm, BulkImportForm, CSVDataField, FilterChoiceField, SlugField
from .models import Secret, SecretRole, UserKey from .models import Secret, SecretRole, UserKey

View File

@ -1,3 +1,5 @@
from __future__ import unicode_literals
from django.contrib.auth.hashers import PBKDF2PasswordHasher from django.contrib.auth.hashers import PBKDF2PasswordHasher

View File

@ -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'),
),
]

View File

@ -1,4 +1,6 @@
from __future__ import unicode_literals
import os import os
from Crypto.Cipher import AES, PKCS1_OAEP, XOR from Crypto.Cipher import AES, PKCS1_OAEP, XOR
from Crypto.PublicKey import RSA 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 dcim.models import Device
from utilities.models import CreatedUpdatedModel from utilities.models import CreatedUpdatedModel
from .exceptions import InvalidKey from .exceptions import InvalidKey
from .hashers import SecretValidationHasher from .hashers import SecretValidationHasher
@ -301,8 +302,8 @@ class Secret(CreatedUpdatedModel):
def __str__(self): def __str__(self):
if self.role and self.device: if self.role and self.device:
return u'{} for {}'.format(self.role, self.device) return '{} for {}'.format(self.role, self.device)
return u'Secret' return 'Secret'
def get_absolute_url(self): def get_absolute_url(self):
return reverse('secrets:secret', args=[self.pk]) return reverse('secrets:secret', args=[self.pk])

View File

@ -1,5 +1,6 @@
from __future__ import unicode_literals
import django_tables2 as tables import django_tables2 as tables
from django_tables2.utils import Accessor
from utilities.tables import BaseTable, SearchTable, ToggleColumn from utilities.tables import BaseTable, SearchTable, ToggleColumn
@ -22,8 +23,9 @@ class SecretRoleTable(BaseTable):
name = tables.LinkColumn(verbose_name='Name') name = tables.LinkColumn(verbose_name='Name')
secret_count = tables.Column(verbose_name='Secrets') secret_count = tables.Column(verbose_name='Secrets')
slug = tables.Column(verbose_name='Slug') slug = tables.Column(verbose_name='Slug')
actions = tables.TemplateColumn(template_code=SECRETROLE_ACTIONS, attrs={'td': {'class': 'text-right'}}, actions = tables.TemplateColumn(
verbose_name='') template_code=SECRETROLE_ACTIONS, attrs={'td': {'class': 'text-right'}}, verbose_name=''
)
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = SecretRole model = SecretRole

View File

@ -1,3 +1,5 @@
from __future__ import unicode_literals
from django import template from django import template

View File

@ -1,4 +1,6 @@
from __future__ import unicode_literals
import base64 import base64
from rest_framework import status from rest_framework import status
from rest_framework.test import APITestCase from rest_framework.test import APITestCase

View File

@ -1,3 +1,5 @@
from __future__ import unicode_literals
from Crypto.PublicKey import RSA from Crypto.PublicKey import RSA
from django.conf import settings from django.conf import settings

View File

@ -1,3 +1,5 @@
from __future__ import unicode_literals
from django.conf.urls import url from django.conf.urls import url
from . import views from . import views
@ -17,7 +19,7 @@ urlpatterns = [
url(r'^secrets/import/$', views.secret_import, name='secret_import'), 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/edit/$', views.SecretBulkEditView.as_view(), name='secret_bulk_edit'),
url(r'^secrets/delete/$', views.SecretBulkDeleteView.as_view(), name='secret_bulk_delete'), url(r'^secrets/delete/$', views.SecretBulkDeleteView.as_view(), name='secret_bulk_delete'),
url(r'^secrets/(?P<pk>\d+)/$', views.secret, name='secret'), url(r'^secrets/(?P<pk>\d+)/$', views.SecretView.as_view(), name='secret'),
url(r'^secrets/(?P<pk>\d+)/edit/$', views.secret_edit, name='secret_edit'), url(r'^secrets/(?P<pk>\d+)/edit/$', views.secret_edit, name='secret_edit'),
url(r'^secrets/(?P<pk>\d+)/delete/$', views.SecretDeleteView.as_view(), name='secret_delete'), url(r'^secrets/(?P<pk>\d+)/delete/$', views.SecretDeleteView.as_view(), name='secret_delete'),

View File

@ -1,3 +1,4 @@
from __future__ import unicode_literals
import base64 import base64
from django.contrib import messages 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.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse from django.urls import reverse
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.views.generic import View
from dcim.models import Device from dcim.models import Device
from utilities.views import BulkDeleteView, BulkEditView, ObjectDeleteView, ObjectEditView, ObjectListView from utilities.views import BulkDeleteView, BulkEditView, ObjectDeleteView, ObjectEditView, ObjectListView
from . import filters, forms, tables from . import filters, forms, tables
from .decorators import userkey_required from .decorators import userkey_required
from .models import SecretRole, Secret, SessionKey from .models import SecretRole, Secret, SessionKey
@ -65,8 +66,10 @@ class SecretListView(ObjectListView):
template_name = 'secrets/secret_list.html' template_name = 'secrets/secret_list.html'
@login_required @method_decorator(login_required, name='dispatch')
def secret(request, pk): class SecretView(View):
def get(self, request, pk):
secret = get_object_or_404(Secret, pk=pk) secret = get_object_or_404(Secret, pk=pk)
@ -107,7 +110,7 @@ def secret_add(request, pk):
secret.plaintext = str(form.cleaned_data['plaintext']) secret.plaintext = str(form.cleaned_data['plaintext'])
secret.encrypt(master_key) secret.encrypt(master_key)
secret.save() secret.save()
messages.success(request, u"Added new secret: {}.".format(secret)) messages.success(request, "Added new secret: {}.".format(secret))
if '_addanother' in request.POST: if '_addanother' in request.POST:
return redirect('dcim:device_addsecret', pk=device.pk) return redirect('dcim:device_addsecret', pk=device.pk)
else: else:
@ -151,7 +154,7 @@ def secret_edit(request, pk):
secret.plaintext = str(form.cleaned_data['plaintext']) secret.plaintext = str(form.cleaned_data['plaintext'])
secret.encrypt(master_key) secret.encrypt(master_key)
secret.save() secret.save()
messages.success(request, u"Modified secret {}.".format(secret)) messages.success(request, "Modified secret {}.".format(secret))
return redirect('secrets:secret', pk=secret.pk) return redirect('secrets:secret', pk=secret.pk)
else: else:
form.add_error(None, "Invalid session key. Unable to encrypt secret data.") 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. # If no new plaintext was specified, a session key is not needed.
else: else:
secret = form.save() 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) return redirect('secrets:secret', pk=secret.pk)
else: else:
@ -217,7 +220,7 @@ def secret_import(request):
new_secrets.append(secret) new_secrets.append(secret)
table = tables.SecretTable(new_secrets) 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', { return render(request, 'import_success.html', {
'table': table, 'table': table,

View File

@ -1,13 +1,15 @@
{% load static from staticfiles %}
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<title>Server Error</title> <title>Server Error</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css"> <link rel="stylesheet" href="{% static 'bootstrap-3.3.7-dist/css/bootstrap.min.css' %}">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css"> <link rel="stylesheet" href="{% static 'font-awesome-4.7.0/css/font-awesome.min.css' %}">
</head> </head>
<body> <body>
<div class="container-fluid">
<div class="row"> <div class="row">
<div class="col-md-4 col-md-offset-4"> <div class="col-md-4 col-md-offset-4">
<div class="panel panel-danger" style="margin-top: 200px"> <div class="panel panel-danger" style="margin-top: 200px">
@ -32,6 +34,7 @@
</div> </div>
</div> </div>
</div> </div>
</div>
</body> </body>
</html> </html>

View File

@ -45,23 +45,8 @@
</div> </div>
</div> </div>
{% render_field form.site %} {% render_field form.site %}
<div class="row">
<div class="col-md-9 col-md-offset-3">
<ul class="nav nav-tabs" role="tablist">
<li role="presentation" class="active"><a href="#select" aria-controls="home" role="tab" data-toggle="tab">Select</a></li>
<li role="presentation"><a href="#search" aria-controls="search" role="tab" data-toggle="tab">Search</a></li>
</ul>
</div>
</div>
<div class="tab-content">
<div class="tab-pane active" id="select">
{% render_field form.rack %} {% render_field form.rack %}
{% render_field form.device %} {% render_field form.device %}
</div>
<div class="tab-pane" id="search">
{% render_field form.livesearch %}
</div>
</div>
{% render_field form.interface %} {% render_field form.interface %}
</div> </div>
</div> </div>

View File

@ -32,12 +32,7 @@
{% render_field form.livesearch %} {% render_field form.livesearch %}
</div> </div>
<div class="tab-pane" id="select"> <div class="tab-pane" id="select">
<div class="form-group"> {% render_field form.site %}
<label class="col-md-3 control-label">Site</label>
<div class="col-md-9">
<p class="form-control-static">{{ consoleport.device.site }}</p>
</div>
</div>
{% render_field form.rack %} {% render_field form.rack %}
{% render_field form.console_server %} {% render_field form.console_server %}
</div> </div>

View File

@ -32,12 +32,7 @@
{% render_field form.livesearch %} {% render_field form.livesearch %}
</div> </div>
<div class="tab-pane" id="select"> <div class="tab-pane" id="select">
<div class="form-group"> {% render_field form.site %}
<label class="col-md-3 control-label">Site</label>
<div class="col-md-9">
<p class="form-control-static">{{ consoleserverport.device.site }}</p>
</div>
</div>
{% render_field form.rack %} {% render_field form.rack %}
{% render_field form.device %} {% render_field form.device %}
</div> </div>

View File

@ -69,6 +69,11 @@
<td>Unique alphanumeric tag (optional)</td> <td>Unique alphanumeric tag (optional)</td>
<td>ABC123456</td> <td>ABC123456</td>
</tr> </tr>
<tr>
<td>Status</td>
<td>Current status</td>
<td>Active</td>
</tr>
<tr> <tr>
<td>Parent device</td> <td>Parent device</td>
<td>Parent device</td> <td>Parent device</td>
@ -82,7 +87,7 @@
</tbody> </tbody>
</table> </table>
<h4>Example</h4> <h4>Example</h4>
<pre>Blade12,Blade Server,Pied Piper,Dell,BS2000T,Linux,CAB00577291,ABC123456,Server101,Slot4</pre> <pre>Blade12,Blade Server,Pied Piper,Dell,BS2000T,Linux,CAB00577291,ABC123456,Active,Server101,Slot4</pre>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -32,12 +32,7 @@
{% render_field form.livesearch %} {% render_field form.livesearch %}
</div> </div>
<div class="tab-pane" id="select"> <div class="tab-pane" id="select">
<div class="form-group"> {% render_field form.site %}
<label class="col-md-3 control-label">Site</label>
<div class="col-md-9">
<p class="form-control-static">{{ poweroutlet.device.site }}</p>
</div>
</div>
{% render_field form.rack %} {% render_field form.rack %}
{% render_field form.device %} {% render_field form.device %}
</div> </div>

View File

@ -32,12 +32,7 @@
{% render_field form.livesearch %} {% render_field form.livesearch %}
</div> </div>
<div class="tab-pane" id="select"> <div class="tab-pane" id="select">
<div class="form-group"> {% render_field form.site %}
<label class="col-md-3 control-label">Site</label>
<div class="col-md-9">
<p class="form-control-static">{{ powerport.device.site }}</p>
</div>
</div>
{% render_field form.rack %} {% render_field form.rack %}
{% render_field form.pdu %} {% render_field form.pdu %}
</div> </div>

View File

@ -6,7 +6,7 @@
<tr> <tr>
{% for column in table.columns %} {% for column in table.columns %}
{% if column.orderable %} {% if column.orderable %}
<th {{ column.attrs.th.as_html }}><a href="{% querystring page=column.order_by_alias.next %}">{{ column.header }}</a></th> <th {{ column.attrs.th.as_html }}><a href="{% querystring table.prefixed_order_by_field=column.order_by_alias.next %}">{{ column.header }}</a></th>
{% else %} {% else %}
<th {{ column.attrs.th.as_html }}>{{ column.header }}</th> <th {{ column.attrs.th.as_html }}>{{ column.header }}</th>
{% endif %} {% endif %}

View File

@ -47,6 +47,7 @@
<div class="tab-content"> <div class="tab-content">
<div class="tab-pane active" id="select"> <div class="tab-pane active" id="select">
{% render_field form.nat_site %} {% render_field form.nat_site %}
{% render_field form.nat_rack %}
{% render_field form.nat_device %} {% render_field form.nat_device %}
</div> </div>
<div class="tab-pane" id="search"> <div class="tab-pane" id="search">

View File

@ -1,3 +1,5 @@
from __future__ import unicode_literals
from rest_framework import serializers from rest_framework import serializers
from extras.api.customfields import CustomFieldModelSerializer from extras.api.customfields import CustomFieldModelSerializer

View File

@ -1,3 +1,5 @@
from __future__ import unicode_literals
from rest_framework import routers from rest_framework import routers
from . import views from . import views

View File

@ -1,9 +1,10 @@
from __future__ import unicode_literals
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from extras.api.views import CustomFieldModelViewSet
from tenancy.models import Tenant, TenantGroup from tenancy.models import Tenant, TenantGroup
from tenancy.filters import TenantFilter from tenancy.filters import TenantFilter
from extras.api.views import CustomFieldModelViewSet
from utilities.api import WritableSerializerMixin from utilities.api import WritableSerializerMixin
from . import serializers from . import serializers

View File

@ -1,3 +1,5 @@
from __future__ import unicode_literals
from django.apps import AppConfig from django.apps import AppConfig

View File

@ -1,3 +1,5 @@
from __future__ import unicode_literals
import django_filters import django_filters
from django.db.models import Q from django.db.models import Q

View File

@ -1,3 +1,5 @@
from __future__ import unicode_literals
from django import forms from django import forms
from django.db.models import Count from django.db.models import Count
@ -79,7 +81,9 @@ class TenancyForm(ChainedFieldsMixin, forms.Form):
) )
tenant = ChainedModelChoiceField( tenant = ChainedModelChoiceField(
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
chains={'group': 'tenant_group'}, chains=(
('group', 'tenant_group'),
),
required=False, required=False,
widget=APISelect( widget=APISelect(
api_url='/api/tenancy/tenants/?group_id={{tenant_group}}' api_url='/api/tenancy/tenants/?group_id={{tenant_group}}'

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