Compare commits

..

27 Commits

Author SHA1 Message Date
Jeremy Stretch
5c63a499d5 Merge pull request #1259 from digitalocean/develop
Release v2.0.6
2017-06-12 09:51:15 -04:00
Jeremy Stretch
50496b1a59 Merge pull request #1251 from digitalocean/develop
Release v2.0.5
2017-06-08 10:10:41 -04:00
Jeremy Stretch
f7b0d22f86 Merge pull request #1230 from digitalocean/develop
Release v2.0.4
2017-05-25 14:45:13 -04:00
Jeremy Stretch
ad95b86fdd Merge pull request #1201 from digitalocean/develop
Release v2.0.3
2017-05-18 14:37:19 -04:00
Jeremy Stretch
43e1e0dbc8 Merge pull request #1181 from digitalocean/develop
Release v2.0.2
2017-05-15 13:23:33 -04:00
Jeremy Stretch
f731900e2f Merge pull request #1154 from digitalocean/develop
Release v2.0.1
2017-05-09 22:47:52 -04:00
Jeremy Stretch
b1bcaa33e7 Merge pull request #1148 from digitalocean/develop
Release v2.0.0
2017-05-09 15:09:28 -04:00
Jeremy Stretch
17873706b7 Merge pull request #1094 from digitalocean/develop
Release v1.9.6
2017-04-21 14:52:53 -04:00
Jeremy Stretch
e0ad2b4555 Merge pull request #1054 from digitalocean/develop
Release v1.9.5
2017-04-06 16:35:15 -04:00
Jeremy Stretch
f89d91783b Merge pull request #1035 from digitalocean/develop
Release v1.9.4-r1
2017-04-04 15:50:28 -04:00
Jeremy Stretch
3ffe36e5ed Merge pull request #1032 from digitalocean/develop
Release v1.9.4
2017-04-04 12:01:58 -04:00
Jeremy Stretch
be393a9d10 Merge pull request #989 from digitalocean/develop
Release v1.9.3
2017-03-23 16:27:06 -04:00
Jeremy Stretch
27eefd8705 Merge pull request #966 from digitalocean/develop
Release v1.9.2
2017-03-14 17:14:19 -04:00
Jeremy Stretch
097e0f38ff Merge pull request #949 from digitalocean/develop
Release v1.9.1
2017-03-08 14:40:16 -05:00
Jeremy Stretch
ce26b566a4 Merge pull request #939 from digitalocean/develop
Release v1.9.0-r1
2017-03-03 11:28:02 -05:00
Jeremy Stretch
0e14bc1e02 Merge pull request #933 from digitalocean/develop
Release v1.9.0
2017-03-02 13:27:10 -05:00
Jeremy Stretch
ce6796ed9b Merge pull request #870 from digitalocean/develop
Release v1.8.4
2017-02-03 13:59:02 -05:00
Jeremy Stretch
c90cecc2fb Merge pull request #849 from digitalocean/develop
Release v1.8.3
2017-01-26 13:58:52 -05:00
Jeremy Stretch
b6bbcb0609 Merge pull request #814 from digitalocean/develop
Release v1.8.2
2017-01-18 16:23:28 -05:00
Jeremy Stretch
23f6832d9c Merge pull request #774 from digitalocean/develop
Release v1.8.1
2017-01-04 15:30:54 -05:00
Jeremy Stretch
88dace75a1 Merge pull request #766 from digitalocean/develop
Release v1.8.0
2017-01-03 15:13:36 -05:00
Jeremy Stretch
8eb140fd65 Merge pull request #736 from digitalocean/develop
Release v1.7.3
2016-12-08 12:34:53 -05:00
Jeremy Stretch
1f09f3d096 Merge pull request #728 from digitalocean/develop
Release v1.7.2-r1
2016-12-06 15:38:52 -05:00
Jeremy Stretch
66be85a41f Merge pull request #726 from digitalocean/develop
Release v1.7.2
2016-12-06 14:55:19 -05:00
Jeremy Stretch
814c11167e Merge pull request #694 from digitalocean/develop
Release v1.7.1
2016-11-15 12:34:09 -05:00
Jeremy Stretch
57ddd5086f Merge pull request #666 from digitalocean/develop
Release v1.7.0
2016-11-03 15:12:33 -04:00
Jeremy Stretch
c171547037 Merge pull request #625 from digitalocean/develop
Release v1.6.3
2016-10-19 16:25:50 -04:00
70 changed files with 849 additions and 1656 deletions

View File

@@ -119,7 +119,7 @@ Each line of the **device patterns** field represents a hierarchical layer withi
```
core-switch-[abcd]
dist-switch\d
access-switch\d+;oob-switch\d+
access-switch\d+,oob-switch\d+
```
Note that you can combine multiple regexes onto one line using semicolons. The order in which regexes are listed on a line is significant: devices matching the first regex will be rendered first, and subsequent groups will be rendered to the right of those.

View File

@@ -1,11 +1,12 @@
# Installation
**Ubuntu**
**Debian/Ubuntu**
Python 3:
```no-highlight
# apt-get install -y python3 python3-dev python3-pip libxml2-dev libxslt1-dev libffi-dev graphviz libpq-dev libssl-dev zlib1g-dev
# update-alternatives --install /usr/bin/python python /usr/bin/python3 1
```
Python 2:
@@ -14,7 +15,7 @@ Python 2:
# apt-get install -y python2.7 python-dev python-pip libxml2-dev libxslt1-dev libffi-dev graphviz libpq-dev libssl-dev zlib1g-dev
```
**CentOS**
**CentOS/RHEL**
Python 3:
@@ -56,13 +57,13 @@ Create the base directory for the NetBox installation. For this guide, we'll use
If `git` is not already installed, install it:
**Ubuntu**
**Debian/Ubuntu**
```no-highlight
# apt-get install -y git
```
**CentOS**
**CentOS/RHEL**
```no-highlight
# yum install -y git
@@ -149,14 +150,11 @@ You may use the script located at `netbox/generate_secret_key.py` to generate a
# Run Database Migrations
!!! warning
The examples on the rest of this page call the `python` executable, which will be Python2 on most systems. Replace this with `python3` if you're running NetBox on Python3.
Before NetBox can run, we need to install the database schema. This is done by running `python manage.py migrate` from the `netbox` directory (`/opt/netbox/netbox/` in our example):
Before NetBox can run, we need to install the database schema. This is done by running `./manage.py migrate` from the `netbox` directory (`/opt/netbox/netbox/` in our example):
```no-highlight
# cd /opt/netbox/netbox/
# python manage.py migrate
# ./manage.py migrate
Operations to perform:
Apply all migrations: dcim, sessions, admin, ipam, utilities, auth, circuits, contenttypes, extras, secrets, users
Running migrations:
@@ -174,7 +172,7 @@ If this step results in a PostgreSQL authentication error, ensure that the usern
NetBox does not come with any predefined user accounts. You'll need to create a super user to be able to log into NetBox:
```no-highlight
# python manage.py createsuperuser
# ./manage.py createsuperuser
Username: admin
Email address: admin@example.com
Password:
@@ -185,7 +183,7 @@ Superuser created successfully.
# Collect Static Files
```no-highlight
# python manage.py collectstatic --no-input
# ./manage.py collectstatic --no-input
You have requested to collect static files at the destination
location as specified in your settings:
@@ -206,7 +204,7 @@ NetBox ships with some initial data to help you get started: RIR definitions, co
This step is optional. It's perfectly fine to start using NetBox without using this initial data if you'd rather create everything from scratch.
```no-highlight
# python manage.py loaddata initial_data
# ./manage.py loaddata initial_data
Installed 43 object(s) from 4 fixture(s)
```
@@ -215,7 +213,7 @@ Installed 43 object(s) from 4 fixture(s)
At this point, NetBox should be able to run. We can verify this by starting a development instance:
```no-highlight
# python manage.py runserver 0.0.0.0:8000 --insecure
# ./manage.py runserver 0.0.0.0:8000 --insecure
Performing system checks...
System check identified no issues (0 silenced).

View File

@@ -1,18 +1,15 @@
NetBox requires a PostgreSQL database to store data. (Please note that MySQL is not supported, as NetBox leverages PostgreSQL's built-in [network address types](https://www.postgresql.org/docs/9.1/static/datatype-net-types.html).)
!!! note
The installation instructions provided here have been tested to work on Ubuntu 16.04 and CentOS 6.9. The particular commands needed to install dependencies on other distributions may vary significantly. Unfortunately, this is outside the control of the NetBox maintainers. Please consult your distribution's documentation for assistance with any errors.
# Installation
**Ubuntu**
**Debian/Ubuntu**
```no-highlight
# apt-get update
# apt-get install -y postgresql libpq-dev
```
**CentOS**
**CentOS/RHEL**
```no-highlight
# yum install -y postgresql postgresql-server postgresql-devel

View File

@@ -3,7 +3,7 @@
We'll set up a simple WSGI front end using [gunicorn](http://gunicorn.org/) for the purposes of this guide. For web servers, we provide example configurations for both [nginx](https://www.nginx.com/resources/wiki/) and [Apache](http://httpd.apache.org/docs/2.4). (You are of course free to use whichever combination of HTTP and WSGI services you'd like.) We'll also use [supervisord](http://supervisord.org/) to enable service persistence.
!!! info
For the sake of brevity, only Ubuntu 16.04 instructions are provided here, but this sort of web server and WSGI configuration is not unique to NetBox. Please consult your distribution's documentation for assistance if needed.
Only Debian/Ubuntu instructions are provided here, but the installation process for CentOS/RHEL does not differ much. Please consult the documentation for those distributions for details.
```no-highlight
# apt-get install -y gunicorn supervisor

View File

@@ -6,7 +6,6 @@ from circuits.models import Provider, Circuit, CircuitTermination, CircuitType
from dcim.api.serializers import NestedSiteSerializer, InterfaceSerializer
from extras.api.customfields import CustomFieldModelSerializer
from tenancy.api.serializers import NestedTenantSerializer
from utilities.api import ModelValidationMixin
#
@@ -45,7 +44,7 @@ class WritableProviderSerializer(CustomFieldModelSerializer):
# Circuit types
#
class CircuitTypeSerializer(ModelValidationMixin, serializers.ModelSerializer):
class CircuitTypeSerializer(serializers.ModelSerializer):
class Meta:
model = CircuitType
@@ -111,7 +110,7 @@ class CircuitTerminationSerializer(serializers.ModelSerializer):
]
class WritableCircuitTerminationSerializer(ModelValidationMixin, serializers.ModelSerializer):
class WritableCircuitTerminationSerializer(serializers.ModelSerializer):
class Meta:
model = CircuitTermination

View File

@@ -43,7 +43,6 @@ class ProviderViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
class CircuitTypeViewSet(ModelViewSet):
queryset = CircuitType.objects.all()
serializer_class = serializers.CircuitTypeSerializer
filter_class = filters.CircuitTypeFilter
#

View File

@@ -1,10 +0,0 @@
from __future__ import unicode_literals
# CircuitTermination sides
TERM_SIDE_A = 'A'
TERM_SIDE_Z = 'Z'
TERM_SIDE_CHOICES = (
(TERM_SIDE_A, 'A'),
(TERM_SIDE_Z, 'Z'),
)

View File

@@ -31,7 +31,7 @@ class ProviderFilter(CustomFieldFilterSet, django_filters.FilterSet):
class Meta:
model = Provider
fields = ['name', 'slug', 'asn', 'account']
fields = ['name', 'account', 'asn']
def search(self, queryset, name, value):
if not value.strip():
@@ -39,19 +39,10 @@ class ProviderFilter(CustomFieldFilterSet, django_filters.FilterSet):
return queryset.filter(
Q(name__icontains=value) |
Q(account__icontains=value) |
Q(noc_contact__icontains=value) |
Q(admin_contact__icontains=value) |
Q(comments__icontains=value)
)
class CircuitTypeFilter(django_filters.FilterSet):
class Meta:
model = CircuitType
fields = ['name', 'slug']
class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
id__in = NumericInFilter(name='id', lookup_expr='in')
q = django_filters.CharFilter(
@@ -59,6 +50,7 @@ class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Search',
)
provider_id = django_filters.ModelMultipleChoiceFilter(
name='provider',
queryset=Provider.objects.all(),
label='Provider (ID)',
)
@@ -69,6 +61,7 @@ class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Provider (slug)',
)
type_id = django_filters.ModelMultipleChoiceFilter(
name='type',
queryset=CircuitType.objects.all(),
label='Circuit type (ID)',
)
@@ -79,6 +72,7 @@ class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Circuit type (slug)',
)
tenant_id = NullableModelMultipleChoiceFilter(
name='tenant',
queryset=Tenant.objects.all(),
label='Tenant (ID)',
)
@@ -102,7 +96,7 @@ class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
class Meta:
model = Circuit
fields = ['cid', 'install_date', 'commit_rate']
fields = ['install_date']
def search(self, queryset, name, value):
if not value.strip():
@@ -117,34 +111,12 @@ class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
class CircuitTerminationFilter(django_filters.FilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
)
circuit_id = django_filters.ModelMultipleChoiceFilter(
name='circuit',
queryset=Circuit.objects.all(),
label='Circuit',
)
site_id = django_filters.ModelMultipleChoiceFilter(
queryset=Site.objects.all(),
label='Site (ID)',
)
site = django_filters.ModelMultipleChoiceFilter(
name='site__slug',
queryset=Site.objects.all(),
to_field_name='slug',
label='Site (slug)',
)
class Meta:
model = CircuitTermination
fields = ['term_side', 'port_speed', 'upstream_speed', 'xconnect_id']
def search(self, queryset, name, value):
if not value.strip():
return queryset
return queryset.filter(
Q(circuit__cid__icontains=value) |
Q(xconnect_id__icontains=value) |
Q(pp_info__icontains=value)
).distinct()
fields = ['term_side', 'site']

View File

@@ -3,7 +3,7 @@ from __future__ import unicode_literals
from django import forms
from django.db.models import Count
from dcim.models import Site, Device, Interface, Rack
from dcim.models import Site, Device, Interface, Rack, VIRTUAL_IFACE_TYPES
from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
from tenancy.forms import TenancyForm
from tenancy.models import Tenant
@@ -210,7 +210,7 @@ class CircuitTerminationForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm
)
)
interface = ChainedModelChoiceField(
queryset=Interface.objects.connectable().select_related(
queryset=Interface.objects.exclude(form_factor__in=VIRTUAL_IFACE_TYPES).select_related(
'circuit_termination', 'connected_as_a', 'connected_as_b'
),
chains=(
@@ -252,11 +252,6 @@ class CircuitTerminationForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm
super(CircuitTerminationForm, self).__init__(*args, **kwargs)
# Mark connected interfaces as disabled
self.fields['interface'].choices = []
for iface in self.fields['interface'].queryset:
self.fields['interface'].choices.append(
(iface.id, {
'label': iface.name,
'disabled': iface.is_connected and iface.pk != self.initial.get('interface'),
})
)
self.fields['interface'].choices = [
(iface.id, {'label': iface.name, 'disabled': iface.is_connected}) for iface in self.fields['interface'].queryset
]

View File

@@ -10,7 +10,14 @@ from extras.models import CustomFieldModel, CustomFieldValue
from tenancy.models import Tenant
from utilities.utils import csv_format
from utilities.models import CreatedUpdatedModel
from .constants import *
TERM_SIDE_A = 'A'
TERM_SIDE_Z = 'Z'
TERM_SIDE_CHOICES = (
(TERM_SIDE_A, 'A'),
(TERM_SIDE_Z, 'Z'),
)
def humanize_speed(speed):

View File

@@ -10,7 +10,7 @@ urlpatterns = [
# Providers
url(r'^providers/$', views.ProviderListView.as_view(), name='provider_list'),
url(r'^providers/add/$', views.ProviderCreateView.as_view(), name='provider_add'),
url(r'^providers/add/$', views.ProviderEditView.as_view(), name='provider_add'),
url(r'^providers/import/$', views.ProviderBulkImportView.as_view(), name='provider_import'),
url(r'^providers/edit/$', views.ProviderBulkEditView.as_view(), name='provider_bulk_edit'),
url(r'^providers/delete/$', views.ProviderBulkDeleteView.as_view(), name='provider_bulk_delete'),
@@ -20,13 +20,13 @@ urlpatterns = [
# Circuit types
url(r'^circuit-types/$', views.CircuitTypeListView.as_view(), name='circuittype_list'),
url(r'^circuit-types/add/$', views.CircuitTypeCreateView.as_view(), name='circuittype_add'),
url(r'^circuit-types/add/$', views.CircuitTypeEditView.as_view(), name='circuittype_add'),
url(r'^circuit-types/delete/$', views.CircuitTypeBulkDeleteView.as_view(), name='circuittype_bulk_delete'),
url(r'^circuit-types/(?P<slug>[\w-]+)/edit/$', views.CircuitTypeEditView.as_view(), name='circuittype_edit'),
# Circuits
url(r'^circuits/$', views.CircuitListView.as_view(), name='circuit_list'),
url(r'^circuits/add/$', views.CircuitCreateView.as_view(), name='circuit_add'),
url(r'^circuits/add/$', views.CircuitEditView.as_view(), name='circuit_add'),
url(r'^circuits/import/$', views.CircuitBulkImportView.as_view(), name='circuit_import'),
url(r'^circuits/edit/$', views.CircuitBulkEditView.as_view(), name='circuit_bulk_edit'),
url(r'^circuits/delete/$', views.CircuitBulkDeleteView.as_view(), name='circuit_bulk_delete'),
@@ -36,7 +36,7 @@ urlpatterns = [
url(r'^circuits/(?P<pk>\d+)/terminations/swap/$', views.circuit_terminations_swap, name='circuit_terminations_swap'),
# Circuit terminations
url(r'^circuits/(?P<circuit>\d+)/terminations/add/$', views.CircuitTerminationCreateView.as_view(), name='circuittermination_add'),
url(r'^circuits/(?P<circuit>\d+)/terminations/add/$', views.CircuitTerminationEditView.as_view(), name='circuittermination_add'),
url(r'^circuit-terminations/(?P<pk>\d+)/edit/$', views.CircuitTerminationEditView.as_view(), name='circuittermination_edit'),
url(r'^circuit-terminations/(?P<pk>\d+)/delete/$', views.CircuitTerminationDeleteView.as_view(), name='circuittermination_delete'),

View File

@@ -49,18 +49,14 @@ class ProviderView(View):
})
class ProviderCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'circuits.add_provider'
class ProviderEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'circuits.change_provider'
model = Provider
form_class = forms.ProviderForm
template_name = 'circuits/provider_edit.html'
default_return_url = 'circuits:provider_list'
class ProviderEditView(ProviderCreateView):
permission_required = 'circuits.change_provider'
class ProviderDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'circuits.delete_provider'
model = Provider
@@ -100,8 +96,8 @@ class CircuitTypeListView(ObjectListView):
template_name = 'circuits/circuittype_list.html'
class CircuitTypeCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'circuits.add_circuittype'
class CircuitTypeEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'circuits.change_circuittype'
model = CircuitType
form_class = forms.CircuitTypeForm
@@ -109,10 +105,6 @@ class CircuitTypeCreateView(PermissionRequiredMixin, ObjectEditView):
return reverse('circuits:circuittype_list')
class CircuitTypeEditView(CircuitTypeCreateView):
permission_required = 'circuits.change_circuittype'
class CircuitTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'circuits.delete_circuittype'
cls = CircuitType
@@ -154,18 +146,14 @@ class CircuitView(View):
})
class CircuitCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'circuits.add_circuit'
class CircuitEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'circuits.change_circuit'
model = Circuit
form_class = forms.CircuitForm
template_name = 'circuits/circuit_edit.html'
default_return_url = 'circuits:circuit_list'
class CircuitEditView(CircuitCreateView):
permission_required = 'circuits.change_circuit'
class CircuitDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'circuits.delete_circuit'
model = Circuit
@@ -244,8 +232,8 @@ def circuit_terminations_swap(request, pk):
# Circuit terminations
#
class CircuitTerminationCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'circuits.add_circuittermination'
class CircuitTerminationEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'circuits.change_circuittermination'
model = CircuitTermination
form_class = forms.CircuitTerminationForm
template_name = 'circuits/circuittermination_edit.html'
@@ -259,10 +247,6 @@ class CircuitTerminationCreateView(PermissionRequiredMixin, ObjectEditView):
return obj.circuit.get_absolute_url()
class CircuitTerminationEditView(CircuitTerminationCreateView):
permission_required = 'circuits.change_circuittermination'
class CircuitTerminationDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'circuits.delete_circuittermination'
model = CircuitTermination

View File

@@ -1,21 +1,19 @@
from __future__ import unicode_literals
from collections import OrderedDict
from rest_framework import serializers
from rest_framework.validators import UniqueTogetherValidator
from ipam.models import IPAddress
from circuits.models import Circuit, CircuitTermination
from dcim.models import (
CONNECTION_STATUS_CHOICES, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device,
DeviceBay, DeviceBayTemplate, DeviceType, DeviceRole, IFACE_FF_CHOICES, IFACE_ORDERING_CHOICES, Interface,
InterfaceConnection, InterfaceTemplate, Manufacturer, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate,
PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RACK_FACE_CHOICES, RACK_TYPE_CHOICES,
InterfaceConnection, InterfaceTemplate, Manufacturer, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort,
PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RACK_FACE_CHOICES, RACK_TYPE_CHOICES,
RACK_WIDTH_CHOICES, Region, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHOICES,
)
from extras.api.customfields import CustomFieldModelSerializer
from tenancy.api.serializers import NestedTenantSerializer
from utilities.api import ChoiceFieldSerializer, ModelValidationMixin
from utilities.api import ChoiceFieldSerializer
#
@@ -38,7 +36,7 @@ class RegionSerializer(serializers.ModelSerializer):
fields = ['id', 'name', 'slug', 'parent']
class WritableRegionSerializer(ModelValidationMixin, serializers.ModelSerializer):
class WritableRegionSerializer(serializers.ModelSerializer):
class Meta:
model = Region
@@ -100,7 +98,7 @@ class NestedRackGroupSerializer(serializers.ModelSerializer):
fields = ['id', 'url', 'name', 'slug']
class WritableRackGroupSerializer(ModelValidationMixin, serializers.ModelSerializer):
class WritableRackGroupSerializer(serializers.ModelSerializer):
class Meta:
model = RackGroup
@@ -111,7 +109,7 @@ class WritableRackGroupSerializer(ModelValidationMixin, serializers.ModelSeriali
# Rack roles
#
class RackRoleSerializer(ModelValidationMixin, serializers.ModelSerializer):
class RackRoleSerializer(serializers.ModelSerializer):
class Meta:
model = RackRole
@@ -176,9 +174,6 @@ class WritableRackSerializer(CustomFieldModelSerializer):
validator.set_context(self)
validator(data)
# Enforce model validation
super(WritableRackSerializer, self).validate(data)
return data
@@ -216,7 +211,7 @@ class RackReservationSerializer(serializers.ModelSerializer):
fields = ['id', 'rack', 'units', 'created', 'user', 'description']
class WritableRackReservationSerializer(ModelValidationMixin, serializers.ModelSerializer):
class WritableRackReservationSerializer(serializers.ModelSerializer):
class Meta:
model = RackReservation
@@ -227,7 +222,7 @@ class WritableRackReservationSerializer(ModelValidationMixin, serializers.ModelS
# Manufacturers
#
class ManufacturerSerializer(ModelValidationMixin, serializers.ModelSerializer):
class ManufacturerSerializer(serializers.ModelSerializer):
class Meta:
model = Manufacturer
@@ -292,7 +287,7 @@ class ConsolePortTemplateSerializer(serializers.ModelSerializer):
fields = ['id', 'device_type', 'name']
class WritableConsolePortTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer):
class WritableConsolePortTemplateSerializer(serializers.ModelSerializer):
class Meta:
model = ConsolePortTemplate
@@ -311,7 +306,7 @@ class ConsoleServerPortTemplateSerializer(serializers.ModelSerializer):
fields = ['id', 'device_type', 'name']
class WritableConsoleServerPortTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer):
class WritableConsoleServerPortTemplateSerializer(serializers.ModelSerializer):
class Meta:
model = ConsoleServerPortTemplate
@@ -330,7 +325,7 @@ class PowerPortTemplateSerializer(serializers.ModelSerializer):
fields = ['id', 'device_type', 'name']
class WritablePowerPortTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer):
class WritablePowerPortTemplateSerializer(serializers.ModelSerializer):
class Meta:
model = PowerPortTemplate
@@ -349,7 +344,7 @@ class PowerOutletTemplateSerializer(serializers.ModelSerializer):
fields = ['id', 'device_type', 'name']
class WritablePowerOutletTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer):
class WritablePowerOutletTemplateSerializer(serializers.ModelSerializer):
class Meta:
model = PowerOutletTemplate
@@ -369,7 +364,7 @@ class InterfaceTemplateSerializer(serializers.ModelSerializer):
fields = ['id', 'device_type', 'name', 'form_factor', 'mgmt_only']
class WritableInterfaceTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer):
class WritableInterfaceTemplateSerializer(serializers.ModelSerializer):
class Meta:
model = InterfaceTemplate
@@ -388,7 +383,7 @@ class DeviceBayTemplateSerializer(serializers.ModelSerializer):
fields = ['id', 'device_type', 'name']
class WritableDeviceBayTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer):
class WritableDeviceBayTemplateSerializer(serializers.ModelSerializer):
class Meta:
model = DeviceBayTemplate
@@ -399,7 +394,7 @@ class WritableDeviceBayTemplateSerializer(ModelValidationMixin, serializers.Mode
# Device roles
#
class DeviceRoleSerializer(ModelValidationMixin, serializers.ModelSerializer):
class DeviceRoleSerializer(serializers.ModelSerializer):
class Meta:
model = DeviceRole
@@ -418,7 +413,7 @@ class NestedDeviceRoleSerializer(serializers.ModelSerializer):
# Platforms
#
class PlatformSerializer(ModelValidationMixin, serializers.ModelSerializer):
class PlatformSerializer(serializers.ModelSerializer):
class Meta:
model = Platform
@@ -501,9 +496,6 @@ class WritableDeviceSerializer(CustomFieldModelSerializer):
validator.set_context(self)
validator(data)
# Enforce model validation
super(WritableDeviceSerializer, self).validate(data)
return data
@@ -520,7 +512,7 @@ class ConsoleServerPortSerializer(serializers.ModelSerializer):
read_only_fields = ['connected_console']
class WritableConsoleServerPortSerializer(ModelValidationMixin, serializers.ModelSerializer):
class WritableConsoleServerPortSerializer(serializers.ModelSerializer):
class Meta:
model = ConsoleServerPort
@@ -540,7 +532,7 @@ class ConsolePortSerializer(serializers.ModelSerializer):
fields = ['id', 'device', 'name', 'cs_port', 'connection_status']
class WritableConsolePortSerializer(ModelValidationMixin, serializers.ModelSerializer):
class WritableConsolePortSerializer(serializers.ModelSerializer):
class Meta:
model = ConsolePort
@@ -560,7 +552,7 @@ class PowerOutletSerializer(serializers.ModelSerializer):
read_only_fields = ['connected_port']
class WritablePowerOutletSerializer(ModelValidationMixin, serializers.ModelSerializer):
class WritablePowerOutletSerializer(serializers.ModelSerializer):
class Meta:
model = PowerOutlet
@@ -580,7 +572,7 @@ class PowerPortSerializer(serializers.ModelSerializer):
fields = ['id', 'device', 'name', 'power_outlet', 'connection_status']
class WritablePowerPortSerializer(ModelValidationMixin, serializers.ModelSerializer):
class WritablePowerPortSerializer(serializers.ModelSerializer):
class Meta:
model = PowerPort
@@ -599,58 +591,28 @@ class NestedInterfaceSerializer(serializers.ModelSerializer):
fields = ['id', 'url', 'name']
class InterfaceNestedCircuitSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuit-detail')
class Meta:
model = Circuit
fields = ['id', 'url', 'cid']
class InterfaceCircuitTerminationSerializer(serializers.ModelSerializer):
circuit = InterfaceNestedCircuitSerializer()
class Meta:
model = CircuitTermination
fields = [
'id', 'circuit', 'term_side', 'port_speed', 'upstream_speed', 'xconnect_id', 'pp_info',
]
class InterfaceSerializer(serializers.ModelSerializer):
device = NestedDeviceSerializer()
form_factor = ChoiceFieldSerializer(choices=IFACE_FF_CHOICES)
lag = NestedInterfaceSerializer()
is_connected = serializers.SerializerMethodField(read_only=True)
interface_connection = serializers.SerializerMethodField(read_only=True)
circuit_termination = InterfaceCircuitTerminationSerializer()
connection = serializers.SerializerMethodField(read_only=True)
connected_interface = serializers.SerializerMethodField(read_only=True)
class Meta:
model = Interface
fields = [
'id', 'device', 'name', 'form_factor', 'enabled', 'lag', 'mtu', 'mac_address', 'mgmt_only', 'description',
'is_connected', 'interface_connection', 'circuit_termination',
'id', 'device', 'name', 'form_factor', 'lag', 'mac_address', 'mgmt_only', 'description', 'connection',
'connected_interface',
]
def get_is_connected(self, obj):
"""
Return True if the interface has a connected interface or circuit termination.
"""
def get_connection(self, obj):
if obj.connection:
return True
try:
circuit_termination = obj.circuit_termination
return True
except CircuitTermination.DoesNotExist:
pass
return False
return NestedInterfaceConnectionSerializer(obj.connection, context=self.context).data
return None
def get_interface_connection(self, obj):
if obj.connection:
return OrderedDict((
('interface', PeerInterfaceSerializer(obj.connected_interface, context=self.context).data),
('status', obj.connection.connection_status),
))
def get_connected_interface(self, obj):
if obj.connected_interface:
return PeerInterfaceSerializer(obj.connected_interface, context=self.context).data
return None
@@ -662,19 +624,14 @@ class PeerInterfaceSerializer(serializers.ModelSerializer):
class Meta:
model = Interface
fields = [
'id', 'url', 'device', 'name', 'form_factor', 'enabled', 'lag', 'mtu', 'mac_address', 'mgmt_only',
'description',
]
fields = ['id', 'url', 'device', 'name', 'form_factor', 'lag', 'mac_address', 'mgmt_only', 'description']
class WritableInterfaceSerializer(ModelValidationMixin, serializers.ModelSerializer):
class WritableInterfaceSerializer(serializers.ModelSerializer):
class Meta:
model = Interface
fields = [
'id', 'device', 'name', 'form_factor', 'enabled', 'lag', 'mtu', 'mac_address', 'mgmt_only', 'description',
]
fields = ['id', 'device', 'name', 'form_factor', 'lag', 'mac_address', 'mgmt_only', 'description']
#
@@ -690,7 +647,7 @@ class DeviceBaySerializer(serializers.ModelSerializer):
fields = ['id', 'device', 'name', 'installed_device']
class WritableDeviceBaySerializer(ModelValidationMixin, serializers.ModelSerializer):
class WritableDeviceBaySerializer(serializers.ModelSerializer):
class Meta:
model = DeviceBay
@@ -707,20 +664,14 @@ class InventoryItemSerializer(serializers.ModelSerializer):
class Meta:
model = InventoryItem
fields = [
'id', 'device', 'parent', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered',
'description',
]
fields = ['id', 'device', 'parent', 'name', 'manufacturer', 'part_id', 'serial', 'discovered']
class WritableInventoryItemSerializer(ModelValidationMixin, serializers.ModelSerializer):
class WritableInventoryItemSerializer(serializers.ModelSerializer):
class Meta:
model = InventoryItem
fields = [
'id', 'device', 'parent', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered',
'description',
]
fields = ['id', 'device', 'parent', 'name', 'manufacturer', 'part_id', 'serial', 'discovered']
#
@@ -745,7 +696,7 @@ class NestedInterfaceConnectionSerializer(serializers.ModelSerializer):
fields = ['id', 'url', 'connection_status']
class WritableInterfaceConnectionSerializer(ModelValidationMixin, serializers.ModelSerializer):
class WritableInterfaceConnectionSerializer(serializers.ModelSerializer):
class Meta:
model = InterfaceConnection

View File

@@ -32,7 +32,6 @@ class RegionViewSet(WritableSerializerMixin, ModelViewSet):
queryset = Region.objects.all()
serializer_class = serializers.RegionSerializer
write_serializer_class = serializers.WritableRegionSerializer
filter_class = filters.RegionFilter
#
@@ -74,7 +73,6 @@ class RackGroupViewSet(WritableSerializerMixin, ModelViewSet):
class RackRoleViewSet(ModelViewSet):
queryset = RackRole.objects.all()
serializer_class = serializers.RackRoleSerializer
filter_class = filters.RackRoleFilter
#
@@ -130,7 +128,6 @@ class RackReservationViewSet(WritableSerializerMixin, ModelViewSet):
class ManufacturerViewSet(ModelViewSet):
queryset = Manufacturer.objects.all()
serializer_class = serializers.ManufacturerSerializer
filter_class = filters.ManufacturerFilter
#
@@ -197,7 +194,6 @@ class DeviceBayTemplateViewSet(WritableSerializerMixin, ModelViewSet):
class DeviceRoleViewSet(ModelViewSet):
queryset = DeviceRole.objects.all()
serializer_class = serializers.DeviceRoleSerializer
filter_class = filters.DeviceRoleFilter
#
@@ -207,7 +203,6 @@ class DeviceRoleViewSet(ModelViewSet):
class PlatformViewSet(ModelViewSet):
queryset = Platform.objects.all()
serializer_class = serializers.PlatformSerializer
filter_class = filters.PlatformFilter
#

View File

@@ -1,230 +0,0 @@
from __future__ import unicode_literals
# Rack types
RACK_TYPE_2POST = 100
RACK_TYPE_4POST = 200
RACK_TYPE_CABINET = 300
RACK_TYPE_WALLFRAME = 1000
RACK_TYPE_WALLCABINET = 1100
RACK_TYPE_CHOICES = (
(RACK_TYPE_2POST, '2-post frame'),
(RACK_TYPE_4POST, '4-post frame'),
(RACK_TYPE_CABINET, '4-post cabinet'),
(RACK_TYPE_WALLFRAME, 'Wall-mounted frame'),
(RACK_TYPE_WALLCABINET, 'Wall-mounted cabinet'),
)
# Rack widths
RACK_WIDTH_19IN = 19
RACK_WIDTH_23IN = 23
RACK_WIDTH_CHOICES = (
(RACK_WIDTH_19IN, '19 inches'),
(RACK_WIDTH_23IN, '23 inches'),
)
# Rack faces
RACK_FACE_FRONT = 0
RACK_FACE_REAR = 1
RACK_FACE_CHOICES = [
[RACK_FACE_FRONT, 'Front'],
[RACK_FACE_REAR, 'Rear'],
]
# Parent/child device roles
SUBDEVICE_ROLE_PARENT = True
SUBDEVICE_ROLE_CHILD = False
SUBDEVICE_ROLE_CHOICES = (
(None, 'None'),
(SUBDEVICE_ROLE_PARENT, 'Parent'),
(SUBDEVICE_ROLE_CHILD, 'Child'),
)
# Interface ordering schemes (for device types)
IFACE_ORDERING_POSITION = 1
IFACE_ORDERING_NAME = 2
IFACE_ORDERING_CHOICES = [
[IFACE_ORDERING_POSITION, 'Slot/position'],
[IFACE_ORDERING_NAME, 'Name (alphabetically)']
]
# Interface form factors
# Virtual
IFACE_FF_VIRTUAL = 0
IFACE_FF_LAG = 200
# Ethernet
IFACE_FF_100ME_FIXED = 800
IFACE_FF_1GE_FIXED = 1000
IFACE_FF_1GE_GBIC = 1050
IFACE_FF_1GE_SFP = 1100
IFACE_FF_10GE_FIXED = 1150
IFACE_FF_10GE_SFP_PLUS = 1200
IFACE_FF_10GE_XFP = 1300
IFACE_FF_10GE_XENPAK = 1310
IFACE_FF_10GE_X2 = 1320
IFACE_FF_25GE_SFP28 = 1350
IFACE_FF_40GE_QSFP_PLUS = 1400
IFACE_FF_100GE_CFP = 1500
IFACE_FF_100GE_QSFP28 = 1600
# Wireless
IFACE_FF_80211A = 2600
IFACE_FF_80211G = 2610
IFACE_FF_80211N = 2620
IFACE_FF_80211AC = 2630
IFACE_FF_80211AD = 2640
# Fibrechannel
IFACE_FF_1GFC_SFP = 3010
IFACE_FF_2GFC_SFP = 3020
IFACE_FF_4GFC_SFP = 3040
IFACE_FF_8GFC_SFP_PLUS = 3080
IFACE_FF_16GFC_SFP_PLUS = 3160
# Serial
IFACE_FF_T1 = 4000
IFACE_FF_E1 = 4010
IFACE_FF_T3 = 4040
IFACE_FF_E3 = 4050
# Stacking
IFACE_FF_STACKWISE = 5000
IFACE_FF_STACKWISE_PLUS = 5050
IFACE_FF_FLEXSTACK = 5100
IFACE_FF_FLEXSTACK_PLUS = 5150
IFACE_FF_JUNIPER_VCP = 5200
# Other
IFACE_FF_OTHER = 32767
IFACE_FF_CHOICES = [
[
'Virtual interfaces',
[
[IFACE_FF_VIRTUAL, 'Virtual'],
[IFACE_FF_LAG, 'Link Aggregation Group (LAG)'],
]
],
[
'Ethernet (fixed)',
[
[IFACE_FF_100ME_FIXED, '100BASE-TX (10/100ME)'],
[IFACE_FF_1GE_FIXED, '1000BASE-T (1GE)'],
[IFACE_FF_10GE_FIXED, '10GBASE-T (10GE)'],
]
],
[
'Ethernet (modular)',
[
[IFACE_FF_1GE_GBIC, 'GBIC (1GE)'],
[IFACE_FF_1GE_SFP, 'SFP (1GE)'],
[IFACE_FF_10GE_SFP_PLUS, 'SFP+ (10GE)'],
[IFACE_FF_10GE_XFP, 'XFP (10GE)'],
[IFACE_FF_10GE_XENPAK, 'XENPAK (10GE)'],
[IFACE_FF_10GE_X2, 'X2 (10GE)'],
[IFACE_FF_25GE_SFP28, 'SFP28 (25GE)'],
[IFACE_FF_40GE_QSFP_PLUS, 'QSFP+ (40GE)'],
[IFACE_FF_100GE_CFP, 'CFP (100GE)'],
[IFACE_FF_100GE_QSFP28, 'QSFP28 (100GE)'],
]
],
[
'Wireless',
[
[IFACE_FF_80211A, 'IEEE 802.11a'],
[IFACE_FF_80211G, 'IEEE 802.11b/g'],
[IFACE_FF_80211N, 'IEEE 802.11n'],
[IFACE_FF_80211AC, 'IEEE 802.11ac'],
[IFACE_FF_80211AD, 'IEEE 802.11ad'],
]
],
[
'FibreChannel',
[
[IFACE_FF_1GFC_SFP, 'SFP (1GFC)'],
[IFACE_FF_2GFC_SFP, 'SFP (2GFC)'],
[IFACE_FF_4GFC_SFP, 'SFP (4GFC)'],
[IFACE_FF_8GFC_SFP_PLUS, 'SFP+ (8GFC)'],
[IFACE_FF_16GFC_SFP_PLUS, 'SFP+ (16GFC)'],
]
],
[
'Serial',
[
[IFACE_FF_T1, 'T1 (1.544 Mbps)'],
[IFACE_FF_E1, 'E1 (2.048 Mbps)'],
[IFACE_FF_T3, 'T3 (45 Mbps)'],
[IFACE_FF_E3, 'E3 (34 Mbps)'],
]
],
[
'Stacking',
[
[IFACE_FF_STACKWISE, 'Cisco StackWise'],
[IFACE_FF_STACKWISE_PLUS, 'Cisco StackWise Plus'],
[IFACE_FF_FLEXSTACK, 'Cisco FlexStack'],
[IFACE_FF_FLEXSTACK_PLUS, 'Cisco FlexStack Plus'],
[IFACE_FF_JUNIPER_VCP, 'Juniper VCP'],
]
],
[
'Other',
[
[IFACE_FF_OTHER, 'Other'],
]
],
]
VIRTUAL_IFACE_TYPES = [
IFACE_FF_VIRTUAL,
IFACE_FF_LAG,
]
WIRELESS_IFACE_TYPES = [
IFACE_FF_80211A,
IFACE_FF_80211G,
IFACE_FF_80211N,
IFACE_FF_80211AC,
IFACE_FF_80211AD,
]
NONCONNECTABLE_IFACE_TYPES = VIRTUAL_IFACE_TYPES + WIRELESS_IFACE_TYPES
# Device statuses
STATUS_OFFLINE = 0
STATUS_ACTIVE = 1
STATUS_PLANNED = 2
STATUS_STAGED = 3
STATUS_FAILED = 4
STATUS_INVENTORY = 5
STATUS_CHOICES = [
[STATUS_ACTIVE, 'Active'],
[STATUS_OFFLINE, 'Offline'],
[STATUS_PLANNED, 'Planned'],
[STATUS_STAGED, 'Staged'],
[STATUS_FAILED, 'Failed'],
[STATUS_INVENTORY, 'Inventory'],
]
# Bootstrap CSS classes for device stasuses
DEVICE_STATUS_CLASSES = {
0: 'warning',
1: 'success',
2: 'info',
3: 'primary',
4: 'danger',
5: 'default',
}
# Console/power/interface connection statuses
CONNECTION_STATUS_PLANNED = False
CONNECTION_STATUS_CONNECTED = True
CONNECTION_STATUS_CHOICES = [
[CONNECTION_STATUS_PLANNED, 'Planned'],
[CONNECTION_STATUS_CONNECTED, 'Connected'],
]
# Platform -> RPC client mappings
RPC_CLIENT_JUNIPER_JUNOS = 'juniper-junos'
RPC_CLIENT_CISCO_IOS = 'cisco-ios'
RPC_CLIENT_OPENGEAR = 'opengear'
RPC_CLIENT_CHOICES = [
[RPC_CLIENT_JUNIPER_JUNOS, 'Juniper Junos (NETCONF)'],
[RPC_CLIENT_CISCO_IOS, 'Cisco IOS (SSH)'],
[RPC_CLIENT_OPENGEAR, 'Opengear (SSH)'],
]

View File

@@ -3,7 +3,6 @@ from __future__ import unicode_literals
import django_filters
from netaddr.core import AddrFormatError
from django.contrib.auth.models import User
from django.db.models import Q
from extras.filters import CustomFieldFilterSet
@@ -12,28 +11,11 @@ from utilities.filters import NullableModelMultipleChoiceFilter, NumericInFilter
from .models import (
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBayTemplate, DeviceRole, DeviceType, STATUS_CHOICES, IFACE_FF_LAG, Interface, InterfaceConnection,
InterfaceTemplate, Manufacturer, InventoryItem, NONCONNECTABLE_IFACE_TYPES, Platform, PowerOutlet,
PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, Region, Site,
VIRTUAL_IFACE_TYPES, WIRELESS_IFACE_TYPES,
InterfaceTemplate, Manufacturer, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort,
PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, Region, Site, VIRTUAL_IFACE_TYPES,
)
class RegionFilter(django_filters.FilterSet):
parent_id = NullableModelMultipleChoiceFilter(
queryset=Region.objects.all(),
label='Parent region (ID)',
)
parent = NullableModelMultipleChoiceFilter(
queryset=Region.objects.all(),
to_field_name='slug',
label='Parent region (slug)',
)
class Meta:
model = Region
fields = ['name', 'slug']
class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
id__in = NumericInFilter(name='id', lookup_expr='in')
q = django_filters.CharFilter(
@@ -41,19 +23,23 @@ class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Search',
)
region_id = NullableModelMultipleChoiceFilter(
name='region',
queryset=Region.objects.all(),
label='Region (ID)',
)
region = NullableModelMultipleChoiceFilter(
name='region',
queryset=Region.objects.all(),
to_field_name='slug',
label='Region (slug)',
)
tenant_id = NullableModelMultipleChoiceFilter(
name='tenant',
queryset=Tenant.objects.all(),
label='Tenant (ID)',
)
tenant = NullableModelMultipleChoiceFilter(
name='tenant',
queryset=Tenant.objects.all(),
to_field_name='slug',
label='Tenant (slug)',
@@ -61,7 +47,7 @@ class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
class Meta:
model = Site
fields = ['q', 'name', 'slug', 'facility', 'asn', 'contact_name', 'contact_phone', 'contact_email']
fields = ['q', 'name', 'facility', 'asn']
def search(self, queryset, name, value):
if not value.strip():
@@ -71,9 +57,6 @@ class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
Q(facility__icontains=value) |
Q(physical_address__icontains=value) |
Q(shipping_address__icontains=value) |
Q(contact_name__icontains=value) |
Q(contact_phone__icontains=value) |
Q(contact_email__icontains=value) |
Q(comments__icontains=value)
)
try:
@@ -85,6 +68,7 @@ class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
class RackGroupFilter(django_filters.FilterSet):
site_id = django_filters.ModelMultipleChoiceFilter(
name='site',
queryset=Site.objects.all(),
label='Site (ID)',
)
@@ -97,14 +81,7 @@ class RackGroupFilter(django_filters.FilterSet):
class Meta:
model = RackGroup
fields = ['site_id', 'name', 'slug']
class RackRoleFilter(django_filters.FilterSet):
class Meta:
model = RackRole
fields = ['name', 'slug', 'color']
fields = ['name']
class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
@@ -114,6 +91,7 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Search',
)
site_id = django_filters.ModelMultipleChoiceFilter(
name='site',
queryset=Site.objects.all(),
label='Site (ID)',
)
@@ -124,6 +102,7 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Site (slug)',
)
group_id = NullableModelMultipleChoiceFilter(
name='group',
queryset=RackGroup.objects.all(),
label='Group (ID)',
)
@@ -134,6 +113,7 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Group',
)
tenant_id = NullableModelMultipleChoiceFilter(
name='tenant',
queryset=Tenant.objects.all(),
label='Tenant (ID)',
)
@@ -144,6 +124,7 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Tenant (slug)',
)
role_id = NullableModelMultipleChoiceFilter(
name='role',
queryset=RackRole.objects.all(),
label='Role (ID)',
)
@@ -156,7 +137,7 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
class Meta:
model = Rack
fields = ['facility_id', 'type', 'width', 'u_height', 'desc_units']
fields = ['u_height']
def search(self, queryset, name, value):
if not value.strip():
@@ -174,10 +155,6 @@ class RackReservationFilter(django_filters.FilterSet):
method='search',
label='Search',
)
rack_id = django_filters.ModelMultipleChoiceFilter(
queryset=Rack.objects.all(),
label='Rack (ID)',
)
site_id = django_filters.ModelMultipleChoiceFilter(
name='rack__site',
queryset=Site.objects.all(),
@@ -200,20 +177,15 @@ class RackReservationFilter(django_filters.FilterSet):
to_field_name='slug',
label='Group',
)
user_id = django_filters.ModelMultipleChoiceFilter(
queryset=User.objects.all(),
label='User (ID)',
)
user = django_filters.ModelMultipleChoiceFilter(
name='user',
queryset=User.objects.all(),
to_field_name='username',
label='User (name)',
rack_id = django_filters.ModelMultipleChoiceFilter(
name='rack',
queryset=Rack.objects.all(),
label='Rack (ID)',
)
class Meta:
model = RackReservation
fields = ['created']
fields = ['rack', 'user']
def search(self, queryset, name, value):
if not value.strip():
@@ -226,13 +198,6 @@ class RackReservationFilter(django_filters.FilterSet):
)
class ManufacturerFilter(django_filters.FilterSet):
class Meta:
model = Manufacturer
fields = ['name', 'slug']
class DeviceTypeFilter(CustomFieldFilterSet, django_filters.FilterSet):
id__in = NumericInFilter(name='id', lookup_expr='in')
q = django_filters.CharFilter(
@@ -240,6 +205,7 @@ class DeviceTypeFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Search',
)
manufacturer_id = django_filters.ModelMultipleChoiceFilter(
name='manufacturer',
queryset=Manufacturer.objects.all(),
label='Manufacturer (ID)',
)
@@ -253,8 +219,7 @@ class DeviceTypeFilter(CustomFieldFilterSet, django_filters.FilterSet):
class Meta:
model = DeviceType
fields = [
'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'is_console_server', 'is_pdu',
'is_network_device', 'subdevice_role',
'model', 'part_number', 'u_height', 'is_console_server', 'is_pdu', 'is_network_device', 'subdevice_role',
]
def search(self, queryset, name, value):
@@ -270,9 +235,16 @@ class DeviceTypeFilter(CustomFieldFilterSet, django_filters.FilterSet):
class DeviceTypeComponentFilterSet(django_filters.FilterSet):
devicetype_id = django_filters.ModelMultipleChoiceFilter(
name='device_type',
queryset=DeviceType.objects.all(),
label='Device type (ID)',
)
devicetype = django_filters.ModelMultipleChoiceFilter(
name='device_type',
queryset=DeviceType.objects.all(),
to_field_name='name',
label='Device type (name)',
)
class ConsolePortTemplateFilter(DeviceTypeComponentFilterSet):
@@ -307,7 +279,7 @@ class InterfaceTemplateFilter(DeviceTypeComponentFilterSet):
class Meta:
model = InterfaceTemplate
fields = ['name', 'form_factor', 'mgmt_only']
fields = ['name', 'form_factor']
class DeviceBayTemplateFilter(DeviceTypeComponentFilterSet):
@@ -317,73 +289,18 @@ class DeviceBayTemplateFilter(DeviceTypeComponentFilterSet):
fields = ['name']
class DeviceRoleFilter(django_filters.FilterSet):
class Meta:
model = DeviceRole
fields = ['name', 'slug', 'color']
class PlatformFilter(django_filters.FilterSet):
class Meta:
model = Platform
fields = ['name', 'slug']
class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
id__in = NumericInFilter(name='id', lookup_expr='in')
q = django_filters.CharFilter(
method='search',
label='Search',
)
manufacturer_id = django_filters.ModelMultipleChoiceFilter(
name='device_type__manufacturer',
queryset=Manufacturer.objects.all(),
label='Manufacturer (ID)',
)
manufacturer = django_filters.ModelMultipleChoiceFilter(
name='device_type__manufacturer__slug',
queryset=Manufacturer.objects.all(),
to_field_name='slug',
label='Manufacturer (slug)',
)
device_type_id = django_filters.ModelMultipleChoiceFilter(
queryset=DeviceType.objects.all(),
label='Device type (ID)',
)
role_id = django_filters.ModelMultipleChoiceFilter(
name='device_role_id',
queryset=DeviceRole.objects.all(),
label='Role (ID)',
)
role = django_filters.ModelMultipleChoiceFilter(
name='device_role__slug',
queryset=DeviceRole.objects.all(),
to_field_name='slug',
label='Role (slug)',
)
tenant_id = NullableModelMultipleChoiceFilter(
queryset=Tenant.objects.all(),
label='Tenant (ID)',
)
tenant = NullableModelMultipleChoiceFilter(
name='tenant',
queryset=Tenant.objects.all(),
to_field_name='slug',
label='Tenant (slug)',
)
platform_id = NullableModelMultipleChoiceFilter(
queryset=Platform.objects.all(),
label='Platform (ID)',
)
platform = NullableModelMultipleChoiceFilter(
name='platform',
queryset=Platform.objects.all(),
to_field_name='slug',
label='Platform (slug)',
mac_address = django_filters.CharFilter(
method='_mac_address',
label='MAC address',
)
site_id = django_filters.ModelMultipleChoiceFilter(
name='site',
queryset=Site.objects.all(),
label='Site (ID)',
)
@@ -403,18 +320,60 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
queryset=Rack.objects.all(),
label='Rack (ID)',
)
role_id = django_filters.ModelMultipleChoiceFilter(
name='device_role',
queryset=DeviceRole.objects.all(),
label='Role (ID)',
)
role = django_filters.ModelMultipleChoiceFilter(
name='device_role__slug',
queryset=DeviceRole.objects.all(),
to_field_name='slug',
label='Role (slug)',
)
tenant_id = NullableModelMultipleChoiceFilter(
name='tenant',
queryset=Tenant.objects.all(),
label='Tenant (ID)',
)
tenant = NullableModelMultipleChoiceFilter(
name='tenant',
queryset=Tenant.objects.all(),
to_field_name='slug',
label='Tenant (slug)',
)
device_type_id = django_filters.ModelMultipleChoiceFilter(
name='device_type',
queryset=DeviceType.objects.all(),
label='Device type (ID)',
)
manufacturer_id = django_filters.ModelMultipleChoiceFilter(
name='device_type__manufacturer',
queryset=Manufacturer.objects.all(),
label='Manufacturer (ID)',
)
manufacturer = django_filters.ModelMultipleChoiceFilter(
name='device_type__manufacturer__slug',
queryset=Manufacturer.objects.all(),
to_field_name='slug',
label='Manufacturer (slug)',
)
model = django_filters.ModelMultipleChoiceFilter(
name='device_type__slug',
queryset=DeviceType.objects.all(),
to_field_name='slug',
label='Device model (slug)',
)
status = django_filters.MultipleChoiceFilter(
choices=STATUS_CHOICES
platform_id = NullableModelMultipleChoiceFilter(
name='platform',
queryset=Platform.objects.all(),
label='Platform (ID)',
)
is_full_depth = django_filters.BooleanFilter(
name='device_type__is_full_depth',
label='Is full depth',
platform = NullableModelMultipleChoiceFilter(
name='platform',
queryset=Platform.objects.all(),
to_field_name='slug',
label='Platform (slug)',
)
is_console_server = django_filters.BooleanFilter(
name='device_type__is_console_server',
@@ -428,14 +387,13 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
name='device_type__is_network_device',
label='Is a network device',
)
mac_address = django_filters.CharFilter(
method='_mac_address',
label='MAC address',
)
has_primary_ip = django_filters.BooleanFilter(
method='_has_primary_ip',
label='Has a primary IP',
)
status = django_filters.MultipleChoiceFilter(
choices=STATUS_CHOICES
)
class Meta:
model = Device
@@ -475,11 +433,13 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
class DeviceComponentFilterSet(django_filters.FilterSet):
device_id = django_filters.ModelChoiceFilter(
device_id = django_filters.ModelMultipleChoiceFilter(
name='device',
queryset=Device.objects.all(),
label='Device (ID)',
)
device = django_filters.ModelChoiceFilter(
device = django_filters.ModelMultipleChoiceFilter(
name='device__name',
queryset=Device.objects.all(),
to_field_name='name',
label='Device (name)',
@@ -514,21 +474,7 @@ class PowerOutletFilter(DeviceComponentFilterSet):
fields = ['name']
class InterfaceFilter(django_filters.FilterSet):
"""
Not using DeviceComponentFilterSet for Interfaces because we need to glean the ordering logic from the parent
Device's DeviceType.
"""
device = django_filters.CharFilter(
method='filter_device',
name='name',
label='Device',
)
device_id = django_filters.NumberFilter(
method='filter_device',
name='pk',
label='Device (ID)',
)
class InterfaceFilter(DeviceComponentFilterSet):
type = django_filters.CharFilter(
method='filter_type',
label='Interface type',
@@ -545,24 +491,17 @@ class InterfaceFilter(django_filters.FilterSet):
class Meta:
model = Interface
fields = ['name', 'form_factor', 'enabled', 'mtu', 'mgmt_only']
def filter_device(self, queryset, name, value):
try:
device = Device.objects.select_related('device_type').get(**{name: value})
ordering = device.device_type.interface_ordering
return queryset.filter(device=device).order_naturally(ordering)
except Device.DoesNotExist:
return queryset.none()
fields = ['name', 'form_factor']
def filter_type(self, queryset, name, value):
value = value.strip().lower()
return {
'physical': queryset.exclude(form_factor__in=NONCONNECTABLE_IFACE_TYPES),
'virtual': queryset.filter(form_factor__in=VIRTUAL_IFACE_TYPES),
'wireless': queryset.filter(form_factor__in=WIRELESS_IFACE_TYPES),
'lag': queryset.filter(form_factor=IFACE_FF_LAG),
}.get(value, queryset.none())
if value == 'physical':
return queryset.exclude(form_factor__in=VIRTUAL_IFACE_TYPES)
elif value == 'virtual':
return queryset.filter(form_factor__in=VIRTUAL_IFACE_TYPES)
elif value == 'lag':
return queryset.filter(form_factor=IFACE_FF_LAG)
return queryset
def _mac_address(self, queryset, name, value):
value = value.strip()
@@ -582,24 +521,10 @@ class DeviceBayFilter(DeviceComponentFilterSet):
class InventoryItemFilter(DeviceComponentFilterSet):
parent_id = NullableModelMultipleChoiceFilter(
queryset=InventoryItem.objects.all(),
label='Parent inventory item (ID)',
)
manufacturer_id = django_filters.ModelMultipleChoiceFilter(
queryset=Manufacturer.objects.all(),
label='Manufacturer (ID)',
)
manufacturer = django_filters.ModelMultipleChoiceFilter(
name='manufacturer__slug',
queryset=Manufacturer.objects.all(),
to_field_name='slug',
label='Manufacturer (slug)',
)
class Meta:
model = InventoryItem
fields = ['name', 'part_id', 'serial', 'asset_tag', 'discovered']
fields = ['name']
class ConsoleConnectionFilter(django_filters.FilterSet):

View File

@@ -13,8 +13,8 @@ from tenancy.forms import TenancyForm
from tenancy.models import Tenant
from utilities.forms import (
APISelect, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect,
ChainedFieldsMixin, ChainedModelChoiceField, CommentField, ConfirmationForm, CSVChoiceField, ExpandableNameField,
FilterChoiceField, FlexibleModelChoiceField, Livesearch, SelectWithDisabled, SmallTextarea, SlugField,
ChainedFieldsMixin, ChainedModelChoiceField, CommentField, CSVChoiceField, ExpandableNameField, FilterChoiceField,
FlexibleModelChoiceField, Livesearch, SelectWithDisabled, SmallTextarea, SlugField,
FilterTreeNodeMultipleChoiceField,
)
from .formfields import MACAddressFormField
@@ -24,7 +24,7 @@ from .models import (
IFACE_FF_CHOICES, IFACE_FF_LAG, IFACE_ORDERING_CHOICES, InterfaceConnection, InterfaceTemplate, Manufacturer,
InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, RACK_FACE_CHOICES,
RACK_TYPE_CHOICES, RACK_WIDTH_CHOICES, Rack, RackGroup, RackReservation, RackRole, RACK_WIDTH_19IN, RACK_WIDTH_23IN,
Region, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHILD, SUBDEVICE_ROLE_PARENT,
Region, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHILD, SUBDEVICE_ROLE_PARENT, VIRTUAL_IFACE_TYPES,
)
@@ -1174,10 +1174,6 @@ class ConsoleServerPortConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.
}
class ConsoleServerPortBulkDisconnectForm(ConfirmationForm):
pk = forms.ModelMultipleChoiceField(queryset=ConsoleServerPort.objects.all(), widget=forms.MultipleHiddenInput)
#
# Power ports
#
@@ -1435,10 +1431,6 @@ class PowerOutletConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.Form):
}
class PowerOutletBulkDisconnectForm(ConfirmationForm):
pk = forms.ModelMultipleChoiceField(queryset=PowerOutlet.objects.all(), widget=forms.MultipleHiddenInput)
#
# Interfaces
#
@@ -1447,7 +1439,7 @@ class InterfaceForm(BootstrapMixin, forms.ModelForm):
class Meta:
model = Interface
fields = ['device', 'name', 'form_factor', 'enabled', 'lag', 'mac_address', 'mtu', 'mgmt_only', 'description']
fields = ['device', 'name', 'form_factor', 'lag', 'mac_address', 'mgmt_only', 'description']
widgets = {
'device': forms.HiddenInput(),
}
@@ -1469,19 +1461,12 @@ class InterfaceForm(BootstrapMixin, forms.ModelForm):
class InterfaceCreateForm(DeviceComponentForm):
name_pattern = ExpandableNameField(label='Name')
form_factor = forms.ChoiceField(choices=IFACE_FF_CHOICES)
enabled = forms.BooleanField(required=False)
lag = forms.ModelChoiceField(queryset=Interface.objects.all(), required=False, label='Parent LAG')
mtu = forms.IntegerField(required=False, min_value=1, max_value=32767, label='MTU')
mac_address = MACAddressFormField(required=False, label='MAC Address')
mgmt_only = forms.BooleanField(required=False, label='OOB Management')
description = forms.CharField(max_length=100, required=False)
def __init__(self, *args, **kwargs):
# Set interfaces enabled by default
kwargs['initial'] = kwargs.get('initial', {})
kwargs['initial'].update({'enabled': True})
super(InterfaceCreateForm, self).__init__(*args, **kwargs)
# Limit LAG choices to interfaces belonging to this device
@@ -1496,15 +1481,13 @@ class InterfaceCreateForm(DeviceComponentForm):
class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
pk = forms.ModelMultipleChoiceField(queryset=Interface.objects.all(), widget=forms.MultipleHiddenInput)
device = forms.ModelChoiceField(queryset=Device.objects.all(), widget=forms.HiddenInput)
form_factor = forms.ChoiceField(choices=add_blank_choice(IFACE_FF_CHOICES), required=False)
enabled = forms.NullBooleanField(required=False, widget=BulkEditNullBooleanSelect)
lag = forms.ModelChoiceField(queryset=Interface.objects.all(), required=False, label='Parent LAG')
mtu = forms.IntegerField(required=False, min_value=1, max_value=32767, label='MTU')
form_factor = forms.ChoiceField(choices=add_blank_choice(IFACE_FF_CHOICES), required=False)
mgmt_only = forms.NullBooleanField(required=False, widget=BulkEditNullBooleanSelect, label='Management only')
description = forms.CharField(max_length=100, required=False)
class Meta:
nullable_fields = ['lag', 'mtu', 'description']
nullable_fields = ['lag', 'description']
def __init__(self, *args, **kwargs):
super(InterfaceBulkEditForm, self).__init__(*args, **kwargs)
@@ -1525,10 +1508,6 @@ class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
self.fields['lag'].choices = []
class InterfaceBulkDisconnectForm(ConfirmationForm):
pk = forms.ModelMultipleChoiceField(queryset=Interface.objects.all(), widget=forms.MultipleHiddenInput)
#
# Interface connections
#
@@ -1583,7 +1562,7 @@ class InterfaceConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelFor
)
)
interface_b = ChainedModelChoiceField(
queryset=Interface.objects.connectable().select_related(
queryset=Interface.objects.exclude(form_factor__in=VIRTUAL_IFACE_TYPES).select_related(
'circuit_termination', 'connected_as_a', 'connected_as_b'
),
chains=(
@@ -1592,7 +1571,7 @@ class InterfaceConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelFor
label='Interface',
widget=APISelect(
api_url='/api/dcim/interfaces/?device_id={{device_b}}&type=physical',
disabled_indicator='is_connected'
disabled_indicator='connection'
)
)
@@ -1605,7 +1584,9 @@ class InterfaceConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelFor
super(InterfaceConnectionForm, self).__init__(*args, **kwargs)
# Initialize interface A choices
device_a_interfaces = Interface.objects.connectable().order_naturally().filter(device=device_a).select_related(
device_a_interfaces = Interface.objects.order_naturally().filter(device=device_a).exclude(
form_factor__in=VIRTUAL_IFACE_TYPES
).select_related(
'circuit_termination', 'connected_as_a', 'connected_as_b'
)
self.fields['interface_a'].choices = [
@@ -1613,10 +1594,9 @@ class InterfaceConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelFor
]
# Mark connected interfaces as disabled
if self.data.get('device_b'):
self.fields['interface_b'].choices = [
(iface.id, {'label': iface.name, 'disabled': iface.is_connected}) for iface in self.fields['interface_b'].queryset
]
self.fields['interface_b'].choices = [
(iface.id, {'label': iface.name, 'disabled': iface.is_connected}) for iface in self.fields['interface_b'].queryset
]
class InterfaceConnectionCSVForm(forms.ModelForm):
@@ -1765,4 +1745,4 @@ class InventoryItemForm(BootstrapMixin, forms.ModelForm):
class Meta:
model = InventoryItem
fields = ['name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description']
fields = ['name', 'manufacturer', 'part_id', 'serial']

View File

@@ -1,25 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.1 on 2017-06-16 21:38
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dcim', '0037_unicode_literals'),
]
operations = [
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)']]], ['Wireless', [[2600, 'IEEE 802.11a'], [2610, 'IEEE 802.11b/g'], [2620, 'IEEE 802.11n'], [2630, 'IEEE 802.11ac'], [2640, 'IEEE 802.11ad']]], ['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)']]], ['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='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)']]], ['Wireless', [[2600, 'IEEE 802.11a'], [2610, 'IEEE 802.11b/g'], [2620, 'IEEE 802.11n'], [2630, 'IEEE 802.11ac'], [2640, 'IEEE 802.11ad']]], ['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)']]], ['Stacking', [[5000, 'Cisco StackWise'], [5050, 'Cisco StackWise Plus'], [5100, 'Cisco FlexStack'], [5150, 'Cisco FlexStack Plus'], [5200, 'Juniper VCP']]], ['Other', [[32767, 'Other']]]], default=1200),
),
]

View File

@@ -1,25 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.1 on 2017-06-23 17:05
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dcim', '0038_wireless_interfaces'),
]
operations = [
migrations.AddField(
model_name='interface',
name='enabled',
field=models.BooleanField(default=True),
),
migrations.AddField(
model_name='interface',
name='mtu',
field=models.PositiveSmallIntegerField(blank=True, null=True, verbose_name='MTU'),
),
]

View File

@@ -1,26 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2017-06-23 20:44
from __future__ import unicode_literals
from django.db import migrations, models
import utilities.fields
class Migration(migrations.Migration):
dependencies = [
('dcim', '0039_interface_add_enabled_mtu'),
]
operations = [
migrations.AddField(
model_name='inventoryitem',
name='asset_tag',
field=utilities.fields.NullableCharField(blank=True, help_text='A unique tag used to identify this item', max_length=50, null=True, unique=True, verbose_name='Asset tag'),
),
migrations.AddField(
model_name='inventoryitem',
name='description',
field=models.CharField(blank=True, max_length=100),
),
]

View File

@@ -24,10 +24,204 @@ from utilities.fields import ColorField, NullableCharField
from utilities.managers import NaturalOrderByManager
from utilities.models import CreatedUpdatedModel
from utilities.utils import csv_format
from .constants import *
from .fields import ASNField, MACAddressField
RACK_TYPE_2POST = 100
RACK_TYPE_4POST = 200
RACK_TYPE_CABINET = 300
RACK_TYPE_WALLFRAME = 1000
RACK_TYPE_WALLCABINET = 1100
RACK_TYPE_CHOICES = (
(RACK_TYPE_2POST, '2-post frame'),
(RACK_TYPE_4POST, '4-post frame'),
(RACK_TYPE_CABINET, '4-post cabinet'),
(RACK_TYPE_WALLFRAME, 'Wall-mounted frame'),
(RACK_TYPE_WALLCABINET, 'Wall-mounted cabinet'),
)
RACK_WIDTH_19IN = 19
RACK_WIDTH_23IN = 23
RACK_WIDTH_CHOICES = (
(RACK_WIDTH_19IN, '19 inches'),
(RACK_WIDTH_23IN, '23 inches'),
)
RACK_FACE_FRONT = 0
RACK_FACE_REAR = 1
RACK_FACE_CHOICES = [
[RACK_FACE_FRONT, 'Front'],
[RACK_FACE_REAR, 'Rear'],
]
SUBDEVICE_ROLE_PARENT = True
SUBDEVICE_ROLE_CHILD = False
SUBDEVICE_ROLE_CHOICES = (
(None, 'None'),
(SUBDEVICE_ROLE_PARENT, 'Parent'),
(SUBDEVICE_ROLE_CHILD, 'Child'),
)
IFACE_ORDERING_POSITION = 1
IFACE_ORDERING_NAME = 2
IFACE_ORDERING_CHOICES = [
[IFACE_ORDERING_POSITION, 'Slot/position'],
[IFACE_ORDERING_NAME, 'Name (alphabetically)']
]
# Virtual
IFACE_FF_VIRTUAL = 0
IFACE_FF_LAG = 200
# Ethernet
IFACE_FF_100ME_FIXED = 800
IFACE_FF_1GE_FIXED = 1000
IFACE_FF_1GE_GBIC = 1050
IFACE_FF_1GE_SFP = 1100
IFACE_FF_10GE_FIXED = 1150
IFACE_FF_10GE_SFP_PLUS = 1200
IFACE_FF_10GE_XFP = 1300
IFACE_FF_10GE_XENPAK = 1310
IFACE_FF_10GE_X2 = 1320
IFACE_FF_25GE_SFP28 = 1350
IFACE_FF_40GE_QSFP_PLUS = 1400
IFACE_FF_100GE_CFP = 1500
IFACE_FF_100GE_QSFP28 = 1600
# Fibrechannel
IFACE_FF_1GFC_SFP = 3010
IFACE_FF_2GFC_SFP = 3020
IFACE_FF_4GFC_SFP = 3040
IFACE_FF_8GFC_SFP_PLUS = 3080
IFACE_FF_16GFC_SFP_PLUS = 3160
# Serial
IFACE_FF_T1 = 4000
IFACE_FF_E1 = 4010
IFACE_FF_T3 = 4040
IFACE_FF_E3 = 4050
# Stacking
IFACE_FF_STACKWISE = 5000
IFACE_FF_STACKWISE_PLUS = 5050
IFACE_FF_FLEXSTACK = 5100
IFACE_FF_FLEXSTACK_PLUS = 5150
IFACE_FF_JUNIPER_VCP = 5200
# Other
IFACE_FF_OTHER = 32767
IFACE_FF_CHOICES = [
[
'Virtual interfaces',
[
[IFACE_FF_VIRTUAL, 'Virtual'],
[IFACE_FF_LAG, 'Link Aggregation Group (LAG)'],
]
],
[
'Ethernet (fixed)',
[
[IFACE_FF_100ME_FIXED, '100BASE-TX (10/100ME)'],
[IFACE_FF_1GE_FIXED, '1000BASE-T (1GE)'],
[IFACE_FF_10GE_FIXED, '10GBASE-T (10GE)'],
]
],
[
'Ethernet (modular)',
[
[IFACE_FF_1GE_GBIC, 'GBIC (1GE)'],
[IFACE_FF_1GE_SFP, 'SFP (1GE)'],
[IFACE_FF_10GE_SFP_PLUS, 'SFP+ (10GE)'],
[IFACE_FF_10GE_XFP, 'XFP (10GE)'],
[IFACE_FF_10GE_XENPAK, 'XENPAK (10GE)'],
[IFACE_FF_10GE_X2, 'X2 (10GE)'],
[IFACE_FF_25GE_SFP28, 'SFP28 (25GE)'],
[IFACE_FF_40GE_QSFP_PLUS, 'QSFP+ (40GE)'],
[IFACE_FF_100GE_CFP, 'CFP (100GE)'],
[IFACE_FF_100GE_QSFP28, 'QSFP28 (100GE)'],
]
],
[
'FibreChannel',
[
[IFACE_FF_1GFC_SFP, 'SFP (1GFC)'],
[IFACE_FF_2GFC_SFP, 'SFP (2GFC)'],
[IFACE_FF_4GFC_SFP, 'SFP (4GFC)'],
[IFACE_FF_8GFC_SFP_PLUS, 'SFP+ (8GFC)'],
[IFACE_FF_16GFC_SFP_PLUS, 'SFP+ (16GFC)'],
]
],
[
'Serial',
[
[IFACE_FF_T1, 'T1 (1.544 Mbps)'],
[IFACE_FF_E1, 'E1 (2.048 Mbps)'],
[IFACE_FF_T3, 'T3 (45 Mbps)'],
[IFACE_FF_E3, 'E3 (34 Mbps)'],
[IFACE_FF_E3, 'E3 (34 Mbps)'],
]
],
[
'Stacking',
[
[IFACE_FF_STACKWISE, 'Cisco StackWise'],
[IFACE_FF_STACKWISE_PLUS, 'Cisco StackWise Plus'],
[IFACE_FF_FLEXSTACK, 'Cisco FlexStack'],
[IFACE_FF_FLEXSTACK_PLUS, 'Cisco FlexStack Plus'],
[IFACE_FF_JUNIPER_VCP, 'Juniper VCP'],
]
],
[
'Other',
[
[IFACE_FF_OTHER, 'Other'],
]
],
]
VIRTUAL_IFACE_TYPES = [
IFACE_FF_VIRTUAL,
IFACE_FF_LAG,
]
STATUS_OFFLINE = 0
STATUS_ACTIVE = 1
STATUS_PLANNED = 2
STATUS_STAGED = 3
STATUS_FAILED = 4
STATUS_INVENTORY = 5
STATUS_CHOICES = [
[STATUS_ACTIVE, 'Active'],
[STATUS_OFFLINE, 'Offline'],
[STATUS_PLANNED, 'Planned'],
[STATUS_STAGED, 'Staged'],
[STATUS_FAILED, 'Failed'],
[STATUS_INVENTORY, 'Inventory'],
]
DEVICE_STATUS_CLASSES = {
0: 'warning',
1: 'success',
2: 'info',
3: 'primary',
4: 'danger',
5: 'default',
}
CONNECTION_STATUS_PLANNED = False
CONNECTION_STATUS_CONNECTED = True
CONNECTION_STATUS_CHOICES = [
[CONNECTION_STATUS_PLANNED, 'Planned'],
[CONNECTION_STATUS_CONNECTED, 'Connected'],
]
# For mapping platform -> NC client
RPC_CLIENT_JUNIPER_JUNOS = 'juniper-junos'
RPC_CLIENT_CISCO_IOS = 'cisco-ios'
RPC_CLIENT_OPENGEAR = 'opengear'
RPC_CLIENT_CHOICES = [
[RPC_CLIENT_JUNIPER_JUNOS, 'Juniper Junos (NETCONF)'],
[RPC_CLIENT_CISCO_IOS, 'Cisco IOS (SSH)'],
[RPC_CLIENT_OPENGEAR, 'Opengear (SSH)'],
]
#
# Regions
#
@@ -622,7 +816,7 @@ class PowerOutletTemplate(models.Model):
return self.name
class InterfaceQuerySet(models.QuerySet):
class InterfaceManager(models.Manager):
def order_naturally(self, method=IFACE_ORDERING_POSITION):
"""
@@ -647,12 +841,13 @@ class InterfaceQuerySet(models.QuerySet):
The original `name` field is taken as a whole to serve as a fallback in the event interfaces do not match any of
the prescribed fields.
"""
sql_col = '{}.name'.format(self.model._meta.db_table)
queryset = self.get_queryset()
sql_col = '{}.name'.format(queryset.model._meta.db_table)
ordering = {
IFACE_ORDERING_POSITION: ('_slot', '_subslot', '_position', '_channel', '_vc', '_type', 'name'),
IFACE_ORDERING_NAME: ('_type', '_slot', '_subslot', '_position', '_channel', '_vc', 'name'),
}[method]
return self.extra(select={
return queryset.extra(select={
'_type': "SUBSTRING({} FROM '^([^0-9]+)')".format(sql_col),
'_slot': "CAST(SUBSTRING({} FROM '([0-9]+)\/[0-9]+\/[0-9]+(:[0-9]+)?(\.[0-9]+)?$') AS integer)".format(sql_col),
'_subslot': "CAST(SUBSTRING({} FROM '([0-9]+)\/[0-9]+(:[0-9]+)?(\.[0-9]+)?$') AS integer)".format(sql_col),
@@ -661,13 +856,6 @@ class InterfaceQuerySet(models.QuerySet):
'_vc': "COALESCE(CAST(SUBSTRING({} FROM '\.([0-9]+)$') AS integer), 0)".format(sql_col),
}).order_by(*ordering)
def connectable(self):
"""
Return only physical interfaces which are capable of being connected to other interfaces (i.e. not virtual or
wireless).
"""
return self.exclude(form_factor__in=NONCONNECTABLE_IFACE_TYPES)
@python_2_unicode_compatible
class InterfaceTemplate(models.Model):
@@ -679,7 +867,7 @@ class InterfaceTemplate(models.Model):
form_factor = models.PositiveSmallIntegerField(choices=IFACE_FF_CHOICES, default=IFACE_FF_10GE_SFP_PLUS)
mgmt_only = models.BooleanField(default=False, verbose_name='Management only')
objects = InterfaceQuerySet.as_manager()
objects = InterfaceManager()
class Meta:
ordering = ['device_type', 'name']
@@ -1120,27 +1308,16 @@ class Interface(models.Model):
of an InterfaceConnection.
"""
device = models.ForeignKey('Device', related_name='interfaces', on_delete=models.CASCADE)
lag = models.ForeignKey(
'self',
models.SET_NULL,
related_name='member_interfaces',
null=True,
blank=True,
verbose_name='Parent LAG'
)
lag = models.ForeignKey('self', related_name='member_interfaces', null=True, blank=True, on_delete=models.SET_NULL,
verbose_name='Parent LAG')
name = models.CharField(max_length=30)
form_factor = models.PositiveSmallIntegerField(choices=IFACE_FF_CHOICES, default=IFACE_FF_10GE_SFP_PLUS)
enabled = models.BooleanField(default=True)
mac_address = MACAddressField(null=True, blank=True, verbose_name='MAC Address')
mtu = models.PositiveSmallIntegerField(blank=True, null=True, verbose_name='MTU')
mgmt_only = models.BooleanField(
default=False,
verbose_name='OOB Management',
help_text="This interface is used only for out-of-band management"
)
mgmt_only = models.BooleanField(default=False, verbose_name='OOB Management',
help_text="This interface is used only for out-of-band management")
description = models.CharField(max_length=100, blank=True)
objects = InterfaceQuerySet.as_manager()
objects = InterfaceManager()
class Meta:
ordering = ['device', 'name']
@@ -1152,10 +1329,10 @@ class Interface(models.Model):
def clean(self):
# Virtual interfaces cannot be connected
if self.form_factor in NONCONNECTABLE_IFACE_TYPES and self.is_connected:
if self.form_factor in VIRTUAL_IFACE_TYPES and self.is_connected:
raise ValidationError({
'form_factor': "Virtual and wireless interfaces cannot be connected to another interface or circuit. "
"Disconnect the interface or choose a suitable form factor."
'form_factor': "Virtual interfaces cannot be connected to another interface or circuit. Disconnect the "
"interface or choose a physical form factor."
})
# An interface's LAG must belong to the same device
@@ -1167,7 +1344,7 @@ class Interface(models.Model):
})
# A virtual interface cannot have a parent LAG
if self.form_factor in NONCONNECTABLE_IFACE_TYPES and self.lag is not None:
if self.form_factor in VIRTUAL_IFACE_TYPES and self.lag is not None:
raise ValidationError({
'lag': "{} interfaces cannot have a parent LAG interface.".format(self.get_form_factor_display())
})
@@ -1184,10 +1361,6 @@ class Interface(models.Model):
def is_virtual(self):
return self.form_factor in VIRTUAL_IFACE_TYPES
@property
def is_wireless(self):
return self.form_factor in WIRELESS_IFACE_TYPES
@property
def is_lag(self):
return self.form_factor == IFACE_FF_LAG
@@ -1306,17 +1479,11 @@ class InventoryItem(models.Model):
device = models.ForeignKey('Device', related_name='inventory_items', on_delete=models.CASCADE)
parent = models.ForeignKey('self', related_name='child_items', blank=True, null=True, on_delete=models.CASCADE)
name = models.CharField(max_length=50, verbose_name='Name')
manufacturer = models.ForeignKey(
'Manufacturer', models.PROTECT, related_name='inventory_items', blank=True, null=True
)
manufacturer = models.ForeignKey('Manufacturer', related_name='inventory_items', blank=True, null=True,
on_delete=models.PROTECT)
part_id = models.CharField(max_length=50, verbose_name='Part ID', blank=True)
serial = models.CharField(max_length=50, verbose_name='Serial number', blank=True)
asset_tag = NullableCharField(
max_length=50, blank=True, null=True, unique=True, verbose_name='Asset tag',
help_text='A unique tag used to identify this item'
)
discovered = models.BooleanField(default=False, verbose_name='Discovered')
description = models.CharField(max_length=100, blank=True)
class Meta:
ordering = ['device__id', 'parent__id', 'name']

View File

@@ -368,11 +368,10 @@ class PowerOutletTemplateTable(BaseTable):
class InterfaceTemplateTable(BaseTable):
pk = ToggleColumn()
mgmt_only = tables.TemplateColumn("{% if value %}OOB Management{% endif %}")
class Meta(BaseTable.Meta):
model = InterfaceTemplate
fields = ('pk', 'name', 'mgmt_only', 'form_factor')
fields = ('pk', 'name', 'form_factor')
empty_text = "None"
show_header = False

View File

@@ -3,7 +3,7 @@ from __future__ import unicode_literals
from django.conf.urls import url
from extras.views import ImageAttachmentEditView
from ipam.views import ServiceCreateView
from ipam.views import ServiceEditView
from secrets.views import secret_add
from .models import Device, Rack, Site
from . import views
@@ -14,13 +14,13 @@ urlpatterns = [
# Regions
url(r'^regions/$', views.RegionListView.as_view(), name='region_list'),
url(r'^regions/add/$', views.RegionCreateView.as_view(), name='region_add'),
url(r'^regions/add/$', views.RegionEditView.as_view(), name='region_add'),
url(r'^regions/delete/$', views.RegionBulkDeleteView.as_view(), name='region_bulk_delete'),
url(r'^regions/(?P<pk>\d+)/edit/$', views.RegionEditView.as_view(), name='region_edit'),
# Sites
url(r'^sites/$', views.SiteListView.as_view(), name='site_list'),
url(r'^sites/add/$', views.SiteCreateView.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/edit/$', views.SiteBulkEditView.as_view(), name='site_bulk_edit'),
url(r'^sites/(?P<slug>[\w-]+)/$', views.SiteView.as_view(), name='site'),
@@ -30,13 +30,13 @@ urlpatterns = [
# Rack groups
url(r'^rack-groups/$', views.RackGroupListView.as_view(), name='rackgroup_list'),
url(r'^rack-groups/add/$', views.RackGroupCreateView.as_view(), name='rackgroup_add'),
url(r'^rack-groups/add/$', views.RackGroupEditView.as_view(), name='rackgroup_add'),
url(r'^rack-groups/delete/$', views.RackGroupBulkDeleteView.as_view(), name='rackgroup_bulk_delete'),
url(r'^rack-groups/(?P<pk>\d+)/edit/$', views.RackGroupEditView.as_view(), name='rackgroup_edit'),
# Rack roles
url(r'^rack-roles/$', views.RackRoleListView.as_view(), name='rackrole_list'),
url(r'^rack-roles/add/$', views.RackRoleCreateView.as_view(), name='rackrole_add'),
url(r'^rack-roles/add/$', views.RackRoleEditView.as_view(), name='rackrole_add'),
url(r'^rack-roles/delete/$', views.RackRoleBulkDeleteView.as_view(), name='rackrole_bulk_delete'),
url(r'^rack-roles/(?P<pk>\d+)/edit/$', views.RackRoleEditView.as_view(), name='rackrole_edit'),
@@ -56,18 +56,18 @@ urlpatterns = [
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+)/delete/$', views.RackDeleteView.as_view(), name='rack_delete'),
url(r'^racks/(?P<rack>\d+)/reservations/add/$', views.RackReservationCreateView.as_view(), name='rack_add_reservation'),
url(r'^racks/(?P<rack>\d+)/reservations/add/$', views.RackReservationEditView.as_view(), name='rack_add_reservation'),
url(r'^racks/(?P<object_id>\d+)/images/add/$', ImageAttachmentEditView.as_view(), name='rack_add_image', kwargs={'model': Rack}),
# Manufacturers
url(r'^manufacturers/$', views.ManufacturerListView.as_view(), name='manufacturer_list'),
url(r'^manufacturers/add/$', views.ManufacturerCreateView.as_view(), name='manufacturer_add'),
url(r'^manufacturers/add/$', views.ManufacturerEditView.as_view(), name='manufacturer_add'),
url(r'^manufacturers/delete/$', views.ManufacturerBulkDeleteView.as_view(), name='manufacturer_bulk_delete'),
url(r'^manufacturers/(?P<slug>[\w-]+)/edit/$', views.ManufacturerEditView.as_view(), name='manufacturer_edit'),
# Device types
url(r'^device-types/$', views.DeviceTypeListView.as_view(), name='devicetype_list'),
url(r'^device-types/add/$', views.DeviceTypeCreateView.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/delete/$', views.DeviceTypeBulkDeleteView.as_view(), name='devicetype_bulk_delete'),
url(r'^device-types/(?P<pk>\d+)/$', views.DeviceTypeView.as_view(), name='devicetype'),
@@ -75,45 +75,45 @@ urlpatterns = [
url(r'^device-types/(?P<pk>\d+)/delete/$', views.DeviceTypeDeleteView.as_view(), name='devicetype_delete'),
# Console port templates
url(r'^device-types/(?P<pk>\d+)/console-ports/add/$', views.ConsolePortTemplateCreateView.as_view(), name='devicetype_add_consoleport'),
url(r'^device-types/(?P<pk>\d+)/console-ports/add/$', views.ConsolePortTemplateAddView.as_view(), name='devicetype_add_consoleport'),
url(r'^device-types/(?P<pk>\d+)/console-ports/delete/$', views.ConsolePortTemplateBulkDeleteView.as_view(), name='devicetype_delete_consoleport'),
# Console server port templates
url(r'^device-types/(?P<pk>\d+)/console-server-ports/add/$', views.ConsoleServerPortTemplateCreateView.as_view(), name='devicetype_add_consoleserverport'),
url(r'^device-types/(?P<pk>\d+)/console-server-ports/add/$', views.ConsoleServerPortTemplateAddView.as_view(), name='devicetype_add_consoleserverport'),
url(r'^device-types/(?P<pk>\d+)/console-server-ports/delete/$', views.ConsoleServerPortTemplateBulkDeleteView.as_view(), name='devicetype_delete_consoleserverport'),
# Power port templates
url(r'^device-types/(?P<pk>\d+)/power-ports/add/$', views.PowerPortTemplateCreateView.as_view(), name='devicetype_add_powerport'),
url(r'^device-types/(?P<pk>\d+)/power-ports/add/$', views.PowerPortTemplateAddView.as_view(), name='devicetype_add_powerport'),
url(r'^device-types/(?P<pk>\d+)/power-ports/delete/$', views.PowerPortTemplateBulkDeleteView.as_view(), name='devicetype_delete_powerport'),
# Power outlet templates
url(r'^device-types/(?P<pk>\d+)/power-outlets/add/$', views.PowerOutletTemplateCreateView.as_view(), name='devicetype_add_poweroutlet'),
url(r'^device-types/(?P<pk>\d+)/power-outlets/add/$', views.PowerOutletTemplateAddView.as_view(), name='devicetype_add_poweroutlet'),
url(r'^device-types/(?P<pk>\d+)/power-outlets/delete/$', views.PowerOutletTemplateBulkDeleteView.as_view(), name='devicetype_delete_poweroutlet'),
# Interface templates
url(r'^device-types/(?P<pk>\d+)/interfaces/add/$', views.InterfaceTemplateCreateView.as_view(), name='devicetype_add_interface'),
url(r'^device-types/(?P<pk>\d+)/interfaces/add/$', views.InterfaceTemplateAddView.as_view(), name='devicetype_add_interface'),
url(r'^device-types/(?P<pk>\d+)/interfaces/edit/$', views.InterfaceTemplateBulkEditView.as_view(), name='devicetype_bulkedit_interface'),
url(r'^device-types/(?P<pk>\d+)/interfaces/delete/$', views.InterfaceTemplateBulkDeleteView.as_view(), name='devicetype_delete_interface'),
# Device bay templates
url(r'^device-types/(?P<pk>\d+)/device-bays/add/$', views.DeviceBayTemplateCreateView.as_view(), name='devicetype_add_devicebay'),
url(r'^device-types/(?P<pk>\d+)/device-bays/add/$', views.DeviceBayTemplateAddView.as_view(), name='devicetype_add_devicebay'),
url(r'^device-types/(?P<pk>\d+)/device-bays/delete/$', views.DeviceBayTemplateBulkDeleteView.as_view(), name='devicetype_delete_devicebay'),
# Device roles
url(r'^device-roles/$', views.DeviceRoleListView.as_view(), name='devicerole_list'),
url(r'^device-roles/add/$', views.DeviceRoleCreateView.as_view(), name='devicerole_add'),
url(r'^device-roles/add/$', views.DeviceRoleEditView.as_view(), name='devicerole_add'),
url(r'^device-roles/delete/$', views.DeviceRoleBulkDeleteView.as_view(), name='devicerole_bulk_delete'),
url(r'^device-roles/(?P<slug>[\w-]+)/edit/$', views.DeviceRoleEditView.as_view(), name='devicerole_edit'),
# Platforms
url(r'^platforms/$', views.PlatformListView.as_view(), name='platform_list'),
url(r'^platforms/add/$', views.PlatformCreateView.as_view(), name='platform_add'),
url(r'^platforms/add/$', views.PlatformEditView.as_view(), name='platform_add'),
url(r'^platforms/delete/$', views.PlatformBulkDeleteView.as_view(), name='platform_bulk_delete'),
url(r'^platforms/(?P<slug>[\w-]+)/edit/$', views.PlatformEditView.as_view(), name='platform_edit'),
# Devices
url(r'^devices/$', views.DeviceListView.as_view(), name='device_list'),
url(r'^devices/add/$', views.DeviceCreateView.as_view(), name='device_add'),
url(r'^devices/add/$', views.DeviceEditView.as_view(), name='device_add'),
url(r'^devices/import/$', views.DeviceBulkImportView.as_view(), name='device_import'),
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'),
@@ -124,12 +124,12 @@ urlpatterns = [
url(r'^devices/(?P<pk>\d+)/inventory/$', views.DeviceInventoryView.as_view(), name='device_inventory'),
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<device>\d+)/services/assign/$', ServiceCreateView.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}),
# Console ports
url(r'^devices/console-ports/add/$', views.DeviceBulkAddConsolePortView.as_view(), name='device_bulk_add_consoleport'),
url(r'^devices/(?P<pk>\d+)/console-ports/add/$', views.ConsolePortCreateView.as_view(), name='consoleport_add'),
url(r'^devices/(?P<pk>\d+)/console-ports/add/$', views.ConsolePortAddView.as_view(), name='consoleport_add'),
url(r'^devices/(?P<pk>\d+)/console-ports/delete/$', views.ConsolePortBulkDeleteView.as_view(), name='consoleport_bulk_delete'),
url(r'^console-ports/(?P<pk>\d+)/connect/$', views.consoleport_connect, name='consoleport_connect'),
url(r'^console-ports/(?P<pk>\d+)/disconnect/$', views.consoleport_disconnect, name='consoleport_disconnect'),
@@ -138,8 +138,7 @@ urlpatterns = [
# Console server ports
url(r'^devices/console-server-ports/add/$', views.DeviceBulkAddConsoleServerPortView.as_view(), name='device_bulk_add_consoleserverport'),
url(r'^devices/(?P<pk>\d+)/console-server-ports/add/$', views.ConsoleServerPortCreateView.as_view(), name='consoleserverport_add'),
url(r'^devices/(?P<pk>\d+)/console-server-ports/disconnect/$', views.ConsoleServerPortBulkDisconnectView.as_view(), name='consoleserverport_bulk_disconnect'),
url(r'^devices/(?P<pk>\d+)/console-server-ports/add/$', views.ConsoleServerPortAddView.as_view(), name='consoleserverport_add'),
url(r'^devices/(?P<pk>\d+)/console-server-ports/delete/$', views.ConsoleServerPortBulkDeleteView.as_view(), name='consoleserverport_bulk_delete'),
url(r'^console-server-ports/(?P<pk>\d+)/connect/$', views.consoleserverport_connect, name='consoleserverport_connect'),
url(r'^console-server-ports/(?P<pk>\d+)/disconnect/$', views.consoleserverport_disconnect, name='consoleserverport_disconnect'),
@@ -148,7 +147,7 @@ urlpatterns = [
# Power ports
url(r'^devices/power-ports/add/$', views.DeviceBulkAddPowerPortView.as_view(), name='device_bulk_add_powerport'),
url(r'^devices/(?P<pk>\d+)/power-ports/add/$', views.PowerPortCreateView.as_view(), name='powerport_add'),
url(r'^devices/(?P<pk>\d+)/power-ports/add/$', views.PowerPortAddView.as_view(), name='powerport_add'),
url(r'^devices/(?P<pk>\d+)/power-ports/delete/$', views.PowerPortBulkDeleteView.as_view(), name='powerport_bulk_delete'),
url(r'^power-ports/(?P<pk>\d+)/connect/$', views.powerport_connect, name='powerport_connect'),
url(r'^power-ports/(?P<pk>\d+)/disconnect/$', views.powerport_disconnect, name='powerport_disconnect'),
@@ -157,8 +156,7 @@ urlpatterns = [
# Power outlets
url(r'^devices/power-outlets/add/$', views.DeviceBulkAddPowerOutletView.as_view(), name='device_bulk_add_poweroutlet'),
url(r'^devices/(?P<pk>\d+)/power-outlets/add/$', views.PowerOutletCreateView.as_view(), name='poweroutlet_add'),
url(r'^devices/(?P<pk>\d+)/power-outlets/disconnect/$', views.PowerOutletBulkDisconnectView.as_view(), name='poweroutlet_bulk_disconnect'),
url(r'^devices/(?P<pk>\d+)/power-outlets/add/$', views.PowerOutletAddView.as_view(), name='poweroutlet_add'),
url(r'^devices/(?P<pk>\d+)/power-outlets/delete/$', views.PowerOutletBulkDeleteView.as_view(), name='poweroutlet_bulk_delete'),
url(r'^power-outlets/(?P<pk>\d+)/connect/$', views.poweroutlet_connect, name='poweroutlet_connect'),
url(r'^power-outlets/(?P<pk>\d+)/disconnect/$', views.poweroutlet_disconnect, name='poweroutlet_disconnect'),
@@ -167,9 +165,8 @@ urlpatterns = [
# Interfaces
url(r'^devices/interfaces/add/$', views.DeviceBulkAddInterfaceView.as_view(), name='device_bulk_add_interface'),
url(r'^devices/(?P<pk>\d+)/interfaces/add/$', views.InterfaceCreateView.as_view(), name='interface_add'),
url(r'^devices/(?P<pk>\d+)/interfaces/add/$', views.InterfaceAddView.as_view(), name='interface_add'),
url(r'^devices/(?P<pk>\d+)/interfaces/edit/$', views.InterfaceBulkEditView.as_view(), name='interface_bulk_edit'),
url(r'^devices/(?P<pk>\d+)/interfaces/disconnect/$', views.InterfaceBulkDisconnectView.as_view(), name='interface_bulk_disconnect'),
url(r'^devices/(?P<pk>\d+)/interfaces/delete/$', views.InterfaceBulkDeleteView.as_view(), name='interface_bulk_delete'),
url(r'^devices/(?P<pk>\d+)/interface-connections/add/$', views.interfaceconnection_add, name='interfaceconnection_add'),
url(r'^interface-connections/(?P<pk>\d+)/delete/$', views.interfaceconnection_delete, name='interfaceconnection_delete'),
@@ -178,7 +175,7 @@ urlpatterns = [
# Device bays
url(r'^devices/device-bays/add/$', views.DeviceBulkAddDeviceBayView.as_view(), name='device_bulk_add_devicebay'),
url(r'^devices/(?P<pk>\d+)/bays/add/$', views.DeviceBayCreateView.as_view(), name='devicebay_add'),
url(r'^devices/(?P<pk>\d+)/bays/add/$', views.DeviceBayAddView.as_view(), name='devicebay_add'),
url(r'^devices/(?P<pk>\d+)/bays/delete/$', views.DeviceBayBulkDeleteView.as_view(), name='devicebay_bulk_delete'),
url(r'^device-bays/(?P<pk>\d+)/edit/$', views.DeviceBayEditView.as_view(), name='devicebay_edit'),
url(r'^device-bays/(?P<pk>\d+)/delete/$', views.DeviceBayDeleteView.as_view(), name='devicebay_delete'),

View File

@@ -1,5 +1,6 @@
from __future__ import unicode_literals
from copy import deepcopy
from difflib import SequenceMatcher
import re
from natsort import natsorted
from operator import attrgetter
@@ -8,7 +9,7 @@ from django.contrib import messages
from django.contrib.auth.decorators import permission_required
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.core.paginator import EmptyPage, PageNotAnInteger
from django.db.models import Count, Q
from django.db.models import Count
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
@@ -141,44 +142,6 @@ class ComponentDeleteView(ObjectDeleteView):
return obj.device.get_absolute_url()
class BulkDisconnectView(View):
"""
An extendable view for disconnection console/power/interface components in bulk.
"""
model = None
form = None
template_name = 'dcim/bulk_disconnect.html'
def disconnect_objects(self, objects):
raise NotImplementedError()
def post(self, request, pk):
device = get_object_or_404(Device, pk=pk)
selected_objects = []
if '_confirm' in request.POST:
form = self.form(request.POST)
if form.is_valid():
count = self.disconnect_objects(form.cleaned_data['pk'])
messages.success(request, "Disconnected {} {} on {}".format(
count, self.model._meta.verbose_name_plural, device
))
return redirect(device.get_absolute_url())
else:
form = self.form(initial={'pk': request.POST.getlist('pk')})
selected_objects = self.model.objects.filter(pk__in=form.initial['pk'])
return render(request, self.template_name, {
'form': form,
'device': device,
'obj_type_plural': self.model._meta.verbose_name_plural,
'selected_objects': selected_objects,
'return_url': device.get_absolute_url(),
})
#
# Regions
#
@@ -189,8 +152,8 @@ class RegionListView(ObjectListView):
template_name = 'dcim/region_list.html'
class RegionCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.add_region'
class RegionEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.change_region'
model = Region
form_class = forms.RegionForm
@@ -198,10 +161,6 @@ class RegionCreateView(PermissionRequiredMixin, ObjectEditView):
return reverse('dcim:region_list')
class RegionEditView(RegionCreateView):
permission_required = 'dcim.change_region'
class RegionBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_region'
cls = Region
@@ -245,18 +204,14 @@ class SiteView(View):
})
class SiteCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.add_site'
class SiteEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.change_site'
model = Site
form_class = forms.SiteForm
template_name = 'dcim/site_edit.html'
default_return_url = 'dcim:site_list'
class SiteEditView(SiteCreateView):
permission_required = 'dcim.change_site'
class SiteDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'dcim.delete_site'
model = Site
@@ -291,8 +246,8 @@ class RackGroupListView(ObjectListView):
template_name = 'dcim/rackgroup_list.html'
class RackGroupCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.add_rackgroup'
class RackGroupEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.change_rackgroup'
model = RackGroup
form_class = forms.RackGroupForm
@@ -300,10 +255,6 @@ class RackGroupCreateView(PermissionRequiredMixin, ObjectEditView):
return reverse('dcim:rackgroup_list')
class RackGroupEditView(RackGroupCreateView):
permission_required = 'dcim.change_rackgroup'
class RackGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_rackgroup'
cls = RackGroup
@@ -321,8 +272,8 @@ class RackRoleListView(ObjectListView):
template_name = 'dcim/rackrole_list.html'
class RackRoleCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.add_rackrole'
class RackRoleEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.change_rackrole'
model = RackRole
form_class = forms.RackRoleForm
@@ -330,10 +281,6 @@ class RackRoleCreateView(PermissionRequiredMixin, ObjectEditView):
return reverse('dcim:rackrole_list')
class RackRoleEditView(RackRoleCreateView):
permission_required = 'dcim.change_rackrole'
class RackRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_rackrole'
cls = RackRole
@@ -427,18 +374,14 @@ class RackView(View):
})
class RackCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.add_rack'
class RackEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.change_rack'
model = Rack
form_class = forms.RackForm
template_name = 'dcim/rack_edit.html'
default_return_url = 'dcim:rack_list'
class RackEditView(RackCreateView):
permission_required = 'dcim.change_rack'
class RackDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'dcim.delete_rack'
model = Rack
@@ -480,8 +423,8 @@ class RackReservationListView(ObjectListView):
template_name = 'dcim/rackreservation_list.html'
class RackReservationCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.add_rackreservation'
class RackReservationEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.change_rackreservation'
model = RackReservation
form_class = forms.RackReservationForm
@@ -495,10 +438,6 @@ class RackReservationCreateView(PermissionRequiredMixin, ObjectEditView):
return obj.rack.get_absolute_url()
class RackReservationEditView(RackReservationCreateView):
permission_required = 'dcim.change_rackreservation'
class RackReservationDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'dcim.delete_rackreservation'
model = RackReservation
@@ -523,8 +462,8 @@ class ManufacturerListView(ObjectListView):
template_name = 'dcim/manufacturer_list.html'
class ManufacturerCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.add_manufacturer'
class ManufacturerEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.change_manufacturer'
model = Manufacturer
form_class = forms.ManufacturerForm
@@ -532,10 +471,6 @@ class ManufacturerCreateView(PermissionRequiredMixin, ObjectEditView):
return reverse('dcim:manufacturer_list')
class ManufacturerEditView(ManufacturerCreateView):
permission_required = 'dcim.change_manufacturer'
class ManufacturerBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_manufacturer'
cls = Manufacturer
@@ -573,10 +508,15 @@ class DeviceTypeView(View):
poweroutlet_table = tables.PowerOutletTemplateTable(
natsorted(PowerOutletTemplate.objects.filter(device_type=devicetype), key=attrgetter('name'))
)
mgmt_interface_table = tables.InterfaceTemplateTable(
list(InterfaceTemplate.objects.order_naturally(devicetype.interface_ordering).filter(
device_type=devicetype, mgmt_only=True
))
)
interface_table = tables.InterfaceTemplateTable(
list(InterfaceTemplate.objects.order_naturally(
devicetype.interface_ordering
).filter(device_type=devicetype))
list(InterfaceTemplate.objects.order_naturally(devicetype.interface_ordering).filter(
device_type=devicetype, mgmt_only=False
))
)
devicebay_table = tables.DeviceBayTemplateTable(
natsorted(DeviceBayTemplate.objects.filter(device_type=devicetype), key=attrgetter('name'))
@@ -586,6 +526,7 @@ class DeviceTypeView(View):
consoleserverport_table.base_columns['pk'].visible = True
powerport_table.base_columns['pk'].visible = True
poweroutlet_table.base_columns['pk'].visible = True
mgmt_interface_table.base_columns['pk'].visible = True
interface_table.base_columns['pk'].visible = True
devicebay_table.base_columns['pk'].visible = True
@@ -595,23 +536,20 @@ class DeviceTypeView(View):
'consoleserverport_table': consoleserverport_table,
'powerport_table': powerport_table,
'poweroutlet_table': poweroutlet_table,
'mgmt_interface_table': mgmt_interface_table,
'interface_table': interface_table,
'devicebay_table': devicebay_table,
})
class DeviceTypeCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.add_devicetype'
class DeviceTypeEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.change_devicetype'
model = DeviceType
form_class = forms.DeviceTypeForm
template_name = 'dcim/devicetype_edit.html'
default_return_url = 'dcim:devicetype_list'
class DeviceTypeEditView(DeviceTypeCreateView):
permission_required = 'dcim.change_devicetype'
class DeviceTypeDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'dcim.delete_devicetype'
model = DeviceType
@@ -638,7 +576,7 @@ class DeviceTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
# Device type components
#
class ConsolePortTemplateCreateView(PermissionRequiredMixin, ComponentCreateView):
class ConsolePortTemplateAddView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_consoleporttemplate'
parent_model = DeviceType
parent_field = 'device_type'
@@ -655,7 +593,7 @@ class ConsolePortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView)
parent_cls = DeviceType
class ConsoleServerPortTemplateCreateView(PermissionRequiredMixin, ComponentCreateView):
class ConsoleServerPortTemplateAddView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_consoleserverporttemplate'
parent_model = DeviceType
parent_field = 'device_type'
@@ -670,7 +608,7 @@ class ConsoleServerPortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDelet
parent_cls = DeviceType
class PowerPortTemplateCreateView(PermissionRequiredMixin, ComponentCreateView):
class PowerPortTemplateAddView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_powerporttemplate'
parent_model = DeviceType
parent_field = 'device_type'
@@ -685,7 +623,7 @@ class PowerPortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
parent_cls = DeviceType
class PowerOutletTemplateCreateView(PermissionRequiredMixin, ComponentCreateView):
class PowerOutletTemplateAddView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_poweroutlettemplate'
parent_model = DeviceType
parent_field = 'device_type'
@@ -700,7 +638,7 @@ class PowerOutletTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView)
parent_cls = DeviceType
class InterfaceTemplateCreateView(PermissionRequiredMixin, ComponentCreateView):
class InterfaceTemplateAddView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_interfacetemplate'
parent_model = DeviceType
parent_field = 'device_type'
@@ -723,7 +661,7 @@ class InterfaceTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
parent_cls = DeviceType
class DeviceBayTemplateCreateView(PermissionRequiredMixin, ComponentCreateView):
class DeviceBayTemplateAddView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_devicebaytemplate'
parent_model = DeviceType
parent_field = 'device_type'
@@ -748,8 +686,8 @@ class DeviceRoleListView(ObjectListView):
template_name = 'dcim/devicerole_list.html'
class DeviceRoleCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.add_devicerole'
class DeviceRoleEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.change_devicerole'
model = DeviceRole
form_class = forms.DeviceRoleForm
@@ -757,10 +695,6 @@ class DeviceRoleCreateView(PermissionRequiredMixin, ObjectEditView):
return reverse('dcim:devicerole_list')
class DeviceRoleEditView(DeviceRoleCreateView):
permission_required = 'dcim.change_devicerole'
class DeviceRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_devicerole'
cls = DeviceRole
@@ -777,8 +711,8 @@ class PlatformListView(ObjectListView):
template_name = 'dcim/platform_list.html'
class PlatformCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.add_platform'
class PlatformEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.change_platform'
model = Platform
form_class = forms.PlatformForm
@@ -786,10 +720,6 @@ class PlatformCreateView(PermissionRequiredMixin, ObjectEditView):
return reverse('dcim:platform_list')
class PlatformEditView(PlatformCreateView):
permission_required = 'dcim.change_platform'
class PlatformBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_platform'
cls = Platform
@@ -828,10 +758,14 @@ class DeviceView(View):
power_outlets = natsorted(
PowerOutlet.objects.filter(device=device).select_related('connected_port'), key=attrgetter('name')
)
interfaces = Interface.objects.order_naturally(
device.device_type.interface_ordering
).filter(
device=device
interfaces = Interface.objects.order_naturally(device.device_type.interface_ordering).filter(
device=device, mgmt_only=False
).select_related(
'connected_as_a__interface_b__device', 'connected_as_b__interface_a__device',
'circuit_termination__circuit'
).prefetch_related('ip_addresses')
mgmt_interfaces = Interface.objects.order_naturally(device.device_type.interface_ordering).filter(
device=device, mgmt_only=True
).select_related(
'connected_as_a__interface_b__device', 'connected_as_b__interface_a__device',
'circuit_termination__circuit'
@@ -862,6 +796,7 @@ class DeviceView(View):
'power_ports': power_ports,
'power_outlets': power_outlets,
'interfaces': interfaces,
'mgmt_interfaces': mgmt_interfaces,
'device_bays': device_bays,
'services': services,
'secrets': secrets,
@@ -908,18 +843,14 @@ class DeviceLLDPNeighborsView(View):
})
class DeviceCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.add_device'
class DeviceEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.change_device'
model = Device
form_class = forms.DeviceForm
template_name = 'dcim/device_edit.html'
default_return_url = 'dcim:device_list'
class DeviceEditView(DeviceCreateView):
permission_required = 'dcim.change_device'
class DeviceDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'dcim.delete_device'
model = Device
@@ -973,7 +904,7 @@ class DeviceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
# Console ports
#
class ConsolePortCreateView(PermissionRequiredMixin, ComponentCreateView):
class ConsolePortAddView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_consoleport'
parent_model = Device
parent_field = 'device'
@@ -1086,7 +1017,7 @@ class ConsoleConnectionsBulkImportView(PermissionRequiredMixin, BulkImportView):
# Console server ports
#
class ConsoleServerPortCreateView(PermissionRequiredMixin, ComponentCreateView):
class ConsoleServerPortAddView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_consoleserverport'
parent_model = Device
parent_field = 'device'
@@ -1185,15 +1116,6 @@ class ConsoleServerPortDeleteView(PermissionRequiredMixin, ComponentDeleteView):
model = ConsoleServerPort
class ConsoleServerPortBulkDisconnectView(PermissionRequiredMixin, BulkDisconnectView):
permission_required = 'dcim.change_consoleserverport'
model = ConsoleServerPort
form = forms.ConsoleServerPortBulkDisconnectForm
def disconnect_objects(self, cs_ports):
return ConsolePort.objects.filter(cs_port__in=cs_ports).update(cs_port=None, connection_status=None)
class ConsoleServerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_consoleserverport'
cls = ConsoleServerPort
@@ -1204,7 +1126,7 @@ class ConsoleServerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
# Power ports
#
class PowerPortCreateView(PermissionRequiredMixin, ComponentCreateView):
class PowerPortAddView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_powerport'
parent_model = Device
parent_field = 'device'
@@ -1317,7 +1239,7 @@ class PowerConnectionsBulkImportView(PermissionRequiredMixin, BulkImportView):
# Power outlets
#
class PowerOutletCreateView(PermissionRequiredMixin, ComponentCreateView):
class PowerOutletAddView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_poweroutlet'
parent_model = Device
parent_field = 'device'
@@ -1416,17 +1338,6 @@ class PowerOutletDeleteView(PermissionRequiredMixin, ComponentDeleteView):
model = PowerOutlet
class PowerOutletBulkDisconnectView(PermissionRequiredMixin, BulkDisconnectView):
permission_required = 'dcim.change_poweroutlet'
model = PowerOutlet
form = forms.PowerOutletBulkDisconnectForm
def disconnect_objects(self, power_outlets):
return PowerPort.objects.filter(power_outlet__in=power_outlets).update(
power_outlet=None, connection_status=None
)
class PowerOutletBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_poweroutlet'
cls = PowerOutlet
@@ -1437,7 +1348,7 @@ class PowerOutletBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
# Interfaces
#
class InterfaceCreateView(PermissionRequiredMixin, ComponentCreateView):
class InterfaceAddView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_interface'
parent_model = Device
parent_field = 'device'
@@ -1457,18 +1368,6 @@ class InterfaceDeleteView(PermissionRequiredMixin, ComponentDeleteView):
model = Interface
class InterfaceBulkDisconnectView(PermissionRequiredMixin, BulkDisconnectView):
permission_required = 'dcim.change_interface'
model = Interface
form = forms.InterfaceBulkDisconnectForm
def disconnect_objects(self, interfaces):
count, _ = InterfaceConnection.objects.filter(
Q(interface_a__in=interfaces) | Q(interface_b__in=interfaces)
).delete()
return count
class InterfaceBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.change_interface'
cls = Interface
@@ -1487,7 +1386,7 @@ class InterfaceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
# Device bays
#
class DeviceBayCreateView(PermissionRequiredMixin, ComponentCreateView):
class DeviceBayAddView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_devicebay'
parent_model = Device
parent_field = 'device'

View File

@@ -49,10 +49,6 @@ class CustomFieldsSerializer(serializers.BaseSerializer):
# Validate selected choice
if cf.type == CF_TYPE_SELECT:
try:
value = int(value)
except ValueError:
raise ValidationError("{}: Choice selections must be passed as integers.".format(field_name))
valid_choices = [c.pk for c in cf.choices.all()]
if value not in valid_choices:
raise ValidationError("Invalid choice for field {}: {}".format(field_name, value))
@@ -111,16 +107,6 @@ class CustomFieldModelSerializer(serializers.ModelSerializer):
defaults={'serialized_value': custom_field.serialize_value(value)},
)
def validate(self, data):
"""
Enforce model validation (see utilities.api.ModelValidationMixin)
"""
model_data = data.copy()
model_data.pop('custom_fields', None)
instance = self.Meta.model(**model_data)
instance.clean()
return data
def create(self, validated_data):
custom_fields = validated_data.pop('custom_fields', None)

View File

@@ -10,7 +10,7 @@ from extras.models import (
ACTION_CHOICES, ExportTemplate, Graph, GRAPH_TYPE_CHOICES, ImageAttachment, TopologyMap, UserAction,
)
from users.api.serializers import NestedUserSerializer
from utilities.api import ChoiceFieldSerializer, ContentTypeFieldSerializer, ModelValidationMixin
from utilities.api import ChoiceFieldSerializer, ContentTypeFieldSerializer
#
@@ -104,7 +104,7 @@ class ImageAttachmentSerializer(serializers.ModelSerializer):
return serializer(obj.parent, context={'request': self.context['request']}).data
class WritableImageAttachmentSerializer(ModelValidationMixin, serializers.ModelSerializer):
class WritableImageAttachmentSerializer(serializers.ModelSerializer):
content_type = ContentTypeFieldSerializer()
class Meta:
@@ -121,9 +121,6 @@ class WritableImageAttachmentSerializer(ModelValidationMixin, serializers.ModelS
"Invalid parent object: {} ID {}".format(data['content_type'], data['object_id'])
)
# Enforce model validation
super(WritableImageAttachmentSerializer, self).validate(data)
return data

View File

@@ -1,62 +0,0 @@
from __future__ import unicode_literals
# Models which support custom fields
CUSTOMFIELD_MODELS = (
'site', 'rack', 'devicetype', 'device', # DCIM
'aggregate', 'prefix', 'ipaddress', 'vlan', 'vrf', # IPAM
'provider', 'circuit', # Circuits
'tenant', # Tenants
)
# Custom field types
CF_TYPE_TEXT = 100
CF_TYPE_INTEGER = 200
CF_TYPE_BOOLEAN = 300
CF_TYPE_DATE = 400
CF_TYPE_URL = 500
CF_TYPE_SELECT = 600
CUSTOMFIELD_TYPE_CHOICES = (
(CF_TYPE_TEXT, 'Text'),
(CF_TYPE_INTEGER, 'Integer'),
(CF_TYPE_BOOLEAN, 'Boolean (true/false)'),
(CF_TYPE_DATE, 'Date'),
(CF_TYPE_URL, 'URL'),
(CF_TYPE_SELECT, 'Selection'),
)
# Graph types
GRAPH_TYPE_INTERFACE = 100
GRAPH_TYPE_PROVIDER = 200
GRAPH_TYPE_SITE = 300
GRAPH_TYPE_CHOICES = (
(GRAPH_TYPE_INTERFACE, 'Interface'),
(GRAPH_TYPE_PROVIDER, 'Provider'),
(GRAPH_TYPE_SITE, 'Site'),
)
# Models which support export templates
EXPORTTEMPLATE_MODELS = [
'site', 'rack', 'device', 'consoleport', 'powerport', 'interfaceconnection', # DCIM
'aggregate', 'prefix', 'ipaddress', 'vlan', # IPAM
'provider', 'circuit', # Circuits
'tenant', # Tenants
]
# User action types
ACTION_CREATE = 1
ACTION_IMPORT = 2
ACTION_EDIT = 3
ACTION_BULK_EDIT = 4
ACTION_DELETE = 5
ACTION_BULK_DELETE = 6
ACTION_BULK_CREATE = 7
ACTION_CHOICES = (
(ACTION_CREATE, 'created'),
(ACTION_BULK_CREATE, 'bulk created'),
(ACTION_IMPORT, 'imported'),
(ACTION_EDIT, 'modified'),
(ACTION_BULK_EDIT, 'bulk edited'),
(ACTION_DELETE, 'deleted'),
(ACTION_BULK_DELETE, 'bulk deleted'),
)

View File

@@ -15,7 +15,62 @@ from django.utils.encoding import python_2_unicode_compatible
from django.utils.safestring import mark_safe
from utilities.utils import foreground_color
from .constants import *
CUSTOMFIELD_MODELS = (
'site', 'rack', 'devicetype', 'device', # DCIM
'aggregate', 'prefix', 'ipaddress', 'vlan', 'vrf', # IPAM
'provider', 'circuit', # Circuits
'tenant', # Tenants
)
CF_TYPE_TEXT = 100
CF_TYPE_INTEGER = 200
CF_TYPE_BOOLEAN = 300
CF_TYPE_DATE = 400
CF_TYPE_URL = 500
CF_TYPE_SELECT = 600
CUSTOMFIELD_TYPE_CHOICES = (
(CF_TYPE_TEXT, 'Text'),
(CF_TYPE_INTEGER, 'Integer'),
(CF_TYPE_BOOLEAN, 'Boolean (true/false)'),
(CF_TYPE_DATE, 'Date'),
(CF_TYPE_URL, 'URL'),
(CF_TYPE_SELECT, 'Selection'),
)
GRAPH_TYPE_INTERFACE = 100
GRAPH_TYPE_PROVIDER = 200
GRAPH_TYPE_SITE = 300
GRAPH_TYPE_CHOICES = (
(GRAPH_TYPE_INTERFACE, 'Interface'),
(GRAPH_TYPE_PROVIDER, 'Provider'),
(GRAPH_TYPE_SITE, 'Site'),
)
EXPORTTEMPLATE_MODELS = [
'site', 'rack', 'device', 'consoleport', 'powerport', 'interfaceconnection', # DCIM
'aggregate', 'prefix', 'ipaddress', 'vlan', # IPAM
'provider', 'circuit', # Circuits
'tenant', # Tenants
]
ACTION_CREATE = 1
ACTION_IMPORT = 2
ACTION_EDIT = 3
ACTION_BULK_EDIT = 4
ACTION_DELETE = 5
ACTION_BULK_DELETE = 6
ACTION_BULK_CREATE = 7
ACTION_CHOICES = (
(ACTION_CREATE, 'created'),
(ACTION_BULK_CREATE, 'bulk created'),
(ACTION_IMPORT, 'imported'),
(ACTION_EDIT, 'modified'),
(ACTION_BULK_EDIT, 'bulk edited'),
(ACTION_DELETE, 'deleted'),
(ACTION_BULK_DELETE, 'bulk deleted'),
)
#
@@ -316,8 +371,7 @@ class TopologyMap(models.Model):
# Add all circuits to the graph
for termination in CircuitTermination.objects.filter(term_side='A', interface__device__in=devices):
peer_termination = termination.get_peer_termination()
if (peer_termination is not None and peer_termination.interface is not None and
peer_termination.interface.device in devices):
if peer_termination is not None and peer_termination.interface.device in devices:
graph.edge(termination.interface.device.name, peer_termination.interface.device.name, color='blue')
return graph.pipe(format=img_format)
@@ -332,7 +386,7 @@ def image_upload(instance, filename):
path = 'image-attachments/'
# Rename the file to the provided name, if any. Attempt to preserve the file extension.
extension = filename.rsplit('.')[-1].lower()
extension = filename.rsplit('.')[-1]
if instance.name and extension in ['bmp', 'gif', 'jpeg', 'jpg', 'png']:
filename = '.'.join([instance.name, extension])
elif instance.name:

View File

@@ -25,7 +25,7 @@ class ImageAttachmentEditView(PermissionRequiredMixin, ObjectEditView):
class ImageAttachmentDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'extras.delete_imageattachment'
permission_required = 'dcim.delete_imageattachment'
model = ImageAttachment
def get_return_url(self, request, imageattachment):

View File

@@ -1,5 +1,4 @@
from __future__ import unicode_literals
from collections import OrderedDict
from rest_framework import serializers
from rest_framework.validators import UniqueTogetherValidator
@@ -7,11 +6,11 @@ from rest_framework.validators import UniqueTogetherValidator
from dcim.api.serializers import NestedDeviceSerializer, InterfaceSerializer, NestedSiteSerializer
from extras.api.customfields import CustomFieldModelSerializer
from ipam.models import (
Aggregate, IPAddress, IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, IP_PROTOCOL_CHOICES, Prefix,
PREFIX_STATUS_CHOICES, RIR, Role, Service, VLAN, VLAN_STATUS_CHOICES, VLANGroup, VRF,
Aggregate, IPAddress, IPADDRESS_STATUS_CHOICES, IP_PROTOCOL_CHOICES, Prefix, PREFIX_STATUS_CHOICES, RIR, Role,
Service, VLAN, VLAN_STATUS_CHOICES, VLANGroup, VRF,
)
from tenancy.api.serializers import NestedTenantSerializer
from utilities.api import ChoiceFieldSerializer, ModelValidationMixin
from utilities.api import ChoiceFieldSerializer
#
@@ -23,7 +22,7 @@ class VRFSerializer(CustomFieldModelSerializer):
class Meta:
model = VRF
fields = ['id', 'name', 'rd', 'tenant', 'enforce_unique', 'description', 'display_name', 'custom_fields']
fields = ['id', 'name', 'rd', 'tenant', 'enforce_unique', 'description', 'custom_fields']
class NestedVRFSerializer(serializers.ModelSerializer):
@@ -45,7 +44,7 @@ class WritableVRFSerializer(CustomFieldModelSerializer):
# Roles
#
class RoleSerializer(ModelValidationMixin, serializers.ModelSerializer):
class RoleSerializer(serializers.ModelSerializer):
class Meta:
model = Role
@@ -64,7 +63,7 @@ class NestedRoleSerializer(serializers.ModelSerializer):
# RIRs
#
class RIRSerializer(ModelValidationMixin, serializers.ModelSerializer):
class RIRSerializer(serializers.ModelSerializer):
class Meta:
model = RIR
@@ -142,9 +141,6 @@ class WritableVLANGroupSerializer(serializers.ModelSerializer):
validator.set_context(self)
validator(data)
# Enforce model validation
super(WritableVLANGroupSerializer, self).validate(data)
return data
@@ -191,9 +187,6 @@ class WritableVLANSerializer(CustomFieldModelSerializer):
validator.set_context(self)
validator(data)
# Enforce model validation
super(WritableVLANSerializer, self).validate(data)
return data
@@ -243,13 +236,12 @@ class IPAddressSerializer(CustomFieldModelSerializer):
vrf = NestedVRFSerializer()
tenant = NestedTenantSerializer()
status = ChoiceFieldSerializer(choices=IPADDRESS_STATUS_CHOICES)
role = ChoiceFieldSerializer(choices=IPADDRESS_ROLE_CHOICES)
interface = InterfaceSerializer()
class Meta:
model = IPAddress
fields = [
'id', 'family', 'address', 'vrf', 'tenant', 'status', 'role', 'interface', 'description', 'nat_inside',
'id', 'family', 'address', 'vrf', 'tenant', 'status', 'interface', 'description', 'nat_inside',
'nat_outside', 'custom_fields',
]
@@ -269,24 +261,7 @@ class WritableIPAddressSerializer(CustomFieldModelSerializer):
class Meta:
model = IPAddress
fields = [
'id', 'address', 'vrf', 'tenant', 'status', 'role', 'interface', 'description', 'nat_inside',
'custom_fields',
]
class AvailableIPSerializer(serializers.Serializer):
def to_representation(self, instance):
if self.context.get('vrf'):
vrf = NestedVRFSerializer(self.context['vrf'], context={'request': self.context['request']}).data
else:
vrf = None
return OrderedDict([
('family', self.context['prefix'].version),
('address', '{}/{}'.format(instance, self.context['prefix'].prefixlen)),
('vrf', vrf),
])
fields = ['id', 'address', 'vrf', 'tenant', 'status', 'interface', 'description', 'nat_inside', 'custom_fields']
#
@@ -303,7 +278,6 @@ class ServiceSerializer(serializers.ModelSerializer):
fields = ['id', 'device', 'name', 'port', 'protocol', 'ipaddresses', 'description']
# TODO: Figure out how to use ModelValidationMixin with ManyToManyFields. Calling clean() yields a ValueError.
class WritableServiceSerializer(serializers.ModelSerializer):
class Meta:

View File

@@ -1,14 +1,7 @@
from __future__ import unicode_literals
from rest_framework import status
from rest_framework.decorators import detail_route
from rest_framework.exceptions import PermissionDenied
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet
from django.conf import settings
from django.shortcuts import get_object_or_404
from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
from ipam import filters
from extras.api.views import CustomFieldModelViewSet
@@ -27,6 +20,15 @@ class VRFViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
filter_class = filters.VRFFilter
#
# Roles
#
class RoleViewSet(ModelViewSet):
queryset = Role.objects.all()
serializer_class = serializers.RoleSerializer
#
# RIRs
#
@@ -48,16 +50,6 @@ class AggregateViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
filter_class = filters.AggregateFilter
#
# Roles
#
class RoleViewSet(ModelViewSet):
queryset = Role.objects.all()
serializer_class = serializers.RoleSerializer
filter_class = filters.RoleFilter
#
# Prefixes
#
@@ -68,62 +60,6 @@ class PrefixViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
write_serializer_class = serializers.WritablePrefixSerializer
filter_class = filters.PrefixFilter
@detail_route(url_path='available-ips', methods=['get', 'post'])
def available_ips(self, request, pk=None):
"""
A convenience method for returning available IP addresses within a prefix. By default, the number of IPs
returned will be equivalent to PAGINATE_COUNT. An arbitrary limit (up to MAX_PAGE_SIZE, if set) may be passed,
however results will not be paginated.
"""
prefix = get_object_or_404(Prefix, pk=pk)
# Create the next available IP within the prefix
if request.method == 'POST':
# Permissions check
if not request.user.has_perm('ipam.add_ipaddress'):
raise PermissionDenied()
# Find the first available IP address in the prefix
try:
ipaddress = list(prefix.get_available_ips())[0]
except IndexError:
return Response(
{
"detail": "There are no available IPs within this prefix ({})".format(prefix)
},
status=status.HTTP_400_BAD_REQUEST
)
# Create the new IP address
data = request.data.copy()
data['address'] = '{}/{}'.format(ipaddress, prefix.prefix.prefixlen)
data['vrf'] = prefix.vrf
serializer = serializers.WritableIPAddressSerializer(data=data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
# Determine the maximum amount of IPs to return
else:
try:
limit = int(request.query_params.get('limit', settings.PAGINATE_COUNT))
except ValueError:
limit = settings.PAGINATE_COUNT
if settings.MAX_PAGE_SIZE:
limit = min(limit, settings.MAX_PAGE_SIZE)
# Calculate available IPs within the prefix
ip_list = list(prefix.get_available_ips())[:limit]
serializer = serializers.AvailableIPSerializer(ip_list, many=True, context={
'request': request,
'prefix': prefix.prefix,
'vrf': prefix.vrf,
})
return Response(serializer.data)
#
# IP addresses

View File

@@ -1,78 +0,0 @@
from __future__ import unicode_literals
# IP address families
AF_CHOICES = (
(4, 'IPv4'),
(6, 'IPv6'),
)
# Prefix statuses
PREFIX_STATUS_CONTAINER = 0
PREFIX_STATUS_ACTIVE = 1
PREFIX_STATUS_RESERVED = 2
PREFIX_STATUS_DEPRECATED = 3
PREFIX_STATUS_CHOICES = (
(PREFIX_STATUS_CONTAINER, 'Container'),
(PREFIX_STATUS_ACTIVE, 'Active'),
(PREFIX_STATUS_RESERVED, 'Reserved'),
(PREFIX_STATUS_DEPRECATED, 'Deprecated')
)
# IP address statuses
IPADDRESS_STATUS_ACTIVE = 1
IPADDRESS_STATUS_RESERVED = 2
IPADDRESS_STATUS_DEPRECATED = 3
IPADDRESS_STATUS_DHCP = 5
IPADDRESS_STATUS_CHOICES = (
(IPADDRESS_STATUS_ACTIVE, 'Active'),
(IPADDRESS_STATUS_RESERVED, 'Reserved'),
(IPADDRESS_STATUS_DEPRECATED, 'Deprecated'),
(IPADDRESS_STATUS_DHCP, 'DHCP')
)
# IP address roles
IPADDRESS_ROLE_LOOPBACK = 10
IPADDRESS_ROLE_SECONDARY = 20
IPADDRESS_ROLE_ANYCAST = 30
IPADDRESS_ROLE_VIP = 40
IPADDRESS_ROLE_VRRP = 41
IPADDRESS_ROLE_HSRP = 42
IPADDRESS_ROLE_GLBP = 43
IPADDRESS_ROLE_CHOICES = (
(IPADDRESS_ROLE_LOOPBACK, 'Loopback'),
(IPADDRESS_ROLE_SECONDARY, 'Secondary'),
(IPADDRESS_ROLE_ANYCAST, 'Anycast'),
(IPADDRESS_ROLE_VIP, 'VIP'),
(IPADDRESS_ROLE_VRRP, 'VRRP'),
(IPADDRESS_ROLE_HSRP, 'HSRP'),
(IPADDRESS_ROLE_GLBP, 'GLBP'),
)
# VLAN statuses
VLAN_STATUS_ACTIVE = 1
VLAN_STATUS_RESERVED = 2
VLAN_STATUS_DEPRECATED = 3
VLAN_STATUS_CHOICES = (
(VLAN_STATUS_ACTIVE, 'Active'),
(VLAN_STATUS_RESERVED, 'Reserved'),
(VLAN_STATUS_DEPRECATED, 'Deprecated')
)
# Bootstrap CSS classes for various statuses
STATUS_CHOICE_CLASSES = {
0: 'default',
1: 'primary',
2: 'info',
3: 'danger',
4: 'warning',
5: 'success',
}
# IP protocols (for services)
IP_PROTOCOL_TCP = 6
IP_PROTOCOL_UDP = 17
IP_PROTOCOL_CHOICES = (
(IP_PROTOCOL_TCP, 'TCP'),
(IP_PROTOCOL_UDP, 'UDP'),
)

View File

@@ -11,8 +11,8 @@ from extras.filters import CustomFieldFilterSet
from tenancy.models import Tenant
from utilities.filters import NullableModelMultipleChoiceFilter, NumericInFilter
from .models import (
Aggregate, IPAddress, IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, Prefix, PREFIX_STATUS_CHOICES, RIR, Role,
Service, VLAN, VLAN_STATUS_CHOICES, VLANGroup, VRF,
Aggregate, IPAddress, IPADDRESS_STATUS_CHOICES, Prefix, PREFIX_STATUS_CHOICES, RIR, Role, Service, VLAN,
VLAN_STATUS_CHOICES, VLANGroup, VRF,
)
@@ -23,6 +23,7 @@ class VRFFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Search',
)
tenant_id = NullableModelMultipleChoiceFilter(
name='tenant',
queryset=Tenant.objects.all(),
label='Tenant (ID)',
)
@@ -44,7 +45,7 @@ class VRFFilter(CustomFieldFilterSet, django_filters.FilterSet):
class Meta:
model = VRF
fields = ['name', 'rd', 'enforce_unique']
fields = ['name', 'rd']
class RIRFilter(django_filters.FilterSet):
@@ -52,7 +53,7 @@ class RIRFilter(django_filters.FilterSet):
class Meta:
model = RIR
fields = ['name', 'slug', 'is_private']
fields = ['is_private']
class AggregateFilter(CustomFieldFilterSet, django_filters.FilterSet):
@@ -62,6 +63,7 @@ class AggregateFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Search',
)
rir_id = django_filters.ModelMultipleChoiceFilter(
name='rir',
queryset=RIR.objects.all(),
label='RIR (ID)',
)
@@ -88,13 +90,6 @@ class AggregateFilter(CustomFieldFilterSet, django_filters.FilterSet):
return queryset.filter(qs_filter)
class RoleFilter(django_filters.FilterSet):
class Meta:
model = Role
fields = ['name', 'slug']
class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
id__in = NumericInFilter(name='id', lookup_expr='in')
q = django_filters.CharFilter(
@@ -110,6 +105,7 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Mask length',
)
vrf_id = NullableModelMultipleChoiceFilter(
name='vrf_id',
queryset=VRF.objects.all(),
label='VRF',
)
@@ -120,6 +116,7 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='VRF (RD)',
)
tenant_id = NullableModelMultipleChoiceFilter(
name='tenant',
queryset=Tenant.objects.all(),
label='Tenant (ID)',
)
@@ -130,6 +127,7 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Tenant (slug)',
)
site_id = NullableModelMultipleChoiceFilter(
name='site',
queryset=Site.objects.all(),
label='Site (ID)',
)
@@ -140,6 +138,7 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Site (slug)',
)
vlan_id = NullableModelMultipleChoiceFilter(
name='vlan',
queryset=VLAN.objects.all(),
label='VLAN (ID)',
)
@@ -148,6 +147,7 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='VLAN number (1-4095)',
)
role_id = NullableModelMultipleChoiceFilter(
name='role',
queryset=Role.objects.all(),
label='Role (ID)',
)
@@ -163,7 +163,7 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
class Meta:
model = Prefix
fields = ['family', 'is_pool']
fields = ['family']
def search(self, queryset, name, value):
if not value.strip():
@@ -207,6 +207,7 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Mask length',
)
vrf_id = NullableModelMultipleChoiceFilter(
name='vrf_id',
queryset=VRF.objects.all(),
label='VRF',
)
@@ -217,6 +218,7 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='VRF (RD)',
)
tenant_id = NullableModelMultipleChoiceFilter(
name='tenant',
queryset=Tenant.objects.all(),
label='Tenant (ID)',
)
@@ -238,15 +240,13 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Device (name)',
)
interface_id = django_filters.ModelMultipleChoiceFilter(
name='interface',
queryset=Interface.objects.all(),
label='Interface (ID)',
)
status = django_filters.MultipleChoiceFilter(
choices=IPADDRESS_STATUS_CHOICES
)
role = django_filters.MultipleChoiceFilter(
choices=IPADDRESS_ROLE_CHOICES
)
class Meta:
model = IPAddress
@@ -281,6 +281,7 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
class VLANGroupFilter(django_filters.FilterSet):
site_id = NullableModelMultipleChoiceFilter(
name='site',
queryset=Site.objects.all(),
label='Site (ID)',
)
@@ -293,7 +294,7 @@ class VLANGroupFilter(django_filters.FilterSet):
class Meta:
model = VLANGroup
fields = ['name', 'slug']
fields = ['name']
class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
@@ -303,6 +304,7 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Search',
)
site_id = NullableModelMultipleChoiceFilter(
name='site',
queryset=Site.objects.all(),
label='Site (ID)',
)
@@ -313,6 +315,7 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Site (slug)',
)
group_id = NullableModelMultipleChoiceFilter(
name='group',
queryset=VLANGroup.objects.all(),
label='Group (ID)',
)
@@ -323,6 +326,7 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Group',
)
tenant_id = NullableModelMultipleChoiceFilter(
name='tenant',
queryset=Tenant.objects.all(),
label='Tenant (ID)',
)
@@ -333,6 +337,7 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Tenant (slug)',
)
role_id = NullableModelMultipleChoiceFilter(
name='role',
queryset=Role.objects.all(),
label='Role (ID)',
)
@@ -348,7 +353,7 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
class Meta:
model = VLAN
fields = ['vid', 'name']
fields = ['name', 'vid']
def search(self, queryset, name, value):
if not value.strip():
@@ -363,6 +368,7 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
class ServiceFilter(django_filters.FilterSet):
device_id = django_filters.ModelMultipleChoiceFilter(
name='device',
queryset=Device.objects.all(),
label='Device (ID)',
)

View File

@@ -1,7 +1,6 @@
from __future__ import unicode_literals
from django import forms
from django.core.exceptions import MultipleObjectsReturned
from django.db.models import Count
from dcim.models import Site, Rack, Device, Interface
@@ -14,8 +13,8 @@ from utilities.forms import (
add_blank_choice,
)
from .models import (
Aggregate, IPAddress, IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, Prefix, PREFIX_STATUS_CHOICES, RIR, Role,
Service, VLAN, VLANGroup, VLAN_STATUS_CHOICES, VRF,
Aggregate, IPAddress, IPADDRESS_STATUS_CHOICES, Prefix, PREFIX_STATUS_CHOICES, RIR, Role, Service, VLAN,
VLANGroup, VLAN_STATUS_CHOICES, VRF,
)
@@ -302,10 +301,6 @@ class PrefixCSVForm(forms.ModelForm):
))
else:
raise forms.ValidationError("Global VLAN {} not found in group {}".format(vlan_vid, vlan_group))
except MultipleObjectsReturned:
raise forms.ValidationError(
"Multiple VLANs with VID {} found in group {}".format(vlan_vid, vlan_group)
)
elif vlan_vid:
try:
self.instance.vlan = VLAN.objects.get(site=site, group__isnull=True, vid=vlan_vid)
@@ -314,8 +309,6 @@ class PrefixCSVForm(forms.ModelForm):
raise forms.ValidationError("VLAN {} not found in site {}".format(vlan_vid, site))
else:
raise forms.ValidationError("Global VLAN {} not found".format(vlan_vid))
except MultipleObjectsReturned:
raise forms.ValidationError("Multiple VLANs with VID {} found".format(vlan_vid))
class PrefixBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
@@ -484,7 +477,7 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm)
class Meta:
model = IPAddress
fields = [
'address', 'vrf', 'status', 'role', 'description', 'interface', 'primary_for_device', 'nat_site', 'nat_rack',
'address', 'vrf', 'status', 'description', 'interface', 'primary_for_device', 'nat_site', 'nat_rack',
'nat_inside', 'tenant_group', 'tenant',
]
@@ -497,7 +490,7 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm)
initial['interface_site'] = instance.interface.device.site
initial['interface_rack'] = instance.interface.device.rack
initial['interface_device'] = instance.interface.device
if instance and instance.nat_inside and instance.nat_inside.device is not None:
if instance and instance.nat_inside is not None:
initial['nat_site'] = instance.nat_inside.device.site
initial['nat_rack'] = instance.nat_inside.device.rack
initial['nat_device'] = instance.nat_inside.device
@@ -562,7 +555,7 @@ class IPAddressBulkAddForm(BootstrapMixin, TenancyForm, CustomFieldForm):
class Meta:
model = IPAddress
fields = ['address', 'vrf', 'status', 'role', 'description', 'tenant_group', 'tenant']
fields = ['address', 'status', 'vrf', 'description', 'tenant_group', 'tenant']
def __init__(self, *args, **kwargs):
super(IPAddressBulkAddForm, self).__init__(*args, **kwargs)
@@ -589,14 +582,9 @@ class IPAddressCSVForm(forms.ModelForm):
}
)
status = CSVChoiceField(
choices=IPADDRESS_STATUS_CHOICES,
choices=PREFIX_STATUS_CHOICES,
help_text='Operational status'
)
role = CSVChoiceField(
choices=IPADDRESS_ROLE_CHOICES,
required=False,
help_text='Functional role'
)
device = FlexibleModelChoiceField(
queryset=Device.objects.all(),
required=False,
@@ -617,7 +605,7 @@ class IPAddressCSVForm(forms.ModelForm):
class Meta:
model = IPAddress
fields = ['address', 'vrf', 'tenant', 'status', 'role', 'device', 'interface_name', 'is_primary', 'description']
fields = ['address', 'vrf', 'tenant', 'status', 'device', 'interface_name', 'is_primary', 'description']
def clean(self):
@@ -646,23 +634,16 @@ class IPAddressCSVForm(forms.ModelForm):
# Set interface
if self.cleaned_data['device'] and self.cleaned_data['interface_name']:
self.instance.interface = Interface.objects.get(
device=self.cleaned_data['device'],
name=self.cleaned_data['interface_name']
)
ipaddress = super(IPAddressCSVForm, self).save(*args, **kwargs)
self.instance.interface = Interface.objects.get(device=self.cleaned_data['device'],
name=self.cleaned_data['interface_name'])
# Set as primary for device
if self.cleaned_data['is_primary']:
device = self.cleaned_data['device']
if self.instance.address.version == 4:
device.primary_ip4 = ipaddress
self.instance.primary_ip4_for = self.cleaned_data['device']
elif self.instance.address.version == 6:
device.primary_ip6 = ipaddress
device.save()
self.instance.primary_ip6_for = self.cleaned_data['device']
return ipaddress
return super(IPAddressCSVForm, self).save(*args, **kwargs)
class IPAddressBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
@@ -670,11 +651,10 @@ class IPAddressBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, label='VRF')
tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
status = forms.ChoiceField(choices=add_blank_choice(IPADDRESS_STATUS_CHOICES), required=False)
role = forms.ChoiceField(choices=add_blank_choice(IPADDRESS_ROLE_CHOICES), required=False)
description = forms.CharField(max_length=100, required=False)
class Meta:
nullable_fields = ['vrf', 'role', 'tenant', 'description']
nullable_fields = ['vrf', 'tenant', 'description']
def ipaddress_status_choices():
@@ -684,13 +664,6 @@ def ipaddress_status_choices():
return [(s[0], '{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in IPADDRESS_STATUS_CHOICES]
def ipaddress_role_choices():
role_counts = {}
for role in IPAddress.objects.values('role').annotate(count=Count('role')).order_by('role'):
role_counts[role['role']] = role['count']
return [(r[0], '{} ({})'.format(r[1], role_counts.get(r[0], 0))) for r in IPADDRESS_ROLE_CHOICES]
class IPAddressFilterForm(BootstrapMixin, CustomFieldFilterForm):
model = IPAddress
q = forms.CharField(required=False, label='Search')
@@ -711,7 +684,6 @@ class IPAddressFilterForm(BootstrapMixin, CustomFieldFilterForm):
null_option=(0, 'None')
)
status = forms.MultipleChoiceField(choices=ipaddress_status_choices, required=False)
role = forms.MultipleChoiceField(choices=ipaddress_role_choices, required=False)
#

View File

@@ -1,25 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.1 on 2017-06-16 19:37
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ipam', '0016_unicode_literals'),
]
operations = [
migrations.AddField(
model_name='ipaddress',
name='role',
field=models.PositiveSmallIntegerField(blank=True, choices=[(10, 'Loopback'), (20, 'Secondary'), (30, 'Anycast'), (40, 'VIP'), (41, 'VRRP'), (42, 'HSRP'), (43, 'GLBP')], help_text='The functional role of this IP', null=True, verbose_name='Role'),
),
migrations.AlterField(
model_name='ipaddress',
name='status',
field=models.PositiveSmallIntegerField(choices=[(1, 'Active'), (2, 'Reserved'), (3, 'Deprecated'), (5, 'DHCP')], default=1, help_text='The operational status of this IP', verbose_name='Status'),
),
]

View File

@@ -1,5 +1,6 @@
from __future__ import unicode_literals
import netaddr
from netaddr import IPNetwork, cidr_merge
from django.conf import settings
from django.contrib.contenttypes.fields import GenericRelation
@@ -16,10 +17,63 @@ from tenancy.models import Tenant
from utilities.models import CreatedUpdatedModel
from utilities.sql import NullsFirstQuerySet
from utilities.utils import csv_format
from .constants import *
from .fields import IPNetworkField, IPAddressField
AF_CHOICES = (
(4, 'IPv4'),
(6, 'IPv6'),
)
PREFIX_STATUS_CONTAINER = 0
PREFIX_STATUS_ACTIVE = 1
PREFIX_STATUS_RESERVED = 2
PREFIX_STATUS_DEPRECATED = 3
PREFIX_STATUS_CHOICES = (
(PREFIX_STATUS_CONTAINER, 'Container'),
(PREFIX_STATUS_ACTIVE, 'Active'),
(PREFIX_STATUS_RESERVED, 'Reserved'),
(PREFIX_STATUS_DEPRECATED, 'Deprecated')
)
IPADDRESS_STATUS_ACTIVE = 1
IPADDRESS_STATUS_RESERVED = 2
IPADDRESS_STATUS_DEPRECATED = 3
IPADDRESS_STATUS_DHCP = 5
IPADDRESS_STATUS_CHOICES = (
(IPADDRESS_STATUS_ACTIVE, 'Active'),
(IPADDRESS_STATUS_RESERVED, 'Reserved'),
(IPADDRESS_STATUS_DEPRECATED, 'Deprecated'),
(IPADDRESS_STATUS_DHCP, 'DHCP')
)
VLAN_STATUS_ACTIVE = 1
VLAN_STATUS_RESERVED = 2
VLAN_STATUS_DEPRECATED = 3
VLAN_STATUS_CHOICES = (
(VLAN_STATUS_ACTIVE, 'Active'),
(VLAN_STATUS_RESERVED, 'Reserved'),
(VLAN_STATUS_DEPRECATED, 'Deprecated')
)
STATUS_CHOICE_CLASSES = {
0: 'default',
1: 'primary',
2: 'info',
3: 'danger',
4: 'warning',
5: 'success',
}
IP_PROTOCOL_TCP = 6
IP_PROTOCOL_UDP = 17
IP_PROTOCOL_CHOICES = (
(IP_PROTOCOL_TCP, 'TCP'),
(IP_PROTOCOL_UDP, 'UDP'),
)
@python_2_unicode_compatible
class VRF(CreatedUpdatedModel, CustomFieldModel):
"""
@@ -43,7 +97,7 @@ class VRF(CreatedUpdatedModel, CustomFieldModel):
verbose_name_plural = 'VRFs'
def __str__(self):
return self.display_name or super(VRF, self).__str__()
return self.name
def get_absolute_url(self):
return reverse('ipam:vrf', args=[self.pk])
@@ -57,12 +111,6 @@ class VRF(CreatedUpdatedModel, CustomFieldModel):
self.description,
])
@property
def display_name(self):
if self.name and self.rd:
return "{} ({})".format(self.name, self.rd)
return None
@python_2_unicode_compatible
class RIR(models.Model):
@@ -158,9 +206,13 @@ class Aggregate(CreatedUpdatedModel, CustomFieldModel):
"""
Determine the prefix utilization of the aggregate and return it as a percentage.
"""
queryset = Prefix.objects.filter(prefix__net_contained_or_equal=str(self.prefix))
child_prefixes = netaddr.IPSet([p.prefix for p in queryset])
return int(float(child_prefixes.size) / self.prefix.size * 100)
child_prefixes = Prefix.objects.filter(prefix__net_contained_or_equal=str(self.prefix))
# Remove overlapping prefixes from list of children
networks = cidr_merge([c.prefix for c in child_prefixes])
children_size = float(0)
for p in networks:
children_size += p.size
return int(children_size / self.prefix.size * 100)
@python_2_unicode_compatible
@@ -316,56 +368,25 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel):
def get_duplicates(self):
return Prefix.objects.filter(vrf=self.vrf, prefix=str(self.prefix)).exclude(pk=self.pk)
def get_child_ips(self):
"""
Return all IPAddresses within this Prefix.
"""
return IPAddress.objects.filter(address__net_contained_or_equal=str(self.prefix), vrf=self.vrf)
def get_available_ips(self):
"""
Return all available IPs within this prefix as an IPSet.
"""
prefix = netaddr.IPSet(self.prefix)
child_ips = netaddr.IPSet([ip.address.ip for ip in self.get_child_ips()])
available_ips = prefix - child_ips
# Remove unusable IPs from non-pool prefixes
if not self.is_pool:
available_ips -= netaddr.IPSet([
netaddr.IPAddress(self.prefix.first),
netaddr.IPAddress(self.prefix.last),
])
return available_ips
def get_utilization(self):
"""
Determine the utilization of the prefix and return it as a percentage. For Prefixes with a status of
"container", calculate utilization based on child prefixes. For all others, count child IP addresses.
Determine the utilization of the prefix and return it as a percentage.
"""
if self.status == PREFIX_STATUS_CONTAINER:
queryset = Prefix.objects.filter(prefix__net_contained=str(self.prefix), vrf=self.vrf)
child_prefixes = netaddr.IPSet([p.prefix for p in queryset])
return int(float(child_prefixes.size) / self.prefix.size * 100)
else:
child_count = IPAddress.objects.filter(
address__net_contained_or_equal=str(self.prefix), vrf=self.vrf
).count()
prefix_size = self.prefix.size
if self.family == 4 and self.prefix.prefixlen < 31 and not self.is_pool:
prefix_size -= 2
return int(float(child_count) / prefix_size * 100)
child_count = IPAddress.objects.filter(address__net_contained_or_equal=str(self.prefix), vrf=self.vrf).count()
prefix_size = self.prefix.size
if self.family == 4 and self.prefix.prefixlen < 31 and not self.is_pool:
prefix_size -= 2
return int(float(child_count) / prefix_size * 100)
@property
def new_subnet(self):
if self.family == 4:
if self.prefix.prefixlen <= 30:
return netaddr.IPNetwork('{}/{}'.format(self.prefix.network, self.prefix.prefixlen + 1))
return IPNetwork('{}/{}'.format(self.prefix.network, self.prefix.prefixlen + 1))
return None
if self.family == 6:
if self.prefix.prefixlen <= 126:
return netaddr.IPNetwork('{}/{}'.format(self.prefix.network, self.prefix.prefixlen + 1))
return IPNetwork('{}/{}'.format(self.prefix.network, self.prefix.prefixlen + 1))
return None
@@ -400,13 +421,7 @@ class IPAddress(CreatedUpdatedModel, CustomFieldModel):
vrf = models.ForeignKey('VRF', related_name='ip_addresses', on_delete=models.PROTECT, blank=True, null=True,
verbose_name='VRF')
tenant = models.ForeignKey(Tenant, related_name='ip_addresses', blank=True, null=True, on_delete=models.PROTECT)
status = models.PositiveSmallIntegerField(
'Status', choices=IPADDRESS_STATUS_CHOICES, default=IPADDRESS_STATUS_ACTIVE,
help_text='The operational status of this IP'
)
role = models.PositiveSmallIntegerField(
'Role', choices=IPADDRESS_ROLE_CHOICES, blank=True, null=True, help_text='The functional role of this IP'
)
status = models.PositiveSmallIntegerField('Status', choices=IPADDRESS_STATUS_CHOICES, default=1)
interface = models.ForeignKey(Interface, related_name='ip_addresses', on_delete=models.CASCADE, blank=True,
null=True)
nat_inside = models.OneToOneField('self', related_name='nat_outside', on_delete=models.SET_NULL, blank=True,
@@ -417,9 +432,7 @@ class IPAddress(CreatedUpdatedModel, CustomFieldModel):
objects = IPAddressManager()
csv_headers = [
'address', 'vrf', 'tenant', 'status', 'role', 'device', 'interface_name', 'is_primary', 'description',
]
csv_headers = ['address', 'vrf', 'tenant', 'status', 'device', 'interface_name', 'is_primary', 'description']
class Meta:
ordering = ['family', 'address']
@@ -471,7 +484,6 @@ class IPAddress(CreatedUpdatedModel, CustomFieldModel):
self.vrf.rd if self.vrf else None,
self.tenant.name if self.tenant else None,
self.get_status_display(),
self.get_role_display(),
self.device.identifier if self.device else None,
self.interface.name if self.interface else None,
is_primary,

View File

@@ -241,7 +241,7 @@ class PrefixTable(BaseTable):
prefix = tables.TemplateColumn(PREFIX_LINK, attrs={'th': {'style': 'padding-left: 17px'}})
status = tables.TemplateColumn(STATUS_LABEL)
vrf = tables.TemplateColumn(VRF_LINK, verbose_name='VRF')
get_utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False, verbose_name='Utilization')
get_utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False, verbose_name='IP Usage')
tenant = tables.TemplateColumn(TENANT_LINK)
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')])
vlan = tables.LinkColumn('ipam:vlan', args=[Accessor('vlan.pk')], verbose_name='VLAN')
@@ -299,7 +299,7 @@ class IPAddressTable(BaseTable):
class Meta(BaseTable.Meta):
model = IPAddress
fields = ('pk', 'address', 'vrf', 'status', 'role', 'tenant', 'nat_inside', 'device', 'description')
fields = ('pk', 'address', 'status', 'vrf', 'tenant', 'nat_inside', 'device', 'description')
row_attrs = {
'class': lambda record: 'success' if not isinstance(record, IPAddress) else '',
}
@@ -328,7 +328,7 @@ class IPAddressSearchTable(SearchTable):
class Meta(SearchTable.Meta):
model = IPAddress
fields = ('address', 'vrf', 'status', 'role', 'tenant', 'device', 'interface', 'description')
fields = ('address', 'status', 'vrf', 'tenant', 'device', 'interface', 'description')
#

View File

@@ -367,35 +367,6 @@ class PrefixTest(HttpStatusMixin, APITestCase):
self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
self.assertEqual(Prefix.objects.count(), 2)
def test_available_ips(self):
prefix = Prefix.objects.create(prefix=IPNetwork('192.0.2.0/29'), is_pool=True)
url = reverse('ipam-api:prefix-available-ips', kwargs={'pk': prefix.pk})
# Retrieve all available IPs
response = self.client.get(url, **self.header)
self.assertEqual(len(response.data), 8) # 8 because prefix.is_pool = True
# Change the prefix to not be a pool and try again
prefix.is_pool = False
prefix.save()
response = self.client.get(url, **self.header)
self.assertEqual(len(response.data), 6) # 8 - 2 because prefix.is_pool = False
# Create all six available IPs
for i in range(6):
data = {
'description': 'Test IP {}'.format(i)
}
response = self.client.post(url, data, **self.header)
self.assertHttpStatus(response, status.HTTP_201_CREATED)
self.assertEqual(response.data['description'], data['description'])
# Try to create one more IP
response = self.client.post(url, {}, **self.header)
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
self.assertIn('detail', response.data)
class IPAddressTest(HttpStatusMixin, APITestCase):

View File

@@ -10,7 +10,7 @@ urlpatterns = [
# VRFs
url(r'^vrfs/$', views.VRFListView.as_view(), name='vrf_list'),
url(r'^vrfs/add/$', views.VRFCreateView.as_view(), name='vrf_add'),
url(r'^vrfs/add/$', views.VRFEditView.as_view(), name='vrf_add'),
url(r'^vrfs/import/$', views.VRFBulkImportView.as_view(), name='vrf_import'),
url(r'^vrfs/edit/$', views.VRFBulkEditView.as_view(), name='vrf_bulk_edit'),
url(r'^vrfs/delete/$', views.VRFBulkDeleteView.as_view(), name='vrf_bulk_delete'),
@@ -20,13 +20,13 @@ urlpatterns = [
# RIRs
url(r'^rirs/$', views.RIRListView.as_view(), name='rir_list'),
url(r'^rirs/add/$', views.RIRCreateView.as_view(), name='rir_add'),
url(r'^rirs/add/$', views.RIREditView.as_view(), name='rir_add'),
url(r'^rirs/delete/$', views.RIRBulkDeleteView.as_view(), name='rir_bulk_delete'),
url(r'^rirs/(?P<slug>[\w-]+)/edit/$', views.RIREditView.as_view(), name='rir_edit'),
# Aggregates
url(r'^aggregates/$', views.AggregateListView.as_view(), name='aggregate_list'),
url(r'^aggregates/add/$', views.AggregateCreateView.as_view(), name='aggregate_add'),
url(r'^aggregates/add/$', views.AggregateEditView.as_view(), name='aggregate_add'),
url(r'^aggregates/import/$', views.AggregateBulkImportView.as_view(), name='aggregate_import'),
url(r'^aggregates/edit/$', views.AggregateBulkEditView.as_view(), name='aggregate_bulk_edit'),
url(r'^aggregates/delete/$', views.AggregateBulkDeleteView.as_view(), name='aggregate_bulk_delete'),
@@ -36,13 +36,13 @@ urlpatterns = [
# Roles
url(r'^roles/$', views.RoleListView.as_view(), name='role_list'),
url(r'^roles/add/$', views.RoleCreateView.as_view(), name='role_add'),
url(r'^roles/add/$', views.RoleEditView.as_view(), name='role_add'),
url(r'^roles/delete/$', views.RoleBulkDeleteView.as_view(), name='role_bulk_delete'),
url(r'^roles/(?P<slug>[\w-]+)/edit/$', views.RoleEditView.as_view(), name='role_edit'),
# Prefixes
url(r'^prefixes/$', views.PrefixListView.as_view(), name='prefix_list'),
url(r'^prefixes/add/$', views.PrefixCreateView.as_view(), name='prefix_add'),
url(r'^prefixes/add/$', views.PrefixEditView.as_view(), name='prefix_add'),
url(r'^prefixes/import/$', views.PrefixBulkImportView.as_view(), name='prefix_import'),
url(r'^prefixes/edit/$', views.PrefixBulkEditView.as_view(), name='prefix_bulk_edit'),
url(r'^prefixes/delete/$', views.PrefixBulkDeleteView.as_view(), name='prefix_bulk_delete'),
@@ -53,8 +53,8 @@ urlpatterns = [
# IP addresses
url(r'^ip-addresses/$', views.IPAddressListView.as_view(), name='ipaddress_list'),
url(r'^ip-addresses/add/$', views.IPAddressCreateView.as_view(), name='ipaddress_add'),
url(r'^ip-addresses/bulk-add/$', views.IPAddressBulkCreateView.as_view(), name='ipaddress_bulk_add'),
url(r'^ip-addresses/add/$', views.IPAddressEditView.as_view(), name='ipaddress_add'),
url(r'^ip-addresses/bulk-add/$', views.IPAddressBulkAddView.as_view(), name='ipaddress_bulk_add'),
url(r'^ip-addresses/import/$', views.IPAddressBulkImportView.as_view(), name='ipaddress_import'),
url(r'^ip-addresses/edit/$', views.IPAddressBulkEditView.as_view(), name='ipaddress_bulk_edit'),
url(r'^ip-addresses/delete/$', views.IPAddressBulkDeleteView.as_view(), name='ipaddress_bulk_delete'),
@@ -64,13 +64,13 @@ urlpatterns = [
# VLAN groups
url(r'^vlan-groups/$', views.VLANGroupListView.as_view(), name='vlangroup_list'),
url(r'^vlan-groups/add/$', views.VLANGroupCreateView.as_view(), name='vlangroup_add'),
url(r'^vlan-groups/add/$', views.VLANGroupEditView.as_view(), name='vlangroup_add'),
url(r'^vlan-groups/delete/$', views.VLANGroupBulkDeleteView.as_view(), name='vlangroup_bulk_delete'),
url(r'^vlan-groups/(?P<pk>\d+)/edit/$', views.VLANGroupEditView.as_view(), name='vlangroup_edit'),
# VLANs
url(r'^vlans/$', views.VLANListView.as_view(), name='vlan_list'),
url(r'^vlans/add/$', views.VLANCreateView.as_view(), name='vlan_add'),
url(r'^vlans/add/$', views.VLANEditView.as_view(), name='vlan_add'),
url(r'^vlans/import/$', views.VLANBulkImportView.as_view(), name='vlan_import'),
url(r'^vlans/edit/$', views.VLANBulkEditView.as_view(), name='vlan_bulk_edit'),
url(r'^vlans/delete/$', views.VLANBulkDeleteView.as_view(), name='vlan_bulk_delete'),

View File

@@ -13,7 +13,7 @@ from django.views.generic import View
from dcim.models import Device
from utilities.paginator import EnhancedPaginator
from utilities.views import (
BulkCreateView, BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
BulkAddView, BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
)
from . import filters, forms, tables
from .models import (
@@ -114,18 +114,14 @@ class VRFView(View):
})
class VRFCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.add_vrf'
class VRFEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.change_vrf'
model = VRF
form_class = forms.VRFForm
template_name = 'ipam/vrf_edit.html'
default_return_url = 'ipam:vrf_list'
class VRFEditView(VRFCreateView):
permission_required = 'ipam.change_vrf'
class VRFDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'ipam.delete_vrf'
model = VRF
@@ -243,8 +239,8 @@ class RIRListView(ObjectListView):
}
class RIRCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.add_rir'
class RIREditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.change_rir'
model = RIR
form_class = forms.RIRForm
@@ -252,10 +248,6 @@ class RIRCreateView(PermissionRequiredMixin, ObjectEditView):
return reverse('ipam:rir_list')
class RIREditView(RIRCreateView):
permission_required = 'ipam.change_rir'
class RIRBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'ipam.delete_rir'
cls = RIR
@@ -332,18 +324,14 @@ class AggregateView(View):
})
class AggregateCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.add_aggregate'
class AggregateEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.change_aggregate'
model = Aggregate
form_class = forms.AggregateForm
template_name = 'ipam/aggregate_edit.html'
default_return_url = 'ipam:aggregate_list'
class AggregateEditView(AggregateCreateView):
permission_required = 'ipam.change_aggregate'
class AggregateDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'ipam.delete_aggregate'
model = Aggregate
@@ -383,8 +371,8 @@ class RoleListView(ObjectListView):
template_name = 'ipam/role_list.html'
class RoleCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.add_role'
class RoleEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.change_role'
model = Role
form_class = forms.RoleForm
@@ -392,10 +380,6 @@ class RoleCreateView(PermissionRequiredMixin, ObjectEditView):
return reverse('ipam:role_list')
class RoleEditView(RoleCreateView):
permission_required = 'ipam.change_role'
class RoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'ipam.delete_role'
cls = Role
@@ -535,18 +519,14 @@ class PrefixIPAddressesView(View):
})
class PrefixCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.add_prefix'
class PrefixEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.change_prefix'
model = Prefix
form_class = forms.PrefixForm
template_name = 'ipam/prefix_edit.html'
default_return_url = 'ipam:prefix_list'
class PrefixEditView(PrefixCreateView):
permission_required = 'ipam.change_prefix'
class PrefixDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'ipam.delete_prefix'
model = Prefix
@@ -632,25 +612,21 @@ class IPAddressView(View):
})
class IPAddressCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.add_ipaddress'
class IPAddressEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.change_ipaddress'
model = IPAddress
form_class = forms.IPAddressForm
template_name = 'ipam/ipaddress_edit.html'
default_return_url = 'ipam:ipaddress_list'
class IPAddressEditView(IPAddressCreateView):
permission_required = 'ipam.change_ipaddress'
class IPAddressDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'ipam.delete_ipaddress'
model = IPAddress
default_return_url = 'ipam:ipaddress_list'
class IPAddressBulkCreateView(PermissionRequiredMixin, BulkCreateView):
class IPAddressBulkAddView(PermissionRequiredMixin, BulkAddView):
permission_required = 'ipam.add_ipaddress'
pattern_form = forms.IPAddressPatternForm
model_form = forms.IPAddressBulkAddForm
@@ -665,6 +641,19 @@ class IPAddressBulkImportView(PermissionRequiredMixin, BulkImportView):
table = tables.IPAddressTable
default_return_url = 'ipam:ipaddress_list'
def save_obj(self, obj):
obj.save()
# Update primary IP for device if needed. The Device must be updated directly in the database; otherwise we risk
# overwriting a previous IP assignment from the same import (see #861).
try:
if obj.family == 4 and obj.primary_ip4_for:
Device.objects.filter(pk=obj.primary_ip4_for.pk).update(primary_ip4=obj)
elif obj.family == 6 and obj.primary_ip6_for:
Device.objects.filter(pk=obj.primary_ip6_for.pk).update(primary_ip6=obj)
except Device.DoesNotExist:
pass
class IPAddressBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'ipam.change_ipaddress'
@@ -694,8 +683,8 @@ class VLANGroupListView(ObjectListView):
template_name = 'ipam/vlangroup_list.html'
class VLANGroupCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.add_vlangroup'
class VLANGroupEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.change_vlangroup'
model = VLANGroup
form_class = forms.VLANGroupForm
@@ -703,10 +692,6 @@ class VLANGroupCreateView(PermissionRequiredMixin, ObjectEditView):
return reverse('ipam:vlangroup_list')
class VLANGroupEditView(VLANGroupCreateView):
permission_required = 'ipam.change_vlangroup'
class VLANGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'ipam.delete_vlangroup'
cls = VLANGroup
@@ -743,18 +728,14 @@ class VLANView(View):
})
class VLANCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.add_vlan'
class VLANEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.change_vlan'
model = VLAN
form_class = forms.VLANForm
template_name = 'ipam/vlan_edit.html'
default_return_url = 'ipam:vlan_list'
class VLANEditView(VLANCreateView):
permission_required = 'ipam.change_vlan'
class VLANDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'ipam.delete_vlan'
model = VLAN
@@ -788,8 +769,8 @@ class VLANBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
# Services
#
class ServiceCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.add_service'
class ServiceEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.change_service'
model = Service
form_class = forms.ServiceForm
template_name = 'ipam/service_edit.html'
@@ -803,10 +784,6 @@ class ServiceCreateView(PermissionRequiredMixin, ObjectEditView):
return obj.device.get_absolute_url()
class ServiceEditView(ServiceCreateView):
permission_required = 'ipam.change_service'
class ServiceDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'ipam.delete_service'
model = Service

View File

@@ -13,7 +13,7 @@ except ImportError:
)
VERSION = '2.1-beta'
VERSION = '2.0.6'
# Import required configuration parameters
ALLOWED_HOSTS = DATABASE = SECRET_KEY = None

View File

@@ -1,7 +1,6 @@
$(document).ready(function() {
var search_field = $('#id_livesearch');
var real_field = $('#id_' + search_field.attr('data-field'));
var select_fields = $('#select select');
var search_key = search_field.attr('data-key');
var label = search_field.attr('data-label');
if (!label) {
@@ -41,22 +40,13 @@ $(document).ready(function() {
select: function(event, ui) {
event.preventDefault();
search_field.val(ui.item.label);
select_fields.val('');
select_fields.attr('disabled', 'disabled');
real_field.empty();
real_field.append($("<option></option>").attr('value', ui.item.value).text(ui.item.label));
real_field.change();
// Disable parent selection fields
// $('select[filter-for="' + real_field.attr('name') + '"]').val('');
// If the field has a parent helper, reset the parent to no selection
$('select[filter-for="' + real_field.attr('name') + '"]').val('');
},
minLength: 4,
delay: 500
});
search_field.change(function() {
if (!search_field.val()) {
select_fields.removeAttr('disabled');
select_fields.val('');
}
});
});

View File

@@ -5,14 +5,13 @@ from rest_framework.validators import UniqueTogetherValidator
from dcim.api.serializers import NestedDeviceSerializer
from secrets.models import Secret, SecretRole
from utilities.api import ModelValidationMixin
#
# SecretRoles
#
class SecretRoleSerializer(ModelValidationMixin, serializers.ModelSerializer):
class SecretRoleSerializer(serializers.ModelSerializer):
class Meta:
model = SecretRole
@@ -56,7 +55,4 @@ class WritableSecretSerializer(serializers.ModelSerializer):
validator.set_context(self)
validator(data)
# Enforce model validation
super(WritableSecretSerializer, self).validate(data)
return data

View File

@@ -30,7 +30,6 @@ class SecretRoleViewSet(ModelViewSet):
queryset = SecretRole.objects.all()
serializer_class = serializers.SecretRoleSerializer
permission_classes = [IsAuthenticated]
filter_class = filters.SecretRoleFilter
#

View File

@@ -9,13 +9,6 @@ from dcim.models import Device
from utilities.filters import NumericInFilter
class SecretRoleFilter(django_filters.FilterSet):
class Meta:
model = SecretRole
fields = ['name', 'slug']
class SecretFilter(django_filters.FilterSet):
id__in = NumericInFilter(name='id', lookup_expr='in')
q = django_filters.CharFilter(
@@ -23,6 +16,7 @@ class SecretFilter(django_filters.FilterSet):
label='Search',
)
role_id = django_filters.ModelMultipleChoiceFilter(
name='role',
queryset=SecretRole.objects.all(),
label='Role (ID)',
)
@@ -33,6 +27,7 @@ class SecretFilter(django_filters.FilterSet):
label='Role (slug)',
)
device_id = django_filters.ModelMultipleChoiceFilter(
name='device',
queryset=Device.objects.all(),
label='Device (ID)',
)

View File

@@ -10,7 +10,7 @@ urlpatterns = [
# Secret roles
url(r'^secret-roles/$', views.SecretRoleListView.as_view(), name='secretrole_list'),
url(r'^secret-roles/add/$', views.SecretRoleCreateView.as_view(), name='secretrole_add'),
url(r'^secret-roles/add/$', views.SecretRoleEditView.as_view(), name='secretrole_add'),
url(r'^secret-roles/delete/$', views.SecretRoleBulkDeleteView.as_view(), name='secretrole_bulk_delete'),
url(r'^secret-roles/(?P<slug>[\w-]+)/edit/$', views.SecretRoleEditView.as_view(), name='secretrole_edit'),

View File

@@ -40,8 +40,8 @@ class SecretRoleListView(ObjectListView):
template_name = 'secrets/secretrole_list.html'
class SecretRoleCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'secrets.add_secretrole'
class SecretRoleEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'secrets.change_secretrole'
model = SecretRole
form_class = forms.SecretRoleForm
@@ -49,10 +49,6 @@ class SecretRoleCreateView(PermissionRequiredMixin, ObjectEditView):
return reverse('secrets:secretrole_list')
class SecretRoleEditView(SecretRoleCreateView):
permission_required = 'secrets.change_secretrole'
class SecretRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'secrets.delete_secretrole'
cls = SecretRole

View File

@@ -1,13 +0,0 @@
{% extends 'utilities/confirmation_form.html' %}
{% load helpers %}
{% block title %}Disconnect {{ obj_type_plural|bettertitle }}{% endblock %}
{% block message %}
<p>Are you sure you want to disconnect all {{ selected_objects|length }} of these {{ obj_type_plural }} on <strong>{{ device }}</strong>?</p>
<ul>
{% for obj in selected_objects %}
<li>{{ obj }}</li>
{% endfor %}
</ul>
{% endblock %}

View File

@@ -7,7 +7,7 @@
{% block content %}
{% include 'dcim/inc/device_header.html' with active_tab='info' %}
<div class="row">
<div class="col-md-5">
<div class="col-md-5 col-lg-6">
<div class="panel panel-default">
<div class="panel-heading">
<strong>Device</strong>
@@ -204,7 +204,7 @@
None
</div>
{% endif %}
{% if perms.ipam.add_service %}
{% if perms.dcim.add_service %}
<div class="panel-footer text-right">
<a href="{% url 'dcim:service_assign' device=device.pk %}" class="btn btn-xs btn-primary">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Assign service
@@ -214,9 +214,23 @@
</div>
<div class="panel panel-default">
<div class="panel-heading">
<strong>Console / Power</strong>
<strong>Critical Connections</strong>
</div>
<table class="table table-hover panel-body component-list">
{% for iface in mgmt_interfaces %}
{% include 'dcim/inc/interface.html' with icon='wrench' %}
{% empty %}
{% if device.device_type.interface_templates.exists %}
<tr>
<td colspan="6" class="alert-warning">
<i class="fa fa-fw fa-warning"></i> No management interfaces defined
{% if perms.dcim.add_interface %}
<a href="{% url 'dcim:interface_add' pk=device.pk %}?mgmt_only=1" class="btn btn-primary btn-xs pull-right"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span></a>
{% endif %}
</td>
</tr>
{% endif %}
{% endfor %}
{% for cp in console_ports %}
{% include 'dcim/inc/consoleport.html' %}
{% empty %}
@@ -248,6 +262,11 @@
</table>
{% if perms.dcim.add_interface or perms.dcim.add_consoleport or perms.dcim.add_powerport %}
<div class="panel-footer text-right">
{% if perms.dcim.add_interface %}
<a href="{% url 'dcim:interface_add' pk=device.pk %}?mgmt_only=1" class="btn btn-xs btn-primary">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add interface
</a>
{% endif %}
{% if perms.dcim.add_consoleport %}
<a href="{% url 'dcim:consoleport_add' pk=device.pk %}" class="btn btn-xs btn-primary">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add console port
@@ -314,7 +333,7 @@
{% endif %}
</div>
</div>
<div class="col-md-7">
<div class="col-md-7 col-lg-6">
{% if device_bays or device.device_type.is_parent_device %}
{% if perms.dcim.delete_devicebay %}
<form method="post" action="{% url 'dcim:devicebay_bulk_delete' pk=device.pk %}">
@@ -405,17 +424,12 @@
<div class="panel-footer">
{% if interfaces and perms.dcim.change_interface %}
<button type="submit" name="_edit" formaction="{% url 'dcim:interface_bulk_edit' pk=device.pk %}" class="btn btn-warning btn-xs">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
</button>
{% endif %}
{% if interfaces and perms.dcim.delete_interfaceconnection %}
<button type="submit" name="_disconnect" formaction="{% url 'dcim:interface_bulk_disconnect' pk=device.pk %}" class="btn btn-danger btn-xs">
<span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit selected
</button>
{% endif %}
{% if interfaces and perms.dcim.delete_interface %}
<button type="submit" name="_delete" formaction="{% url 'dcim:interface_bulk_delete' pk=device.pk %}" class="btn btn-danger btn-xs">
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete selected
</button>
{% endif %}
{% if perms.dcim.add_interface %}
@@ -465,14 +479,9 @@
</table>
{% if perms.dcim.add_consoleserverport or perms.dcim.delete_consoleserverport %}
<div class="panel-footer">
{% if cs_ports and perms.dcim.change_consoleport %}
<button type="submit" name="_disconnect" formaction="{% url 'dcim:consoleserverport_bulk_disconnect' pk=device.pk %}" class="btn btn-danger btn-xs">
<span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
</button>
{% endif %}
{% if cs_ports and perms.dcim.delete_consoleserverport %}
<button type="submit" class="btn btn-danger btn-xs">
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete selected
</button>
{% endif %}
{% if perms.dcim.add_consoleserverport %}
@@ -522,14 +531,9 @@
</table>
{% if perms.dcim.add_poweroutlet or perms.dcim.delete_poweroutlet %}
<div class="panel-footer">
{% if power_outlets and perms.dcim.change_powerport %}
<button type="submit" name="_disconnect" formaction="{% url 'dcim:poweroutlet_bulk_disconnect' pk=device.pk %}" class="btn btn-danger btn-xs">
<span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
</button>
{% endif %}
{% if power_outlets and perms.dcim.delete_poweroutlet %}
<button type="submit" class="btn btn-danger btn-xs">
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete selected
</button>
{% endif %}
{% if perms.dcim.add_poweroutlet %}
@@ -572,7 +576,7 @@ function toggleConnection(elem, api_url) {
success: function() {
elem.parents('tr').removeClass('success').addClass('info');
elem.removeClass('connected btn-warning').addClass('btn-success');
elem.attr('title', 'Mark installed');
elem.attr('title', 'Mark connected');
elem.children('i').removeClass('glyphicon glyphicon-ban-circle').addClass('fa fa-plug')
}
});
@@ -591,7 +595,7 @@ function toggleConnection(elem, api_url) {
success: function() {
elem.parents('tr').removeClass('info').addClass('success');
elem.removeClass('btn-success').addClass('connected btn-warning');
elem.attr('title', 'Mark planned');
elem.attr('title', 'Mark disconnected');
elem.children('i').removeClass('fa fa-plug').addClass('glyphicon glyphicon-ban-circle')
}
});

View File

@@ -51,8 +51,6 @@
<th>Manufacturer</th>
<th>Part Number</th>
<th>Serial Number</th>
<th>Asset Tag</th>
<th>Description</th>
<th></th>
</tr>
</thead>

View File

@@ -33,7 +33,7 @@
<h1>{{ devicetype.manufacturer }} {{ devicetype.model }}</h1>
<div class="row">
<div class="col-md-5">
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">
<strong>Chassis</strong>
@@ -163,20 +163,21 @@
{% endif %}
</div>
</div>
</div>
<div class="col-md-7">
{% include 'dcim/inc/devicetype_component_table.html' with table=consoleport_table title='Console Ports' add_url='dcim:devicetype_add_consoleport' delete_url='dcim:devicetype_delete_consoleport' %}
{% include 'dcim/inc/devicetype_component_table.html' with table=powerport_table title='Power Ports' add_url='dcim:devicetype_add_powerport' delete_url='dcim:devicetype_delete_powerport' %}
{% if devicetype.is_parent_device or devicebay_table.rows %}
{% include 'dcim/inc/devicetype_component_table.html' with table=mgmt_interface_table title='Management Interfaces' add_url='dcim:devicetype_add_interface' add_url_extra='?mgmt_only=1' edit_url='dcim:devicetype_bulkedit_interface' delete_url='dcim:devicetype_delete_interface' %}
</div>
<div class="col-md-6">
{% if devicetype.is_parent_device %}
{% include 'dcim/inc/devicetype_component_table.html' with table=devicebay_table title='Device Bays' add_url='dcim:devicetype_add_devicebay' delete_url='dcim:devicetype_delete_devicebay' %}
{% endif %}
{% if devicetype.is_network_device or interface_table.rows %}
{% if devicetype.is_network_device %}
{% include 'dcim/inc/devicetype_component_table.html' with table=interface_table title='Interfaces' add_url='dcim:devicetype_add_interface' edit_url='dcim:devicetype_bulkedit_interface' delete_url='dcim:devicetype_delete_interface' %}
{% endif %}
{% if devicetype.is_console_server or consoleserverport_table.rows %}
{% if devicetype.is_console_server %}
{% include 'dcim/inc/devicetype_component_table.html' with table=consoleserverport_table title='Console Server Ports' add_url='dcim:devicetype_add_consoleserverport' delete_url='dcim:devicetype_delete_consoleserverport' %}
{% endif %}
{% if devicetype.is_pdu or poweroutlet_table.rows %}
{% if devicetype.is_pdu %}
{% include 'dcim/inc/devicetype_component_table.html' with table=poweroutlet_table title='Power Outlets' add_url='dcim:devicetype_add_poweroutlet' delete_url='dcim:devicetype_delete_poweroutlet' %}
{% endif %}
</div>

View File

@@ -1,7 +1,13 @@
<tr class="consoleport{% if cp.cs_port and not cp.connection_status %} info{% endif %}">
{% if selectable and perms.dcim.change_consoleport or perms.dcim.delete_consoleport %}
<td class="pk">
<input name="pk" type="checkbox" value="{{ cp.pk }}" />
</td>
{% endif %}
<td>
<i class="fa fa-fw fa-keyboard-o"></i> {{ cp.name }}
</td>
<td></td>
{% if cp.cs_port %}
<td>
<a href="{% url 'dcim:device' pk=cp.cs_port.device.pk %}">{{ cp.cs_port.device }}</a>
@@ -14,28 +20,28 @@
<span class="text-muted">Not connected</span>
</td>
{% endif %}
<td class="text-right">
<td colspan="2" class="text-right">
{% if perms.dcim.change_consoleport %}
{% if cp.cs_port %}
{% if cp.connection_status %}
<a href="#" class="btn btn-warning btn-xs consoleport-toggle connected" title="Mark planned" data="{{ cp.pk }}">
<i class="glyphicon glyphicon-ban-circle" aria-hidden="true"></i>
<a href="#" class="btn btn-warning btn-xs consoleport-toggle connected" data="{{ cp.pk }}">
<i class="glyphicon glyphicon-ban-circle" aria-hidden="true" title="Mark planned"></i>
</a>
{% else %}
<a href="#" class="btn btn-success btn-xs consoleport-toggle" title="Mark installed" data="{{ cp.pk }}">
<i class="fa fa-plug" aria-hidden="true"></i>
<a href="#" class="btn btn-success btn-xs consoleport-toggle" data="{{ cp.pk }}">
<i class="fa fa-plug" aria-hidden="true" title="Mark connected"></i>
</a>
{% endif %}
<a href="{% url 'dcim:consoleport_disconnect' pk=cp.pk %}" title="Delete connection" class="btn btn-danger btn-xs">
<i class="glyphicon glyphicon-resize-full" aria-hidden="true"></i>
<a href="{% url 'dcim:consoleport_disconnect' pk=cp.pk %}" class="btn btn-danger btn-xs">
<i class="glyphicon glyphicon-resize-full" aria-hidden="true" title="Delete connection"></i>
</a>
{% else %}
<a href="{% url 'dcim:consoleport_connect' pk=cp.pk %}" title="Connect" class="btn btn-success btn-xs">
<i class="glyphicon glyphicon-resize-small" aria-hidden="true"></i>
<a href="{% url 'dcim:consoleport_connect' pk=cp.pk %}" class="btn btn-success btn-xs">
<i class="glyphicon glyphicon-resize-small" aria-hidden="true" title="Connect"></i>
</a>
{% endif %}
<a href="{% url 'dcim:consoleport_edit' pk=cp.pk %}" title="Edit port" class="btn btn-info btn-xs">
<i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
<a href="{% url 'dcim:consoleport_edit' pk=cp.pk %}" class="btn btn-info btn-xs">
<i class="glyphicon glyphicon-pencil" aria-hidden="true" title="Edit port"></i>
</a>
{% endif %}
{% if perms.dcim.delete_consoleport %}
@@ -44,8 +50,8 @@
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
</button>
{% else %}
<a href="{% url 'dcim:consoleport_delete' pk=cp.pk %}" title="Delete port" class="btn btn-danger btn-xs">
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
<a href="{% url 'dcim:consoleport_delete' pk=cp.pk %}" class="btn btn-danger btn-xs">
<i class="glyphicon glyphicon-trash" aria-hidden="true" title="Delete port"></i>
</a>
{% endif %}
{% endif %}

View File

@@ -24,24 +24,24 @@
{% if perms.dcim.change_consoleserverport %}
{% if csp.connected_console %}
{% if csp.connected_console.connection_status %}
<a href="#" class="btn btn-warning btn-xs consoleport-toggle connected" title="Mark planned" data="{{ csp.connected_console.pk }}">
<i class="glyphicon glyphicon-ban-circle" aria-hidden="true"></i>
<a href="#" class="btn btn-warning btn-xs consoleport-toggle connected" data="{{ csp.connected_console.pk }}">
<i class="glyphicon glyphicon-ban-circle" aria-hidden="true" title="Mark planned"></i>
</a>
{% else %}
<a href="#" class="btn btn-success btn-xs consoleport-toggle" title="Mark installed" data="{{ csp.connected_console.pk }}">
<i class="fa fa-plug" aria-hidden="true"></i>
<a href="#" class="btn btn-success btn-xs consoleport-toggle" data="{{ csp.connected_console.pk }}">
<i class="fa fa-plug" aria-hidden="true" title="Mark connected"></i>
</a>
{% endif %}
<a href="{% url 'dcim:consoleserverport_disconnect' pk=csp.pk %}" title="Delete connection" class="btn btn-danger btn-xs">
<i class="glyphicon glyphicon-resize-full" aria-hidden="true"></i>
<a href="{% url 'dcim:consoleserverport_disconnect' pk=csp.pk %}" class="btn btn-danger btn-xs">
<i class="glyphicon glyphicon-resize-full" aria-hidden="true" title="Delete connection"></i>
</a>
{% else %}
<a href="{% url 'dcim:consoleserverport_connect' pk=csp.pk %}" title="Connect" class="btn btn-success btn-xs">
<i class="glyphicon glyphicon-resize-small" aria-hidden="true"></i>
<a href="{% url 'dcim:consoleserverport_connect' pk=csp.pk %}" class="btn btn-success btn-xs">
<i class="glyphicon glyphicon-resize-small" aria-hidden="true" title="Connect"></i>
</a>
{% endif %}
<a href="{% url 'dcim:consoleserverport_edit' pk=csp.pk %}" title="Edit port" class="btn btn-info btn-xs">
<i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
<a href="{% url 'dcim:consoleserverport_edit' pk=csp.pk %}" class="btn btn-info btn-xs">
<i class="glyphicon glyphicon-pencil" aria-hidden="true" title="Edit port"></i>
</a>
{% endif %}
{% if perms.dcim.delete_consoleserverport %}
@@ -50,8 +50,8 @@
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
</button>
{% else %}
<a href="{% url 'dcim:consoleserverport_delete' pk=csp.pk %}" title="Delete port" class="btn btn-danger btn-xs">
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
<a href="{% url 'dcim:consoleserverport_delete' pk=csp.pk %}" class="btn btn-danger btn-xs">
<i class="glyphicon glyphicon-trash" aria-hidden="true" title="Delete port"></i>
</a>
{% endif %}
{% endif %}

View File

@@ -45,7 +45,7 @@
<ul class="nav nav-tabs" style="margin-bottom: 20px">
<li role="presentation"{% if active_tab == 'info' %} class="active"{% endif %}><a href="{% url 'dcim:device' pk=device.pk %}">Info</a></li>
<li role="presentation"{% if active_tab == 'inventory' %} class="active"{% endif %}><a href="{% url 'dcim:device_inventory' pk=device.pk %}">Inventory</a></li>
{% if device.status == 1 and device.platform.rpc_client and device.primary_ip %}
{% if device.status %}
<li role="presentation"{% if active_tab == 'lldp-neighbors' %} class="active"{% endif %}><a href="{% url 'dcim:device_lldp_neighbors' pk=device.pk %}">LLDP Neighbors</a></li>
{% endif %}
</ul>

View File

@@ -1,12 +1,11 @@
<tr class="interface{% if not iface.enabled %} danger{% elif iface.connection and iface.connection.connection_status %} success{% elif iface.connection and not iface.connection.connection_status %} info{% endif %}">
<tr class="interface{% if iface.connection and not iface.connection.connection_status %} info{% endif %}">
{% if selectable and perms.dcim.change_interface or perms.dcim.delete_interface %}
<td class="pk">
<input name="pk" type="checkbox" value="{{ iface.pk }}" />
</td>
{% endif %}
<td>
<i class="fa fa-fw fa-{% if iface.mgmt_only %}wrench{% elif iface.is_virtual %}square{% elif iface.is_wireless %}wifi{% else %}exchange{% endif %}"></i>
<span title="{{ iface.get_form_factor_display }}">{{ iface.name }}</span>
<i class="fa fa-fw fa-{{ icon|default:"exchange" }}"></i> <span title="{{ iface.get_form_factor_display }}">{{ iface.name }}</span>
{% if iface.lag %}
<span class="label label-primary">{{ iface.lag.name }}</span>
{% endif %}
@@ -14,7 +13,6 @@
<i class="fa fa-fw fa-comment-o" title="{{ iface.description }}"></i>
{% endif %}
</td>
<td>{{ iface.mtu|default:"" }}</td>
<td>{{ iface.mac_address|default:"" }}</td>
{% if iface.is_lag %}
<td colspan="2" class="text-muted">
@@ -23,8 +21,6 @@
</td>
{% elif iface.is_virtual %}
<td colspan="2" class="text-muted">Virtual interface</td>
{% elif iface.is_wireless %}
<td colspan="2" class="text-muted">Wireless interface</td>
{% elif iface.connection %}
{% with iface.connected_interface as connected_iface %}
<td>
@@ -76,7 +72,7 @@
<i class="glyphicon glyphicon-ban-circle" aria-hidden="true"></i>
</a>
{% else %}
<a href="#" class="btn btn-success btn-xs interface-toggle" data="{{ iface.connection.pk }}" title="Mark installed">
<a href="#" class="btn btn-success btn-xs interface-toggle" data="{{ iface.connection.pk }}" title="Mark connected">
<i class="fa fa-plug" aria-hidden="true"></i>
</a>
{% endif %}

View File

@@ -1,11 +1,9 @@
<tr>
<td style="padding-left: {{ indent|add:5 }}px">{{ item.name }}</td>
<td>{% if not item.discovered %}<i class="fa fa-asterisk" title="Manually created"></i>{% endif %}</td>
<td>{{ item.manufacturer|default:"" }}</td>
<td>{{ item.manufacturer|default:'' }}</td>
<td>{{ item.part_id }}</td>
<td>{{ item.serial }}</td>
<td>{{ item.asset_tag|default:"" }}</td>
<td>{{ item.description }}</td>
<td class="text-right">
{% if perms.dcim.change_inventoryitem %}
<a href="{% url 'dcim:inventoryitem_edit' pk=item.pk %}" class="btn btn-xs btn-warning"><span class="glyphicon glyphicon-pencil" aria-hidden="true"></span></a>

View File

@@ -24,24 +24,24 @@
{% if perms.dcim.change_poweroutlet %}
{% if po.connected_port %}
{% if po.connected_port.connection_status %}
<a href="#" class="btn btn-warning btn-xs powerport-toggle connected" title="Mark planned" data="{{ po.connected_port.pk }}">
<i class="glyphicon glyphicon-ban-circle" aria-hidden="true"></i>
<a href="#" class="btn btn-warning btn-xs powerport-toggle connected" data="{{ po.connected_port.pk }}">
<i class="glyphicon glyphicon-ban-circle" aria-hidden="true" title="Mark planned"></i>
</a>
{% else %}
<a href="#" class="btn btn-success btn-xs consoleport-toggle" title="Mark installed" data="{{ po.connected_port.pk }}">
<i class="fa fa-plug" aria-hidden="true"></i>
<a href="#" class="btn btn-success btn-xs consoleport-toggle" data="{{ po.connected_port.pk }}">
<i class="fa fa-plug" aria-hidden="true" title="Mark connected"></i>
</a>
{% endif %}
<a href="{% url 'dcim:poweroutlet_disconnect' pk=po.pk %}" title="Delete connection" class="btn btn-danger btn-xs">
<i class="glyphicon glyphicon-resize-full" aria-hidden="true"></i>
<a href="{% url 'dcim:poweroutlet_disconnect' pk=po.pk %}" class="btn btn-danger btn-xs">
<i class="glyphicon glyphicon-resize-full" aria-hidden="true" title="Delete connection"></i>
</a>
{% else %}
<a href="{% url 'dcim:poweroutlet_connect' pk=po.pk %}" title="Connect" class="btn btn-success btn-xs">
<i class="glyphicon glyphicon-resize-small" aria-hidden="true"></i>
<a href="{% url 'dcim:poweroutlet_connect' pk=po.pk %}" class="btn btn-success btn-xs">
<i class="glyphicon glyphicon-resize-small" aria-hidden="true" title="Connect"></i>
</a>
{% endif %}
<a href="{% url 'dcim:poweroutlet_edit' pk=po.pk %}" title="Edit outlet" class="btn btn-info btn-xs">
<i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
<a href="{% url 'dcim:poweroutlet_edit' pk=po.pk %}" class="btn btn-info btn-xs">
<i class="glyphicon glyphicon-pencil" aria-hidden="true" title="Edit outlet"></i>
</a>
{% endif %}
{% if perms.dcim.delete_poweroutlet %}
@@ -50,8 +50,8 @@
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
</button>
{% else %}
<a href="{% url 'dcim:poweroutlet_delete' pk=po.pk %}" title="Delete outlet" class="btn btn-danger btn-xs">
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
<a href="{% url 'dcim:poweroutlet_delete' pk=po.pk %}" class="btn btn-danger btn-xs">
<i class="glyphicon glyphicon-trash" aria-hidden="true" title="Delete outlet"></i>
</a>
{% endif %}
{% endif %}

View File

@@ -1,7 +1,13 @@
<tr class="powerport{% if pp.power_outlet and not pp.connection_status %} info{% endif %}">
{% if selectable and perms.dcim.change_powerport or perms.dcim.delete_powerport %}
<td class="pk">
<input name="pk" type="checkbox" value="{{ pp.pk }}" />
</td>
{% endif %}
<td>
<i class="fa fa-fw fa-bolt"></i> {{ pp.name }}
</td>
<td></td>
{% if pp.power_outlet %}
<td>
<a href="{% url 'dcim:device' pk=pp.power_outlet.device.pk %}">{{ pp.power_outlet.device }}</a>
@@ -14,28 +20,28 @@
<span class="text-muted">Not connected</span>
</td>
{% endif %}
<td class="text-right">
<td colspan="2" class="text-right">
{% if perms.dcim.change_powerport %}
{% if pp.power_outlet %}
{% if pp.connection_status %}
<a href="#" class="btn btn-warning btn-xs powerport-toggle connected" title="Mark planned" data="{{ pp.pk }}">
<i class="glyphicon glyphicon-ban-circle" aria-hidden="true"></i>
<a href="#" class="btn btn-warning btn-xs powerport-toggle connected" data="{{ pp.pk }}">
<i class="glyphicon glyphicon-ban-circle" aria-hidden="true" title="Mark planned"></i>
</a>
{% else %}
<a href="#" class="btn btn-success btn-xs powerport-toggle" title="Mark installed" data="{{ pp.pk }}">
<i class="fa fa-plug" aria-hidden="true"></i>
<a href="#" class="btn btn-success btn-xs powerport-toggle" data="{{ pp.pk }}">
<i class="fa fa-plug" aria-hidden="true" title="Mark connected"></i>
</a>
{% endif %}
<a href="{% url 'dcim:powerport_disconnect' pk=pp.pk %}" title="Delete connection" class="btn btn-danger btn-xs">
<i class="glyphicon glyphicon-resize-full" aria-hidden="true"></i>
<a href="{% url 'dcim:powerport_disconnect' pk=pp.pk %}" class="btn btn-danger btn-xs">
<i class="glyphicon glyphicon-resize-full" aria-hidden="true" title="Delete connection"></i>
</a>
{% else %}
<a href="{% url 'dcim:powerport_connect' pk=pp.pk %}" title="Connect" class="btn btn-success btn-xs">
<i class="glyphicon glyphicon-resize-small" aria-hidden="true"></i>
<a href="{% url 'dcim:powerport_connect' pk=pp.pk %}" class="btn btn-success btn-xs">
<i class="glyphicon glyphicon-resize-small" aria-hidden="true" title="Connect"></i>
</a>
{% endif %}
<a href="{% url 'dcim:powerport_edit' pk=pp.pk %}" title="Edit port" class="btn btn-info btn-xs">
<i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
<a href="{% url 'dcim:powerport_edit' pk=pp.pk %}" class="btn btn-info btn-xs">
<i class="glyphicon glyphicon-pencil" aria-hidden="true" title="Edit port"></i>
</a>
{% endif %}
{% if perms.dcim.delete_powerport %}
@@ -44,8 +50,8 @@
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
</button>
{% else %}
<a href="{% url 'dcim:powerport_delete' pk=pp.pk %}" title="Delete port" class="btn btn-danger btn-xs">
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
<a href="{% url 'dcim:powerport_delete' pk=pp.pk %}" class="btn btn-danger btn-xs">
<i class="glyphicon glyphicon-trash" aria-hidden="true" title="Delete port"></i>
</a>
{% endif %}
{% endif %}

View File

@@ -82,12 +82,6 @@
<span class="label label-{{ ipaddress.get_status_class }}">{{ ipaddress.get_status_display }}</span>
</td>
</tr>
<tr>
<td>Role</td>
<td>
<a href="{% url 'ipam:ipaddress_list' %}?role={{ ipaddress.role }}">{{ ipaddress.get_role_display }}</a>
</td>
</tr>
<tr>
<td>Description</td>
<td>

View File

@@ -14,7 +14,6 @@
<div class="panel-body">
{% render_field pattern_form.pattern %}
{% render_field model_form.status %}
{% render_field model_form.role %}
{% render_field model_form.vrf %}
{% render_field model_form.description %}
</div>

View File

@@ -14,7 +14,6 @@
<div class="panel-body">
{% render_field form.address %}
{% render_field form.status %}
{% render_field form.role %}
{% render_field form.vrf %}
{% render_field form.description %}
</div>

View File

@@ -19,12 +19,6 @@
{% endif %}
</h4>
{% include 'inc/created_updated.html' with obj=userkey %}
{% if not userkey.is_active %}
<div class="alert alert-warning" role="alert">
<i class="fa fa-warning"></i>
Your user key is inactive. Ask an administrator to enable it for you.
</div>
{% endif %}
<pre>{{ userkey.public_key }}</pre>
<hr />
{% if userkey.session_key %}

View File

@@ -4,14 +4,13 @@ from rest_framework import serializers
from extras.api.customfields import CustomFieldModelSerializer
from tenancy.models import Tenant, TenantGroup
from utilities.api import ModelValidationMixin
#
# Tenant groups
#
class TenantGroupSerializer(ModelValidationMixin, serializers.ModelSerializer):
class TenantGroupSerializer(serializers.ModelSerializer):
class Meta:
model = TenantGroup

View File

@@ -3,8 +3,8 @@ from __future__ import unicode_literals
from rest_framework.viewsets import ModelViewSet
from extras.api.views import CustomFieldModelViewSet
from tenancy import filters
from tenancy.models import Tenant, TenantGroup
from tenancy.filters import TenantFilter
from utilities.api import WritableSerializerMixin
from . import serializers
@@ -16,7 +16,6 @@ from . import serializers
class TenantGroupViewSet(ModelViewSet):
queryset = TenantGroup.objects.all()
serializer_class = serializers.TenantGroupSerializer
filter_class = filters.TenantGroupFilter
#
@@ -27,4 +26,4 @@ class TenantViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
queryset = Tenant.objects.select_related('group')
serializer_class = serializers.TenantSerializer
write_serializer_class = serializers.WritableTenantSerializer
filter_class = filters.TenantFilter
filter_class = TenantFilter

View File

@@ -9,13 +9,6 @@ from utilities.filters import NullableModelMultipleChoiceFilter, NumericInFilter
from .models import Tenant, TenantGroup
class TenantGroupFilter(django_filters.FilterSet):
class Meta:
model = TenantGroup
fields = ['name', 'slug']
class TenantFilter(CustomFieldFilterSet, django_filters.FilterSet):
id__in = NumericInFilter(name='id', lookup_expr='in')
q = django_filters.CharFilter(
@@ -23,6 +16,7 @@ class TenantFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Search',
)
group_id = NullableModelMultipleChoiceFilter(
name='group',
queryset=TenantGroup.objects.all(),
label='Group (ID)',
)

View File

@@ -10,13 +10,13 @@ urlpatterns = [
# Tenant groups
url(r'^tenant-groups/$', views.TenantGroupListView.as_view(), name='tenantgroup_list'),
url(r'^tenant-groups/add/$', views.TenantGroupCreateView.as_view(), name='tenantgroup_add'),
url(r'^tenant-groups/add/$', views.TenantGroupEditView.as_view(), name='tenantgroup_add'),
url(r'^tenant-groups/delete/$', views.TenantGroupBulkDeleteView.as_view(), name='tenantgroup_bulk_delete'),
url(r'^tenant-groups/(?P<slug>[\w-]+)/edit/$', views.TenantGroupEditView.as_view(), name='tenantgroup_edit'),
# Tenants
url(r'^tenants/$', views.TenantListView.as_view(), name='tenant_list'),
url(r'^tenants/add/$', views.TenantCreateView.as_view(), name='tenant_add'),
url(r'^tenants/add/$', views.TenantEditView.as_view(), name='tenant_add'),
url(r'^tenants/import/$', views.TenantBulkImportView.as_view(), name='tenant_import'),
url(r'^tenants/edit/$', views.TenantBulkEditView.as_view(), name='tenant_bulk_edit'),
url(r'^tenants/delete/$', views.TenantBulkDeleteView.as_view(), name='tenant_bulk_delete'),

View File

@@ -26,8 +26,8 @@ class TenantGroupListView(ObjectListView):
template_name = 'tenancy/tenantgroup_list.html'
class TenantGroupCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'tenancy.add_tenantgroup'
class TenantGroupEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'tenancy.change_tenantgroup'
model = TenantGroup
form_class = forms.TenantGroupForm
@@ -35,10 +35,6 @@ class TenantGroupCreateView(PermissionRequiredMixin, ObjectEditView):
return reverse('tenancy:tenantgroup_list')
class TenantGroupEditView(TenantGroupCreateView):
permission_required = 'tenancy.change_tenantgroup'
class TenantGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'tenancy.delete_tenantgroup'
cls = TenantGroup
@@ -85,18 +81,14 @@ class TenantView(View):
})
class TenantCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'tenancy.add_tenant'
class TenantEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'tenancy.change_tenant'
model = Tenant
form_class = forms.TenantForm
template_name = 'tenancy/tenant_edit.html'
default_return_url = 'tenancy:tenant_list'
class TenantEditView(TenantCreateView):
permission_required = 'tenancy.change_tenant'
class TenantDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'tenancy.delete_tenant'
model = Tenant

View File

@@ -98,17 +98,6 @@ class ContentTypeFieldSerializer(Field):
raise ValidationError("Invalid content type")
class ModelValidationMixin(object):
"""
Enforce a model's validation through clean() when validating serializer data. This is necessary to ensure we're
employing the same validation logic via both forms and the API.
"""
def validate(self, attrs):
instance = self.Meta.model(**attrs)
instance.clean()
return attrs
class WritableSerializerMixin(object):
"""
Allow for the use of an alternate, writable serializer class for write operations (e.g. POST, PUT).

View File

@@ -249,7 +249,7 @@ class CSVDataField(forms.CharField):
reader = csv.reader(value.splitlines())
# Consume and valdiate the first line of CSV data as column headers
headers = next(reader)
headers = reader.next()
for f in self.required_fields:
if f not in headers:
raise forms.ValidationError('Required column header "{}" not found.'.format(f))
@@ -489,12 +489,6 @@ class ChainedFieldsMixin(forms.BaseForm):
if filters_dict:
field.queryset = field.queryset.filter(**filters_dict)
elif not self.is_bound and getattr(self, 'instance', None) and hasattr(self.instance, field_name):
obj = getattr(self.instance, field_name)
if obj is not None:
field.queryset = field.queryset.filter(pk=obj.pk)
else:
field.queryset = field.queryset.none()
elif not self.is_bound:
field.queryset = field.queryset.none()

View File

@@ -234,7 +234,7 @@ class ObjectDeleteView(GetReturnURLMixin, View):
"""
Delete a single object.
model: The model of the object being deleted
model: The model of the object being edited
template_name: The name of the template
default_return_url: Name of the URL to which the user is redirected after deleting the object
"""
@@ -290,7 +290,7 @@ class ObjectDeleteView(GetReturnURLMixin, View):
})
class BulkCreateView(View):
class BulkAddView(View):
"""
Create new objects in bulk.