mirror of
https://github.com/netbox-community/netbox.git
synced 2026-02-05 00:19:32 +01:00
Compare commits
25 Commits
v2.1-beta1
...
v2.0.4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f7b0d22f86 | ||
|
|
ad95b86fdd | ||
|
|
43e1e0dbc8 | ||
|
|
f731900e2f | ||
|
|
b1bcaa33e7 | ||
|
|
17873706b7 | ||
|
|
e0ad2b4555 | ||
|
|
f89d91783b | ||
|
|
3ffe36e5ed | ||
|
|
be393a9d10 | ||
|
|
27eefd8705 | ||
|
|
097e0f38ff | ||
|
|
ce26b566a4 | ||
|
|
0e14bc1e02 | ||
|
|
ce6796ed9b | ||
|
|
c90cecc2fb | ||
|
|
b6bbcb0609 | ||
|
|
23f6832d9c | ||
|
|
88dace75a1 | ||
|
|
8eb140fd65 | ||
|
|
1f09f3d096 | ||
|
|
66be85a41f | ||
|
|
814c11167e | ||
|
|
57ddd5086f | ||
|
|
c171547037 |
@@ -136,8 +136,3 @@ The response will return devices 1 through 100. The URL provided in the `next` a
|
||||
"results": [...]
|
||||
}
|
||||
```
|
||||
|
||||
The maximum number of objects that can be returned is limited by the [`MAX_PAGE_SIZE`](../configuration/optional-settings/#max_page_size) setting, which is 1000 by default. Setting this to `0` or `None` will remove the maximum limit. An API consumer can then pass `?limit=0` to retrieve _all_ matching objects with a single request.
|
||||
|
||||
!!! warning
|
||||
Disabling the page size limit introduces a potential for very resource-intensive requests, since one API request can effectively retrieve an entire table from the database.
|
||||
|
||||
@@ -83,34 +83,6 @@ Enforcement of unique IP space can be toggled on a per-VRF basis. To enforce uni
|
||||
|
||||
---
|
||||
|
||||
## LOGGING
|
||||
|
||||
By default, all messages of INFO severity or higher will be logged to the console. Additionally, if `DEBUG` is False and email access has been configured, ERROR and CRITICAL messages will be emailed to the users defined in `ADMINS`.
|
||||
|
||||
The Django framework on which NetBox runs allows for the customization of logging, e.g. to write logs to file. Please consult the [Django logging documentation](https://docs.djangoproject.com/en/1.11/topics/logging/) for more information on configuring this setting. Below is an example which will write all INFO and higher messages to a file:
|
||||
|
||||
```
|
||||
LOGGING = {
|
||||
'version': 1,
|
||||
'disable_existing_loggers': False,
|
||||
'handlers': {
|
||||
'file': {
|
||||
'level': 'INFO',
|
||||
'class': 'logging.FileHandler',
|
||||
'filename': '/var/log/netbox.log',
|
||||
},
|
||||
},
|
||||
'loggers': {
|
||||
'django': {
|
||||
'handlers': ['file'],
|
||||
'level': 'INFO',
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## LOGIN_REQUIRED
|
||||
|
||||
Default: False
|
||||
@@ -127,14 +99,6 @@ Setting this to True will display a "maintenance mode" banner at the top of ever
|
||||
|
||||
---
|
||||
|
||||
## MAX_PAGE_SIZE
|
||||
|
||||
Default: 1000
|
||||
|
||||
An API consumer can request an arbitrary number of objects by appending the "limit" parameter to the URL (e.g. `?limit=1000`). This setting defines the maximum limit. Setting it to `0` or `None` will allow an API consumer to request all objects by specifying `?limit=0`.
|
||||
|
||||
---
|
||||
|
||||
## NETBOX_USERNAME
|
||||
|
||||
## NETBOX_PASSWORD
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
This guide explains how to implement LDAP authentication using an external server. User authentication will fall back to built-in Django users in the event of a failure.
|
||||
This guide explains how to implement LDAP authentication using an external server. User authentication will fall back to
|
||||
built-in Django users in the event of a failure.
|
||||
|
||||
# Requirements
|
||||
|
||||
@@ -28,9 +29,6 @@ Create a file in the same directory as `configuration.py` (typically `netbox/net
|
||||
|
||||
## General Server Configuration
|
||||
|
||||
!!! info
|
||||
When using Windows Server 2012 you may need to specify a port on `AUTH_LDAP_SERVER_URI`. Use `3269` for secure, or `3268` for non-secure.
|
||||
|
||||
```python
|
||||
import ldap
|
||||
|
||||
@@ -54,9 +52,6 @@ LDAP_IGNORE_CERT_ERRORS = True
|
||||
|
||||
## User Authentication
|
||||
|
||||
!!! info
|
||||
When using Windows Server, `2012 AUTH_LDAP_USER_DN_TEMPLATE` should be set to None.
|
||||
|
||||
```python
|
||||
from django_auth_ldap.config import LDAPSearch
|
||||
|
||||
@@ -104,16 +99,3 @@ AUTH_LDAP_FIND_GROUP_PERMS = True
|
||||
AUTH_LDAP_CACHE_GROUPS = True
|
||||
AUTH_LDAP_GROUP_CACHE_TIMEOUT = 3600
|
||||
```
|
||||
|
||||
* `is_active` - All users must be mapped to at least this group to enable authentication. Without this, users cannot log in.
|
||||
* `is_staff` - Users mapped to this group are enabled for access to the administration tools; this is the equivalent of checking the "staff status" box on a manually created user. This doesn't grant any specific permissions.
|
||||
* `is_superuser` - Users mapped to this group will be granted superuser status. Superusers are implicitly granted all permissions.
|
||||
|
||||
It is also possible map user attributes to Django attributes:
|
||||
|
||||
```python
|
||||
AUTH_LDAP_USER_ATTR_MAP = {
|
||||
"first_name": "givenName",
|
||||
"last_name": "sn",
|
||||
}
|
||||
```
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -52,13 +52,6 @@ Once the new code is in place, run the upgrade script (which may need to be run
|
||||
# ./upgrade.sh
|
||||
```
|
||||
|
||||
!!! warning
|
||||
The upgrade script will prefer Python3 and pip3 if both executables are available. To force it to use Python2 and pip, use the `-2` argument as below.
|
||||
|
||||
```no-highlight
|
||||
# ./upgrade.sh -2
|
||||
```
|
||||
|
||||
This script:
|
||||
|
||||
* Installs or upgrades any new required Python packages
|
||||
@@ -71,7 +64,7 @@ This script:
|
||||
Your models have changes that are not yet reflected in a migration, and so won't be applied.
|
||||
Run 'manage.py makemigrations' to make new migrations, and then re-run 'manage.py migrate' to apply them.
|
||||
|
||||
This may occur due to semantic differences in environment, and can be safely ignored. Never attempt to create new migrations unless you are intentionally modifying the database schema.
|
||||
This may occur due to semantic differences in environment, and can be safely ignored. Never attempt to create new migrations unless you are inentionally modifying the database schema.
|
||||
|
||||
# Restart the WSGI Service
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -43,7 +43,6 @@ class ProviderViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
|
||||
class CircuitTypeViewSet(ModelViewSet):
|
||||
queryset = CircuitType.objects.all()
|
||||
serializer_class = serializers.CircuitTypeSerializer
|
||||
filter_class = filters.CircuitTypeFilter
|
||||
|
||||
|
||||
#
|
||||
|
||||
@@ -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'),
|
||||
)
|
||||
@@ -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']
|
||||
|
||||
@@ -3,13 +3,13 @@ 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
|
||||
from utilities.forms import (
|
||||
APISelect, BootstrapMixin, ChainedFieldsMixin, ChainedModelChoiceField, CommentField, FilterChoiceField,
|
||||
SmallTextarea, SlugField,
|
||||
APISelect, BootstrapMixin, BulkImportForm, ChainedFieldsMixin, ChainedModelChoiceField, CommentField, CSVDataField,
|
||||
FilterChoiceField, Livesearch, SmallTextarea, SlugField,
|
||||
)
|
||||
|
||||
from .models import Circuit, CircuitTermination, CircuitType, Provider
|
||||
@@ -39,18 +39,15 @@ class ProviderForm(BootstrapMixin, CustomFieldForm):
|
||||
}
|
||||
|
||||
|
||||
class ProviderCSVForm(forms.ModelForm):
|
||||
slug = SlugField()
|
||||
class ProviderFromCSVForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = Provider
|
||||
fields = ['name', 'slug', 'asn', 'account', 'portal_url', 'comments']
|
||||
help_texts = {
|
||||
'name': 'Provider name',
|
||||
'asn': '32-bit autonomous system number',
|
||||
'portal_url': 'Portal URL',
|
||||
'comments': 'Free-form comments',
|
||||
}
|
||||
fields = ['name', 'slug', 'asn', 'account', 'portal_url']
|
||||
|
||||
|
||||
class ProviderImportForm(BootstrapMixin, BulkImportForm):
|
||||
csv = CSVDataField(csv_form=ProviderFromCSVForm)
|
||||
|
||||
|
||||
class ProviderBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
@@ -105,36 +102,21 @@ class CircuitForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
||||
}
|
||||
|
||||
|
||||
class CircuitCSVForm(forms.ModelForm):
|
||||
provider = forms.ModelChoiceField(
|
||||
queryset=Provider.objects.all(),
|
||||
to_field_name='name',
|
||||
help_text='Name of parent provider',
|
||||
error_messages={
|
||||
'invalid_choice': 'Provider not found.'
|
||||
}
|
||||
)
|
||||
type = forms.ModelChoiceField(
|
||||
queryset=CircuitType.objects.all(),
|
||||
to_field_name='name',
|
||||
help_text='Type of circuit',
|
||||
error_messages={
|
||||
'invalid_choice': 'Invalid circuit type.'
|
||||
}
|
||||
)
|
||||
tenant = forms.ModelChoiceField(
|
||||
queryset=Tenant.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text='Name of assigned tenant',
|
||||
error_messages={
|
||||
'invalid_choice': 'Tenant not found.'
|
||||
}
|
||||
)
|
||||
class CircuitFromCSVForm(forms.ModelForm):
|
||||
provider = forms.ModelChoiceField(Provider.objects.all(), to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Provider not found.'})
|
||||
type = forms.ModelChoiceField(CircuitType.objects.all(), to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Invalid circuit type.'})
|
||||
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
|
||||
error_messages={'invalid_choice': 'Tenant not found.'})
|
||||
|
||||
class Meta:
|
||||
model = Circuit
|
||||
fields = ['cid', 'provider', 'type', 'tenant', 'install_date', 'commit_rate', 'description', 'comments']
|
||||
fields = ['cid', 'provider', 'type', 'tenant', 'install_date', 'commit_rate', 'description']
|
||||
|
||||
|
||||
class CircuitImportForm(BootstrapMixin, BulkImportForm):
|
||||
csv = CSVDataField(csv_form=CircuitFromCSVForm)
|
||||
|
||||
|
||||
class CircuitBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
@@ -210,7 +192,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 +234,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
|
||||
]
|
||||
|
||||
@@ -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):
|
||||
@@ -45,8 +52,6 @@ class Provider(CreatedUpdatedModel, CustomFieldModel):
|
||||
comments = models.TextField(blank=True)
|
||||
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
|
||||
|
||||
csv_headers = ['name', 'slug', 'asn', 'account', 'portal_url']
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
|
||||
@@ -102,8 +107,6 @@ class Circuit(CreatedUpdatedModel, CustomFieldModel):
|
||||
comments = models.TextField(blank=True)
|
||||
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
|
||||
|
||||
csv_headers = ['cid', 'provider', 'type', 'tenant', 'install_date', 'commit_rate', 'description']
|
||||
|
||||
class Meta:
|
||||
ordering = ['provider', 'cid']
|
||||
unique_together = ['provider', 'cid']
|
||||
|
||||
@@ -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'),
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -69,8 +65,9 @@ class ProviderDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
|
||||
class ProviderBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
permission_required = 'circuits.add_provider'
|
||||
model_form = forms.ProviderCSVForm
|
||||
form = forms.ProviderImportForm
|
||||
table = tables.ProviderTable
|
||||
template_name = 'circuits/provider_import.html'
|
||||
default_return_url = 'circuits:provider_list'
|
||||
|
||||
|
||||
@@ -100,8 +97,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 +106,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 +147,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
|
||||
@@ -174,8 +163,9 @@ class CircuitDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
|
||||
class CircuitBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
permission_required = 'circuits.add_circuit'
|
||||
model_form = forms.CircuitCSVForm
|
||||
form = forms.CircuitImportForm
|
||||
table = tables.CircuitTable
|
||||
template_name = 'circuits/circuit_import.html'
|
||||
default_return_url = 'circuits:circuit_list'
|
||||
|
||||
|
||||
@@ -244,8 +234,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 +249,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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
#
|
||||
|
||||
@@ -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)'],
|
||||
]
|
||||
@@ -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):
|
||||
|
||||
@@ -5,6 +5,7 @@ import re
|
||||
|
||||
from django import forms
|
||||
from django.contrib.postgres.forms.array import SimpleArrayField
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db.models import Count, Q
|
||||
|
||||
from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
|
||||
@@ -13,18 +14,18 @@ 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,
|
||||
BulkImportForm, ChainedFieldsMixin, ChainedModelChoiceField, CommentField, CSVDataField, ExpandableNameField,
|
||||
FilterChoiceField, FlexibleModelChoiceField, Livesearch, SelectWithDisabled, SmallTextarea, SlugField,
|
||||
FilterTreeNodeMultipleChoiceField,
|
||||
)
|
||||
from .formfields import MACAddressFormField
|
||||
from .models import (
|
||||
DeviceBay, DeviceBayTemplate, CONNECTION_STATUS_CHOICES, CONNECTION_STATUS_CONNECTED, ConsolePort,
|
||||
ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceRole, DeviceType, Interface,
|
||||
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,
|
||||
DeviceBay, DeviceBayTemplate, CONNECTION_STATUS_CHOICES, CONNECTION_STATUS_PLANNED, CONNECTION_STATUS_CONNECTED,
|
||||
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceRole, DeviceType,
|
||||
Interface, IFACE_FF_CHOICES, IFACE_FF_LAG, IFACE_ORDERING_CHOICES, InterfaceConnection, InterfaceTemplate,
|
||||
Manufacturer, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate,
|
||||
RACK_TYPE_CHOICES, RACK_WIDTH_CHOICES, Rack, RackGroup, RackReservation, RackRole, Region, Site, STATUS_CHOICES,
|
||||
SUBDEVICE_ROLE_CHILD, SUBDEVICE_ROLE_PARENT, VIRTUAL_IFACE_TYPES,
|
||||
)
|
||||
|
||||
|
||||
@@ -49,6 +50,14 @@ def get_device_by_name_or_pk(name):
|
||||
return device
|
||||
|
||||
|
||||
def validate_connection_status(value):
|
||||
"""
|
||||
Custom validator for connection statuses. value must be either "planned" or "connected" (case-insensitive).
|
||||
"""
|
||||
if value.lower() not in ['planned', 'connected']:
|
||||
raise ValidationError('Invalid connection status ({}); must be either "planned" or "connected".'.format(value))
|
||||
|
||||
|
||||
class DeviceComponentForm(BootstrapMixin, forms.Form):
|
||||
"""
|
||||
Allow inclusion of the parent device as context for limiting field choices.
|
||||
@@ -98,37 +107,27 @@ class SiteForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
||||
}
|
||||
|
||||
|
||||
class SiteCSVForm(forms.ModelForm):
|
||||
class SiteFromCSVForm(forms.ModelForm):
|
||||
region = forms.ModelChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text='Name of assigned region',
|
||||
error_messages={
|
||||
'invalid_choice': 'Region not found.',
|
||||
Region.objects.all(), to_field_name='name', required=False, error_messages={
|
||||
'invalid_choice': 'Tenant not found.'
|
||||
}
|
||||
)
|
||||
tenant = forms.ModelChoiceField(
|
||||
queryset=Tenant.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text='Name of assigned tenant',
|
||||
error_messages={
|
||||
'invalid_choice': 'Tenant not found.',
|
||||
Tenant.objects.all(), to_field_name='name', required=False, error_messages={
|
||||
'invalid_choice': 'Tenant not found.'
|
||||
}
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Site
|
||||
fields = [
|
||||
'name', 'slug', 'region', 'tenant', 'facility', 'asn', 'physical_address', 'shipping_address',
|
||||
'contact_name', 'contact_phone', 'contact_email', 'comments',
|
||||
'name', 'slug', 'region', 'tenant', 'facility', 'asn', 'contact_name', 'contact_phone', 'contact_email',
|
||||
]
|
||||
help_texts = {
|
||||
'name': 'Site name',
|
||||
'slug': 'URL-friendly slug',
|
||||
'asn': '32-bit autonomous system number',
|
||||
}
|
||||
|
||||
|
||||
class SiteImportForm(BootstrapMixin, BulkImportForm):
|
||||
csv = CSVDataField(csv_form=SiteFromCSVForm)
|
||||
|
||||
|
||||
class SiteBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
@@ -218,73 +217,49 @@ class RackForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
||||
}
|
||||
|
||||
|
||||
class RackCSVForm(forms.ModelForm):
|
||||
site = forms.ModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='name',
|
||||
help_text='Name of parent site',
|
||||
error_messages={
|
||||
'invalid_choice': 'Site not found.',
|
||||
}
|
||||
)
|
||||
group_name = forms.CharField(
|
||||
help_text='Name of rack group',
|
||||
required=False
|
||||
)
|
||||
tenant = forms.ModelChoiceField(
|
||||
queryset=Tenant.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text='Name of assigned tenant',
|
||||
error_messages={
|
||||
'invalid_choice': 'Tenant not found.',
|
||||
}
|
||||
)
|
||||
role = forms.ModelChoiceField(
|
||||
queryset=RackRole.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text='Name of assigned role',
|
||||
error_messages={
|
||||
'invalid_choice': 'Role not found.',
|
||||
}
|
||||
)
|
||||
type = CSVChoiceField(
|
||||
choices=RACK_TYPE_CHOICES,
|
||||
required=False,
|
||||
help_text='Rack type'
|
||||
)
|
||||
width = forms.ChoiceField(
|
||||
choices=(
|
||||
(RACK_WIDTH_19IN, '19'),
|
||||
(RACK_WIDTH_23IN, '23'),
|
||||
),
|
||||
help_text='Rail-to-rail width (in inches)'
|
||||
)
|
||||
class RackFromCSVForm(forms.ModelForm):
|
||||
site = forms.ModelChoiceField(queryset=Site.objects.all(), to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Site not found.'})
|
||||
group_name = forms.CharField(required=False)
|
||||
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
|
||||
error_messages={'invalid_choice': 'Tenant not found.'})
|
||||
role = forms.ModelChoiceField(RackRole.objects.all(), to_field_name='name', required=False,
|
||||
error_messages={'invalid_choice': 'Role not found.'})
|
||||
type = forms.CharField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = Rack
|
||||
fields = [
|
||||
'site', 'group_name', 'name', 'facility_id', 'tenant', 'role', 'type', 'width', 'u_height', 'desc_units',
|
||||
]
|
||||
help_texts = {
|
||||
'name': 'Rack name',
|
||||
'u_height': 'Height in rack units',
|
||||
}
|
||||
fields = ['site', 'group_name', 'name', 'facility_id', 'tenant', 'role', 'type', 'width', 'u_height',
|
||||
'desc_units']
|
||||
|
||||
def clean(self):
|
||||
|
||||
super(RackCSVForm, self).clean()
|
||||
|
||||
site = self.cleaned_data.get('site')
|
||||
group_name = self.cleaned_data.get('group_name')
|
||||
group = self.cleaned_data.get('group_name')
|
||||
|
||||
# Validate rack group
|
||||
if group_name:
|
||||
if site and group:
|
||||
try:
|
||||
self.instance.group = RackGroup.objects.get(site=site, name=group_name)
|
||||
self.instance.group = RackGroup.objects.get(site=site, name=group)
|
||||
except RackGroup.DoesNotExist:
|
||||
raise forms.ValidationError("Rack group {} not found for site {}".format(group_name, site))
|
||||
self.add_error('group_name', "Invalid rack group ({})".format(group))
|
||||
|
||||
def clean_type(self):
|
||||
rack_type = self.cleaned_data['type']
|
||||
if not rack_type:
|
||||
return None
|
||||
try:
|
||||
choices = {v.lower(): k for k, v in RACK_TYPE_CHOICES}
|
||||
return choices[rack_type.lower()]
|
||||
except KeyError:
|
||||
raise forms.ValidationError('Invalid rack type ({}). Valid choices are: {}.'.format(
|
||||
rack_type,
|
||||
', '.join({v: k for k, v in RACK_TYPE_CHOICES}),
|
||||
))
|
||||
|
||||
|
||||
class RackImportForm(BootstrapMixin, BulkImportForm):
|
||||
csv = CSVDataField(csv_form=RackFromCSVForm)
|
||||
|
||||
|
||||
class RackBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
@@ -688,60 +663,32 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
||||
self.initial['rack'] = self.instance.parent_bay.device.rack_id
|
||||
|
||||
|
||||
class BaseDeviceCSVForm(forms.ModelForm):
|
||||
class BaseDeviceFromCSVForm(forms.ModelForm):
|
||||
device_role = forms.ModelChoiceField(
|
||||
queryset=DeviceRole.objects.all(),
|
||||
to_field_name='name',
|
||||
help_text='Name of assigned role',
|
||||
error_messages={
|
||||
'invalid_choice': 'Invalid device role.',
|
||||
}
|
||||
queryset=DeviceRole.objects.all(), to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Invalid device role.'}
|
||||
)
|
||||
tenant = forms.ModelChoiceField(
|
||||
queryset=Tenant.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text='Name of assigned tenant',
|
||||
error_messages={
|
||||
'invalid_choice': 'Tenant not found.',
|
||||
}
|
||||
Tenant.objects.all(), to_field_name='name', required=False,
|
||||
error_messages={'invalid_choice': 'Tenant not found.'}
|
||||
)
|
||||
manufacturer = forms.ModelChoiceField(
|
||||
queryset=Manufacturer.objects.all(),
|
||||
to_field_name='name',
|
||||
help_text='Device type manufacturer',
|
||||
error_messages={
|
||||
'invalid_choice': 'Invalid manufacturer.',
|
||||
}
|
||||
)
|
||||
model_name = forms.CharField(
|
||||
help_text='Device type model name'
|
||||
queryset=Manufacturer.objects.all(), to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Invalid manufacturer.'}
|
||||
)
|
||||
model_name = forms.CharField()
|
||||
platform = forms.ModelChoiceField(
|
||||
queryset=Platform.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text='Name of assigned platform',
|
||||
error_messages={
|
||||
'invalid_choice': 'Invalid platform.',
|
||||
}
|
||||
)
|
||||
status = CSVChoiceField(
|
||||
choices=STATUS_CHOICES,
|
||||
help_text='Operational status of device'
|
||||
queryset=Platform.objects.all(), required=False, to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Invalid platform.'}
|
||||
)
|
||||
status = forms.CharField()
|
||||
|
||||
class Meta:
|
||||
fields = []
|
||||
model = Device
|
||||
help_texts = {
|
||||
'name': 'Device name',
|
||||
}
|
||||
|
||||
def clean(self):
|
||||
|
||||
super(BaseDeviceCSVForm, self).clean()
|
||||
|
||||
manufacturer = self.cleaned_data.get('manufacturer')
|
||||
model_name = self.cleaned_data.get('model_name')
|
||||
|
||||
@@ -750,73 +697,70 @@ class BaseDeviceCSVForm(forms.ModelForm):
|
||||
try:
|
||||
self.instance.device_type = DeviceType.objects.get(manufacturer=manufacturer, model=model_name)
|
||||
except DeviceType.DoesNotExist:
|
||||
raise forms.ValidationError("Device type {} {} not found".format(manufacturer, model_name))
|
||||
self.add_error('model_name', "Invalid device type ({} {})".format(manufacturer, model_name))
|
||||
|
||||
def clean_status(self):
|
||||
status_choices = {s[1].lower(): s[0] for s in STATUS_CHOICES}
|
||||
try:
|
||||
return status_choices[self.cleaned_data['status'].lower()]
|
||||
except KeyError:
|
||||
raise ValidationError("Invalid status: {}".format(self.cleaned_data['status']))
|
||||
|
||||
|
||||
class DeviceCSVForm(BaseDeviceCSVForm):
|
||||
class DeviceFromCSVForm(BaseDeviceFromCSVForm):
|
||||
site = forms.ModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='name',
|
||||
help_text='Name of parent site',
|
||||
error_messages={
|
||||
queryset=Site.objects.all(), to_field_name='name', error_messages={
|
||||
'invalid_choice': 'Invalid site name.',
|
||||
}
|
||||
)
|
||||
rack_group = forms.CharField(
|
||||
required=False,
|
||||
help_text='Parent rack\'s group (if any)'
|
||||
)
|
||||
rack_name = forms.CharField(
|
||||
required=False,
|
||||
help_text='Name of parent rack'
|
||||
)
|
||||
face = CSVChoiceField(
|
||||
choices=RACK_FACE_CHOICES,
|
||||
required=False,
|
||||
help_text='Mounted rack face'
|
||||
)
|
||||
rack_name = forms.CharField(required=False)
|
||||
face = forms.CharField(required=False)
|
||||
|
||||
class Meta(BaseDeviceCSVForm.Meta):
|
||||
class Meta(BaseDeviceFromCSVForm.Meta):
|
||||
fields = [
|
||||
'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag', 'status',
|
||||
'site', 'rack_group', 'rack_name', 'position', 'face',
|
||||
'site', 'rack_name', 'position', 'face',
|
||||
]
|
||||
|
||||
def clean(self):
|
||||
|
||||
super(DeviceCSVForm, self).clean()
|
||||
super(DeviceFromCSVForm, self).clean()
|
||||
|
||||
site = self.cleaned_data.get('site')
|
||||
rack_group = self.cleaned_data.get('rack_group')
|
||||
rack_name = self.cleaned_data.get('rack_name')
|
||||
|
||||
# Validate rack
|
||||
if site and rack_group and rack_name:
|
||||
if site and rack_name:
|
||||
try:
|
||||
self.instance.rack = Rack.objects.get(site=site, group__name=rack_group, name=rack_name)
|
||||
self.instance.rack = Rack.objects.get(site=site, name=rack_name)
|
||||
except Rack.DoesNotExist:
|
||||
raise forms.ValidationError("Rack {} not found in site {} group {}".format(rack_name, site, rack_group))
|
||||
elif site and rack_name:
|
||||
try:
|
||||
self.instance.rack = Rack.objects.get(site=site, group__isnull=True, name=rack_name)
|
||||
except Rack.DoesNotExist:
|
||||
raise forms.ValidationError("Rack {} not found in site {} (no group)".format(rack_name, site))
|
||||
self.add_error('rack_name', "Invalid rack ({})".format(rack_name))
|
||||
|
||||
def clean_face(self):
|
||||
face = self.cleaned_data['face']
|
||||
if not face:
|
||||
return None
|
||||
try:
|
||||
return {
|
||||
'front': 0,
|
||||
'rear': 1,
|
||||
}[face.lower()]
|
||||
except KeyError:
|
||||
raise forms.ValidationError('Invalid rack face ({}); must be "front" or "rear".'.format(face))
|
||||
|
||||
|
||||
class ChildDeviceCSVForm(BaseDeviceCSVForm):
|
||||
class ChildDeviceFromCSVForm(BaseDeviceFromCSVForm):
|
||||
parent = FlexibleModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
to_field_name='name',
|
||||
help_text='Name or ID of parent device',
|
||||
required=False,
|
||||
error_messages={
|
||||
'invalid_choice': 'Parent device not found.',
|
||||
'invalid_choice': 'Parent device not found.'
|
||||
}
|
||||
)
|
||||
device_bay_name = forms.CharField(
|
||||
help_text='Name of device bay',
|
||||
)
|
||||
device_bay_name = forms.CharField(required=False)
|
||||
|
||||
class Meta(BaseDeviceCSVForm.Meta):
|
||||
class Meta(BaseDeviceFromCSVForm.Meta):
|
||||
fields = [
|
||||
'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag', 'status',
|
||||
'parent', 'device_bay_name',
|
||||
@@ -824,7 +768,7 @@ class ChildDeviceCSVForm(BaseDeviceCSVForm):
|
||||
|
||||
def clean(self):
|
||||
|
||||
super(ChildDeviceCSVForm, self).clean()
|
||||
super(ChildDeviceFromCSVForm, self).clean()
|
||||
|
||||
parent = self.cleaned_data.get('parent')
|
||||
device_bay_name = self.cleaned_data.get('device_bay_name')
|
||||
@@ -832,12 +776,22 @@ class ChildDeviceCSVForm(BaseDeviceCSVForm):
|
||||
# Validate device bay
|
||||
if parent and device_bay_name:
|
||||
try:
|
||||
self.instance.parent_bay = DeviceBay.objects.get(device=parent, name=device_bay_name)
|
||||
# Inherit site and rack from parent device
|
||||
self.instance.site = parent.site
|
||||
self.instance.rack = parent.rack
|
||||
device_bay = DeviceBay.objects.get(device=parent, name=device_bay_name)
|
||||
if device_bay.installed_device:
|
||||
self.add_error('device_bay_name',
|
||||
"Device bay ({} {}) is already occupied".format(parent, device_bay_name))
|
||||
else:
|
||||
self.instance.parent_bay = device_bay
|
||||
except DeviceBay.DoesNotExist:
|
||||
raise forms.ValidationError("Parent device/bay ({} {}) not found".format(parent, device_bay_name))
|
||||
self.add_error('device_bay_name', "Parent device/bay ({} {}) not found".format(parent, device_bay_name))
|
||||
|
||||
|
||||
class DeviceImportForm(BootstrapMixin, BulkImportForm):
|
||||
csv = CSVDataField(csv_form=DeviceFromCSVForm)
|
||||
|
||||
|
||||
class ChildDeviceImportForm(BootstrapMixin, BulkImportForm):
|
||||
csv = CSVDataField(csv_form=ChildDeviceFromCSVForm)
|
||||
|
||||
|
||||
class DeviceBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
@@ -935,84 +889,75 @@ class ConsolePortCreateForm(DeviceComponentForm):
|
||||
name_pattern = ExpandableNameField(label='Name')
|
||||
|
||||
|
||||
class ConsoleConnectionCSVForm(forms.ModelForm):
|
||||
class ConsoleConnectionCSVForm(forms.Form):
|
||||
console_server = FlexibleModelChoiceField(
|
||||
queryset=Device.objects.filter(device_type__is_console_server=True),
|
||||
to_field_name='name',
|
||||
help_text='Console server name or ID',
|
||||
error_messages={
|
||||
'invalid_choice': 'Console server not found',
|
||||
}
|
||||
)
|
||||
cs_port = forms.CharField(
|
||||
help_text='Console server port name'
|
||||
)
|
||||
device = FlexibleModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
to_field_name='name',
|
||||
help_text='Device name or ID',
|
||||
error_messages={
|
||||
'invalid_choice': 'Device not found',
|
||||
}
|
||||
)
|
||||
console_port = forms.CharField(
|
||||
help_text='Console port name'
|
||||
)
|
||||
connection_status = CSVChoiceField(
|
||||
choices=CONNECTION_STATUS_CHOICES,
|
||||
help_text='Connection status'
|
||||
)
|
||||
cs_port = forms.CharField()
|
||||
device = FlexibleModelChoiceField(queryset=Device.objects.all(), to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Device not found'})
|
||||
console_port = forms.CharField()
|
||||
status = forms.CharField(validators=[validate_connection_status])
|
||||
|
||||
class Meta:
|
||||
model = ConsolePort
|
||||
fields = ['console_server', 'cs_port', 'device', 'console_port', 'connection_status']
|
||||
def clean(self):
|
||||
|
||||
def clean_console_port(self):
|
||||
# Validate console server port
|
||||
if self.cleaned_data.get('console_server'):
|
||||
try:
|
||||
cs_port = ConsoleServerPort.objects.get(device=self.cleaned_data['console_server'],
|
||||
name=self.cleaned_data['cs_port'])
|
||||
if ConsolePort.objects.filter(cs_port=cs_port):
|
||||
raise forms.ValidationError("Console server port is already occupied (by {} {})"
|
||||
.format(cs_port.connected_console.device, cs_port.connected_console))
|
||||
except ConsoleServerPort.DoesNotExist:
|
||||
raise forms.ValidationError("Invalid console server port ({} {})"
|
||||
.format(self.cleaned_data['console_server'], self.cleaned_data['cs_port']))
|
||||
|
||||
console_port_name = self.cleaned_data.get('console_port')
|
||||
if not self.cleaned_data.get('device') or not console_port_name:
|
||||
return None
|
||||
# Validate console port
|
||||
if self.cleaned_data.get('device'):
|
||||
try:
|
||||
console_port = ConsolePort.objects.get(device=self.cleaned_data['device'],
|
||||
name=self.cleaned_data['console_port'])
|
||||
if console_port.cs_port:
|
||||
raise forms.ValidationError("Console port is already connected (to {} {})"
|
||||
.format(console_port.cs_port.device, console_port.cs_port))
|
||||
except ConsolePort.DoesNotExist:
|
||||
raise forms.ValidationError("Invalid console port ({} {})"
|
||||
.format(self.cleaned_data['device'], self.cleaned_data['console_port']))
|
||||
|
||||
try:
|
||||
# Retrieve console port by name
|
||||
consoleport = ConsolePort.objects.get(
|
||||
device=self.cleaned_data['device'], name=console_port_name
|
||||
)
|
||||
# Check if the console port is already connected
|
||||
if consoleport.cs_port is not None:
|
||||
raise forms.ValidationError("{} {} is already connected".format(
|
||||
self.cleaned_data['device'], console_port_name
|
||||
))
|
||||
except ConsolePort.DoesNotExist:
|
||||
raise forms.ValidationError("Invalid console port ({} {})".format(
|
||||
self.cleaned_data['device'], console_port_name
|
||||
))
|
||||
|
||||
self.instance = consoleport
|
||||
return consoleport
|
||||
class ConsoleConnectionImportForm(BootstrapMixin, BulkImportForm):
|
||||
csv = CSVDataField(csv_form=ConsoleConnectionCSVForm)
|
||||
|
||||
def clean_cs_port(self):
|
||||
def clean(self):
|
||||
records = self.cleaned_data.get('csv')
|
||||
if not records:
|
||||
return
|
||||
|
||||
cs_port_name = self.cleaned_data.get('cs_port')
|
||||
if not self.cleaned_data.get('console_server') or not cs_port_name:
|
||||
return None
|
||||
connection_list = []
|
||||
|
||||
try:
|
||||
# Retrieve console server port by name
|
||||
cs_port = ConsoleServerPort.objects.get(
|
||||
device=self.cleaned_data['console_server'], name=cs_port_name
|
||||
)
|
||||
# Check if the console server port is already connected
|
||||
if ConsolePort.objects.filter(cs_port=cs_port).count():
|
||||
raise forms.ValidationError("{} {} is already connected".format(
|
||||
self.cleaned_data['console_server'], cs_port_name
|
||||
))
|
||||
except ConsoleServerPort.DoesNotExist:
|
||||
raise forms.ValidationError("Invalid console server port ({} {})".format(
|
||||
self.cleaned_data['console_server'], cs_port_name
|
||||
))
|
||||
for i, record in enumerate(records, start=1):
|
||||
form = self.fields['csv'].csv_form(data=record)
|
||||
if form.is_valid():
|
||||
console_port = ConsolePort.objects.get(device=form.cleaned_data['device'],
|
||||
name=form.cleaned_data['console_port'])
|
||||
console_port.cs_port = ConsoleServerPort.objects.get(device=form.cleaned_data['console_server'],
|
||||
name=form.cleaned_data['cs_port'])
|
||||
if form.cleaned_data['status'] == 'planned':
|
||||
console_port.connection_status = CONNECTION_STATUS_PLANNED
|
||||
else:
|
||||
console_port.connection_status = CONNECTION_STATUS_CONNECTED
|
||||
connection_list.append(console_port)
|
||||
else:
|
||||
for field, errors in form.errors.items():
|
||||
for e in errors:
|
||||
self.add_error('csv', "Record {} {}: {}".format(i, field, e))
|
||||
|
||||
return cs_port
|
||||
self.cleaned_data['csv'] = connection_list
|
||||
|
||||
|
||||
class ConsolePortConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm):
|
||||
@@ -1174,10 +1119,6 @@ class ConsoleServerPortConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.
|
||||
}
|
||||
|
||||
|
||||
class ConsoleServerPortBulkDisconnectForm(ConfirmationForm):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=ConsoleServerPort.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
|
||||
|
||||
#
|
||||
# Power ports
|
||||
#
|
||||
@@ -1196,84 +1137,76 @@ class PowerPortCreateForm(DeviceComponentForm):
|
||||
name_pattern = ExpandableNameField(label='Name')
|
||||
|
||||
|
||||
class PowerConnectionCSVForm(forms.ModelForm):
|
||||
class PowerConnectionCSVForm(forms.Form):
|
||||
pdu = FlexibleModelChoiceField(
|
||||
queryset=Device.objects.filter(device_type__is_pdu=True),
|
||||
to_field_name='name',
|
||||
help_text='PDU name or ID',
|
||||
error_messages={
|
||||
'invalid_choice': 'PDU not found.',
|
||||
}
|
||||
)
|
||||
power_outlet = forms.CharField(
|
||||
help_text='Power outlet name'
|
||||
)
|
||||
device = FlexibleModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
to_field_name='name',
|
||||
help_text='Device name or ID',
|
||||
error_messages={
|
||||
'invalid_choice': 'Device not found',
|
||||
}
|
||||
)
|
||||
power_port = forms.CharField(
|
||||
help_text='Power port name'
|
||||
)
|
||||
connection_status = CSVChoiceField(
|
||||
choices=CONNECTION_STATUS_CHOICES,
|
||||
help_text='Connection status'
|
||||
)
|
||||
power_outlet = forms.CharField()
|
||||
device = FlexibleModelChoiceField(queryset=Device.objects.all(), to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Device not found'})
|
||||
power_port = forms.CharField()
|
||||
status = forms.CharField(validators=[validate_connection_status])
|
||||
|
||||
class Meta:
|
||||
model = PowerPort
|
||||
fields = ['pdu', 'power_outlet', 'device', 'power_port', 'connection_status']
|
||||
def clean(self):
|
||||
|
||||
def clean_power_port(self):
|
||||
# Validate power outlet
|
||||
if self.cleaned_data.get('pdu'):
|
||||
try:
|
||||
power_outlet = PowerOutlet.objects.get(device=self.cleaned_data['pdu'],
|
||||
name=self.cleaned_data['power_outlet'])
|
||||
if PowerPort.objects.filter(power_outlet=power_outlet):
|
||||
raise forms.ValidationError("Power outlet is already occupied (by {} {})"
|
||||
.format(power_outlet.connected_port.device,
|
||||
power_outlet.connected_port))
|
||||
except PowerOutlet.DoesNotExist:
|
||||
raise forms.ValidationError("Invalid PDU port ({} {})"
|
||||
.format(self.cleaned_data['pdu'], self.cleaned_data['power_outlet']))
|
||||
|
||||
power_port_name = self.cleaned_data.get('power_port')
|
||||
if not self.cleaned_data.get('device') or not power_port_name:
|
||||
return None
|
||||
# Validate power port
|
||||
if self.cleaned_data.get('device'):
|
||||
try:
|
||||
power_port = PowerPort.objects.get(device=self.cleaned_data['device'],
|
||||
name=self.cleaned_data['power_port'])
|
||||
if power_port.power_outlet:
|
||||
raise forms.ValidationError("Power port is already connected (to {} {})"
|
||||
.format(power_port.power_outlet.device, power_port.power_outlet))
|
||||
except PowerPort.DoesNotExist:
|
||||
raise forms.ValidationError("Invalid power port ({} {})"
|
||||
.format(self.cleaned_data['device'], self.cleaned_data['power_port']))
|
||||
|
||||
try:
|
||||
# Retrieve power port by name
|
||||
powerport = PowerPort.objects.get(
|
||||
device=self.cleaned_data['device'], name=power_port_name
|
||||
)
|
||||
# Check if the power port is already connected
|
||||
if powerport.power_outlet is not None:
|
||||
raise forms.ValidationError("{} {} is already connected".format(
|
||||
self.cleaned_data['device'], power_port_name
|
||||
))
|
||||
except PowerPort.DoesNotExist:
|
||||
raise forms.ValidationError("Invalid power port ({} {})".format(
|
||||
self.cleaned_data['device'], power_port_name
|
||||
))
|
||||
|
||||
self.instance = powerport
|
||||
return powerport
|
||||
class PowerConnectionImportForm(BootstrapMixin, BulkImportForm):
|
||||
csv = CSVDataField(csv_form=PowerConnectionCSVForm)
|
||||
|
||||
def clean_power_outlet(self):
|
||||
def clean(self):
|
||||
records = self.cleaned_data.get('csv')
|
||||
if not records:
|
||||
return
|
||||
|
||||
power_outlet_name = self.cleaned_data.get('power_outlet')
|
||||
if not self.cleaned_data.get('pdu') or not power_outlet_name:
|
||||
return None
|
||||
connection_list = []
|
||||
|
||||
try:
|
||||
# Retrieve power outlet by name
|
||||
power_outlet = PowerOutlet.objects.get(
|
||||
device=self.cleaned_data['pdu'], name=power_outlet_name
|
||||
)
|
||||
# Check if the power outlet is already connected
|
||||
if PowerPort.objects.filter(power_outlet=power_outlet).count():
|
||||
raise forms.ValidationError("{} {} is already connected".format(
|
||||
self.cleaned_data['pdu'], power_outlet_name
|
||||
))
|
||||
except PowerOutlet.DoesNotExist:
|
||||
raise forms.ValidationError("Invalid power outlet ({} {})".format(
|
||||
self.cleaned_data['pdu'], power_outlet_name
|
||||
))
|
||||
for i, record in enumerate(records, start=1):
|
||||
form = self.fields['csv'].csv_form(data=record)
|
||||
if form.is_valid():
|
||||
power_port = PowerPort.objects.get(device=form.cleaned_data['device'],
|
||||
name=form.cleaned_data['power_port'])
|
||||
power_port.power_outlet = PowerOutlet.objects.get(device=form.cleaned_data['pdu'],
|
||||
name=form.cleaned_data['power_outlet'])
|
||||
if form.cleaned_data['status'] == 'planned':
|
||||
power_port.connection_status = CONNECTION_STATUS_PLANNED
|
||||
else:
|
||||
power_port.connection_status = CONNECTION_STATUS_CONNECTED
|
||||
connection_list.append(power_port)
|
||||
else:
|
||||
for field, errors in form.errors.items():
|
||||
for e in errors:
|
||||
self.add_error('csv', "Record {} {}: {}".format(i, field, e))
|
||||
|
||||
return power_outlet
|
||||
self.cleaned_data['csv'] = connection_list
|
||||
|
||||
|
||||
class PowerPortConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm):
|
||||
@@ -1435,10 +1368,6 @@ class PowerOutletConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.Form):
|
||||
}
|
||||
|
||||
|
||||
class PowerOutletBulkDisconnectForm(ConfirmationForm):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=PowerOutlet.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
|
||||
|
||||
#
|
||||
# Interfaces
|
||||
#
|
||||
@@ -1447,7 +1376,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 +1398,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 +1418,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 +1445,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 +1499,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 +1508,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 +1521,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,85 +1531,99 @@ 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):
|
||||
class InterfaceConnectionCSVForm(forms.Form):
|
||||
device_a = FlexibleModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
to_field_name='name',
|
||||
help_text='Name or ID of device A',
|
||||
error_messages={'invalid_choice': 'Device A not found.'}
|
||||
)
|
||||
interface_a = forms.CharField(
|
||||
help_text='Name of interface A'
|
||||
)
|
||||
interface_a = forms.CharField()
|
||||
device_b = FlexibleModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
to_field_name='name',
|
||||
help_text='Name or ID of device B',
|
||||
error_messages={'invalid_choice': 'Device B not found.'}
|
||||
)
|
||||
interface_b = forms.CharField(
|
||||
help_text='Name of interface B'
|
||||
)
|
||||
connection_status = CSVChoiceField(
|
||||
choices=CONNECTION_STATUS_CHOICES,
|
||||
help_text='Connection status'
|
||||
interface_b = forms.CharField()
|
||||
status = forms.CharField(
|
||||
validators=[validate_connection_status]
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = InterfaceConnection
|
||||
fields = ['device_a', 'interface_a', 'device_b', 'interface_b', 'connection_status']
|
||||
def clean(self):
|
||||
|
||||
def clean_interface_a(self):
|
||||
# Validate interface A
|
||||
if self.cleaned_data.get('device_a'):
|
||||
try:
|
||||
interface_a = Interface.objects.get(device=self.cleaned_data['device_a'],
|
||||
name=self.cleaned_data['interface_a'])
|
||||
except Interface.DoesNotExist:
|
||||
raise forms.ValidationError("Invalid interface ({} {})"
|
||||
.format(self.cleaned_data['device_a'], self.cleaned_data['interface_a']))
|
||||
try:
|
||||
InterfaceConnection.objects.get(Q(interface_a=interface_a) | Q(interface_b=interface_a))
|
||||
raise forms.ValidationError("{} {} is already connected"
|
||||
.format(self.cleaned_data['device_a'], self.cleaned_data['interface_a']))
|
||||
except InterfaceConnection.DoesNotExist:
|
||||
pass
|
||||
|
||||
interface_name = self.cleaned_data.get('interface_a')
|
||||
if not interface_name:
|
||||
return None
|
||||
# Validate interface B
|
||||
if self.cleaned_data.get('device_b'):
|
||||
try:
|
||||
interface_b = Interface.objects.get(device=self.cleaned_data['device_b'],
|
||||
name=self.cleaned_data['interface_b'])
|
||||
except Interface.DoesNotExist:
|
||||
raise forms.ValidationError("Invalid interface ({} {})"
|
||||
.format(self.cleaned_data['device_b'], self.cleaned_data['interface_b']))
|
||||
try:
|
||||
InterfaceConnection.objects.get(Q(interface_a=interface_b) | Q(interface_b=interface_b))
|
||||
raise forms.ValidationError("{} {} is already connected"
|
||||
.format(self.cleaned_data['device_b'], self.cleaned_data['interface_b']))
|
||||
except InterfaceConnection.DoesNotExist:
|
||||
pass
|
||||
|
||||
try:
|
||||
# Retrieve interface by name
|
||||
interface = Interface.objects.get(
|
||||
device=self.cleaned_data['device_a'], name=interface_name
|
||||
)
|
||||
# Check for an existing connection to this interface
|
||||
if InterfaceConnection.objects.filter(Q(interface_a=interface) | Q(interface_b=interface)).count():
|
||||
raise forms.ValidationError("{} {} is already connected".format(
|
||||
self.cleaned_data['device_a'], interface_name
|
||||
))
|
||||
except Interface.DoesNotExist:
|
||||
raise forms.ValidationError("Invalid interface ({} {})".format(
|
||||
self.cleaned_data['device_a'], interface_name
|
||||
))
|
||||
|
||||
return interface
|
||||
class InterfaceConnectionImportForm(BootstrapMixin, BulkImportForm):
|
||||
csv = CSVDataField(csv_form=InterfaceConnectionCSVForm)
|
||||
|
||||
def clean_interface_b(self):
|
||||
def clean(self):
|
||||
records = self.cleaned_data.get('csv')
|
||||
if not records:
|
||||
return
|
||||
|
||||
interface_name = self.cleaned_data.get('interface_b')
|
||||
if not interface_name:
|
||||
return None
|
||||
connection_list = []
|
||||
occupied_interfaces = []
|
||||
|
||||
try:
|
||||
# Retrieve interface by name
|
||||
interface = Interface.objects.get(
|
||||
device=self.cleaned_data['device_b'], name=interface_name
|
||||
)
|
||||
# Check for an existing connection to this interface
|
||||
if InterfaceConnection.objects.filter(Q(interface_a=interface) | Q(interface_b=interface)).count():
|
||||
raise forms.ValidationError("{} {} is already connected".format(
|
||||
self.cleaned_data['device_b'], interface_name
|
||||
))
|
||||
except Interface.DoesNotExist:
|
||||
raise forms.ValidationError("Invalid interface ({} {})".format(
|
||||
self.cleaned_data['device_b'], interface_name
|
||||
))
|
||||
for i, record in enumerate(records, start=1):
|
||||
form = self.fields['csv'].csv_form(data=record)
|
||||
if form.is_valid():
|
||||
interface_a = Interface.objects.get(device=form.cleaned_data['device_a'],
|
||||
name=form.cleaned_data['interface_a'])
|
||||
if interface_a in occupied_interfaces:
|
||||
raise forms.ValidationError("{} {} found in multiple connections"
|
||||
.format(interface_a.device.name, interface_a.name))
|
||||
interface_b = Interface.objects.get(device=form.cleaned_data['device_b'],
|
||||
name=form.cleaned_data['interface_b'])
|
||||
if interface_b in occupied_interfaces:
|
||||
raise forms.ValidationError("{} {} found in multiple connections"
|
||||
.format(interface_b.device.name, interface_b.name))
|
||||
connection = InterfaceConnection(interface_a=interface_a, interface_b=interface_b)
|
||||
if form.cleaned_data['status'] == 'planned':
|
||||
connection.connection_status = CONNECTION_STATUS_PLANNED
|
||||
else:
|
||||
connection.connection_status = CONNECTION_STATUS_CONNECTED
|
||||
connection_list.append(connection)
|
||||
occupied_interfaces.append(interface_a)
|
||||
occupied_interfaces.append(interface_b)
|
||||
else:
|
||||
for field, errors in form.errors.items():
|
||||
for e in errors:
|
||||
self.add_error('csv', "Record {} {}: {}".format(i, field, e))
|
||||
|
||||
return interface
|
||||
self.cleaned_data['csv'] = connection_list
|
||||
|
||||
|
||||
class InterfaceConnectionDeletionForm(BootstrapMixin, forms.Form):
|
||||
@@ -1765,4 +1697,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']
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
@@ -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'),
|
||||
),
|
||||
]
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
@@ -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
|
||||
#
|
||||
@@ -86,10 +280,6 @@ class Site(CreatedUpdatedModel, CustomFieldModel):
|
||||
|
||||
objects = SiteManager()
|
||||
|
||||
csv_headers = [
|
||||
'name', 'slug', 'region', 'tenant', 'facility', 'asn', 'contact_name', 'contact_phone', 'contact_email',
|
||||
]
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
|
||||
@@ -156,7 +346,7 @@ class RackGroup(models.Model):
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
return '{} - {}'.format(self.site.name, self.name)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return "{}?group_id={}".format(reverse('dcim:rack_list'), self.pk)
|
||||
@@ -212,10 +402,6 @@ class Rack(CreatedUpdatedModel, CustomFieldModel):
|
||||
|
||||
objects = RackManager()
|
||||
|
||||
csv_headers = [
|
||||
'site', 'group_name', 'name', 'facility_id', 'tenant', 'role', 'type', 'width', 'u_height', 'desc_units',
|
||||
]
|
||||
|
||||
class Meta:
|
||||
ordering = ['site', 'name']
|
||||
unique_together = [
|
||||
@@ -622,7 +808,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 +833,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 +848,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 +859,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']
|
||||
@@ -801,11 +981,6 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
|
||||
|
||||
objects = DeviceManager()
|
||||
|
||||
csv_headers = [
|
||||
'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag', 'status',
|
||||
'site', 'rack_group', 'rack_name', 'position', 'face',
|
||||
]
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
unique_together = ['rack', 'position', 'face']
|
||||
@@ -921,7 +1096,6 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
|
||||
self.asset_tag,
|
||||
self.get_status_display(),
|
||||
self.site.name,
|
||||
self.rack.group.name if self.rack and self.rack.group else None,
|
||||
self.rack.name if self.rack else None,
|
||||
self.position,
|
||||
self.get_face_display(),
|
||||
@@ -988,8 +1162,6 @@ class ConsolePort(models.Model):
|
||||
verbose_name='Console server port', blank=True, null=True)
|
||||
connection_status = models.NullBooleanField(choices=CONNECTION_STATUS_CHOICES, default=CONNECTION_STATUS_CONNECTED)
|
||||
|
||||
csv_headers = ['console_server', 'cs_port', 'device', 'console_port', 'connection_status']
|
||||
|
||||
class Meta:
|
||||
ordering = ['device', 'name']
|
||||
unique_together = ['device', 'name']
|
||||
@@ -1059,8 +1231,6 @@ class PowerPort(models.Model):
|
||||
blank=True, null=True)
|
||||
connection_status = models.NullBooleanField(choices=CONNECTION_STATUS_CHOICES, default=CONNECTION_STATUS_CONNECTED)
|
||||
|
||||
csv_headers = ['pdu', 'power_outlet', 'device', 'power_port', 'connection_status']
|
||||
|
||||
class Meta:
|
||||
ordering = ['device', 'name']
|
||||
unique_together = ['device', 'name']
|
||||
@@ -1120,27 +1290,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 +1311,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 +1326,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 +1343,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
|
||||
@@ -1237,16 +1392,11 @@ class InterfaceConnection(models.Model):
|
||||
connection_status = models.BooleanField(choices=CONNECTION_STATUS_CHOICES, default=CONNECTION_STATUS_CONNECTED,
|
||||
verbose_name='Status')
|
||||
|
||||
csv_headers = ['device_a', 'interface_a', 'device_b', 'interface_b', 'connection_status']
|
||||
|
||||
def clean(self):
|
||||
try:
|
||||
if self.interface_a == self.interface_b:
|
||||
raise ValidationError({
|
||||
'interface_b': "Cannot connect an interface to itself."
|
||||
})
|
||||
except ObjectDoesNotExist:
|
||||
pass
|
||||
if self.interface_a == self.interface_b:
|
||||
raise ValidationError({
|
||||
'interface_b': "Cannot connect an interface to itself."
|
||||
})
|
||||
|
||||
# Used for connections export
|
||||
def to_csv(self):
|
||||
@@ -1306,17 +1456,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']
|
||||
|
||||
@@ -247,7 +247,7 @@ class RackImportTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Rack
|
||||
fields = ('name', 'site', 'group', 'facility_id', 'tenant', 'u_height')
|
||||
fields = ('site', 'group', 'name', 'facility_id', 'tenant', 'u_height')
|
||||
|
||||
|
||||
#
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -8,7 +8,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
|
||||
@@ -29,8 +29,8 @@ from . import filters, forms, tables
|
||||
from .models import (
|
||||
CONNECTION_STATUS_CONNECTED, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device,
|
||||
DeviceBay, DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate,
|
||||
Manufacturer, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack,
|
||||
RackGroup, RackReservation, RackRole, Region, Site,
|
||||
Manufacturer, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
|
||||
RackReservation, RackRole, Region, Site,
|
||||
)
|
||||
|
||||
|
||||
@@ -141,44 +141,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 +151,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 +160,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 +203,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
|
||||
@@ -265,8 +219,9 @@ class SiteDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
|
||||
class SiteBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
permission_required = 'dcim.add_site'
|
||||
model_form = forms.SiteCSVForm
|
||||
form = forms.SiteImportForm
|
||||
table = tables.SiteTable
|
||||
template_name = 'dcim/site_import.html'
|
||||
default_return_url = 'dcim:site_list'
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -447,8 +390,9 @@ class RackDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
|
||||
class RackBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
permission_required = 'dcim.add_rack'
|
||||
model_form = forms.RackCSVForm
|
||||
form = forms.RackImportForm
|
||||
table = tables.RackImportTable
|
||||
template_name = 'dcim/rack_import.html'
|
||||
default_return_url = 'dcim:rack_list'
|
||||
|
||||
|
||||
@@ -480,8 +424,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 +439,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 +463,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 +472,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 +509,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 +527,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 +537,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 +577,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 +594,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 +609,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 +624,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 +639,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 +662,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 +687,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 +696,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 +712,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 +721,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 +759,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'
|
||||
@@ -843,14 +778,20 @@ class DeviceView(View):
|
||||
services = Service.objects.filter(device=device)
|
||||
secrets = device.secrets.all()
|
||||
|
||||
# Find up to ten devices in the same site with the same functional role for quick reference.
|
||||
related_devices = Device.objects.filter(
|
||||
site=device.site, device_role=device.device_role
|
||||
).exclude(
|
||||
pk=device.pk
|
||||
).select_related(
|
||||
'rack', 'device_type__manufacturer'
|
||||
)[:10]
|
||||
# Find any related devices for convenient linking in the UI
|
||||
related_devices = []
|
||||
if device.name:
|
||||
if re.match('.+[0-9]+$', device.name):
|
||||
# Strip 1 or more trailing digits (e.g. core-switch1)
|
||||
base_name = re.match('(.*?)[0-9]+$', device.name).group(1)
|
||||
elif re.match('.+\d[a-z]$', device.name.lower()):
|
||||
# Strip a trailing letter if preceded by a digit (e.g. dist-switch3a -> dist-switch3)
|
||||
base_name = re.match('(.*\d+)[a-z]$', device.name.lower()).group(1)
|
||||
else:
|
||||
base_name = None
|
||||
if base_name:
|
||||
related_devices = Device.objects.filter(name__istartswith=base_name).exclude(pk=device.pk)\
|
||||
.select_related('rack', 'device_type__manufacturer')[:10]
|
||||
|
||||
# Show graph button on interfaces only if at least one graph has been created.
|
||||
show_graphs = Graph.objects.filter(type=GRAPH_TYPE_INTERFACE).exists()
|
||||
@@ -862,6 +803,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 +850,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
|
||||
@@ -928,7 +866,7 @@ class DeviceDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
|
||||
class DeviceBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
permission_required = 'dcim.add_device'
|
||||
model_form = forms.DeviceCSVForm
|
||||
form = forms.DeviceImportForm
|
||||
table = tables.DeviceImportTable
|
||||
template_name = 'dcim/device_import.html'
|
||||
default_return_url = 'dcim:device_list'
|
||||
@@ -936,22 +874,23 @@ class DeviceBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
|
||||
class ChildDeviceBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
permission_required = 'dcim.add_device'
|
||||
model_form = forms.ChildDeviceCSVForm
|
||||
form = forms.ChildDeviceImportForm
|
||||
table = tables.DeviceImportTable
|
||||
template_name = 'dcim/device_import_child.html'
|
||||
default_return_url = 'dcim:device_list'
|
||||
|
||||
def _save_obj(self, obj_form):
|
||||
def save_obj(self, obj):
|
||||
|
||||
obj = obj_form.save()
|
||||
# Inherit site and rack from parent device
|
||||
obj.site = obj.parent_bay.device.site
|
||||
obj.rack = obj.parent_bay.device.rack
|
||||
obj.save()
|
||||
|
||||
# Save the reverse relation to the parent device bay
|
||||
# Save the reverse relation
|
||||
device_bay = obj.parent_bay
|
||||
device_bay.installed_device = obj
|
||||
device_bay.save()
|
||||
|
||||
return obj
|
||||
|
||||
|
||||
class DeviceBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'dcim.change_device'
|
||||
@@ -973,7 +912,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'
|
||||
@@ -1077,8 +1016,9 @@ class ConsolePortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
|
||||
class ConsoleConnectionsBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
permission_required = 'dcim.change_consoleport'
|
||||
model_form = forms.ConsoleConnectionCSVForm
|
||||
form = forms.ConsoleConnectionImportForm
|
||||
table = tables.ConsoleConnectionTable
|
||||
template_name = 'dcim/console_connections_import.html'
|
||||
default_return_url = 'dcim:console_connections_list'
|
||||
|
||||
|
||||
@@ -1086,7 +1026,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 +1125,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 +1135,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'
|
||||
@@ -1308,8 +1239,9 @@ class PowerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
|
||||
class PowerConnectionsBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
permission_required = 'dcim.change_powerport'
|
||||
model_form = forms.PowerConnectionCSVForm
|
||||
form = forms.PowerConnectionImportForm
|
||||
table = tables.PowerConnectionTable
|
||||
template_name = 'dcim/power_connections_import.html'
|
||||
default_return_url = 'dcim:power_connections_list'
|
||||
|
||||
|
||||
@@ -1317,7 +1249,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 +1348,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 +1358,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 +1378,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 +1396,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'
|
||||
@@ -1767,8 +1676,9 @@ def interfaceconnection_delete(request, pk):
|
||||
|
||||
class InterfaceConnectionsBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
permission_required = 'dcim.change_interface'
|
||||
model_form = forms.InterfaceConnectionCSVForm
|
||||
form = forms.InterfaceConnectionImportForm
|
||||
table = tables.InterfaceConnectionTable
|
||||
template_name = 'dcim/interface_connections_import.html'
|
||||
default_return_url = 'dcim:interface_connections_list'
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from __future__ import unicode_literals
|
||||
from datetime import datetime
|
||||
|
||||
from rest_framework import serializers
|
||||
from rest_framework.exceptions import ValidationError
|
||||
@@ -7,9 +6,7 @@ from rest_framework.exceptions import ValidationError
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db import transaction
|
||||
|
||||
from extras.models import (
|
||||
CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_SELECT, CustomField, CustomFieldChoice, CustomFieldValue,
|
||||
)
|
||||
from extras.models import CF_TYPE_SELECT, CustomField, CustomFieldChoice, CustomFieldValue
|
||||
|
||||
|
||||
#
|
||||
@@ -28,34 +25,16 @@ class CustomFieldsSerializer(serializers.BaseSerializer):
|
||||
|
||||
for field_name, value in data.items():
|
||||
|
||||
cf = custom_fields[field_name]
|
||||
|
||||
# Validate custom field name
|
||||
if field_name not in custom_fields:
|
||||
raise ValidationError("Invalid custom field for {} objects: {}".format(content_type, field_name))
|
||||
|
||||
# Validate boolean
|
||||
if cf.type == CF_TYPE_BOOLEAN and value not in [True, False, 1, 0]:
|
||||
raise ValidationError("Invalid value for boolean field {}: {}".format(field_name, value))
|
||||
|
||||
# Validate date
|
||||
if cf.type == CF_TYPE_DATE:
|
||||
try:
|
||||
datetime.strptime(value, '%Y-%m-%d')
|
||||
except ValueError:
|
||||
raise ValidationError("Invalid date for field {}: {}. (Required format is YYYY-MM-DD.)".format(
|
||||
field_name, value
|
||||
))
|
||||
|
||||
# Validate selected choice
|
||||
cf = custom_fields[field_name]
|
||||
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))
|
||||
raise ValidationError("Invalid choice ({}) for field {}".format(value, field_name))
|
||||
|
||||
# Check for missing required fields
|
||||
missing_fields = []
|
||||
@@ -108,19 +87,9 @@ class CustomFieldModelSerializer(serializers.ModelSerializer):
|
||||
field=custom_field,
|
||||
obj_type=content_type,
|
||||
obj_id=instance.pk,
|
||||
defaults={'serialized_value': custom_field.serialize_value(value)},
|
||||
defaults={'serialized_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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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'),
|
||||
)
|
||||
@@ -32,7 +32,7 @@ class CustomFieldFilter(django_filters.Filter):
|
||||
pass
|
||||
return queryset.filter(
|
||||
custom_field_values__field__name=self.name,
|
||||
custom_field_values__serialized_value__icontains=value,
|
||||
custom_field_values__serialized_value=value,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import code
|
||||
import platform
|
||||
import sys
|
||||
|
||||
from django import get_version
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.db.models import Model
|
||||
|
||||
|
||||
APPS = ['circuits', 'dcim', 'extras', 'ipam', 'secrets', 'tenancy', 'users']
|
||||
|
||||
BANNER_TEXT = """### NetBox interactive shell ({node})
|
||||
### Python {python} | Django {django} | NetBox {netbox}
|
||||
### lsmodels() will show available models. Use help(<model>) for more info.""".format(
|
||||
node=platform.node(),
|
||||
python=platform.python_version(),
|
||||
django=get_version(),
|
||||
netbox=settings.VERSION
|
||||
)
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Start the Django shell with all NetBox models already imported"
|
||||
django_models = {}
|
||||
|
||||
def _lsmodels(self):
|
||||
for app, models in self.django_models.items():
|
||||
app_name = apps.get_app_config(app).verbose_name
|
||||
print('{}:'.format(app_name))
|
||||
for m in models:
|
||||
print(' {}'.format(m))
|
||||
|
||||
def get_namespace(self):
|
||||
namespace = {}
|
||||
|
||||
# Gather Django models from each app
|
||||
for app in APPS:
|
||||
self.django_models[app] = []
|
||||
app_models = sys.modules['{}.models'.format(app)]
|
||||
for name in dir(app_models):
|
||||
model = getattr(app_models, name)
|
||||
try:
|
||||
if issubclass(model, Model):
|
||||
namespace[name] = model
|
||||
self.django_models[app].append(name)
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
# Load convenience commands
|
||||
namespace.update({
|
||||
'lsmodels': self._lsmodels,
|
||||
})
|
||||
|
||||
return namespace
|
||||
|
||||
def handle(self, **options):
|
||||
shell = code.interact(banner=BANNER_TEXT, local=self.get_namespace())
|
||||
return shell
|
||||
@@ -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'),
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
@@ -84,11 +139,7 @@ class CustomField(models.Model):
|
||||
if self.type == CF_TYPE_BOOLEAN:
|
||||
return str(int(bool(value)))
|
||||
if self.type == CF_TYPE_DATE:
|
||||
# Could be date/datetime object or string
|
||||
try:
|
||||
return value.strftime('%Y-%m-%d')
|
||||
except AttributeError:
|
||||
return value
|
||||
return value.strftime('%Y-%m-%d')
|
||||
if self.type == CF_TYPE_SELECT:
|
||||
# Could be ModelChoiceField or TypedChoiceField
|
||||
return str(value.id) if hasattr(value, 'id') else str(value)
|
||||
@@ -316,8 +367,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 +382,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:
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
@@ -138,13 +137,10 @@ class WritableVLANGroupSerializer(serializers.ModelSerializer):
|
||||
# Validate uniqueness of name and slug if a site has been assigned.
|
||||
if data.get('site', None):
|
||||
for field in ['name', 'slug']:
|
||||
validator = UniqueTogetherValidator(queryset=VLANGroup.objects.all(), fields=('site', field))
|
||||
validator = UniqueTogetherValidator(queryset=VLAN.objects.all(), fields=('site', field))
|
||||
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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'),
|
||||
)
|
||||
@@ -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)',
|
||||
)
|
||||
@@ -83,18 +85,11 @@ class AggregateFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
try:
|
||||
prefix = str(IPNetwork(value.strip()).cidr)
|
||||
qs_filter |= Q(prefix__net_contains_or_equals=prefix)
|
||||
except (AddrFormatError, ValueError):
|
||||
except AddrFormatError:
|
||||
pass
|
||||
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():
|
||||
@@ -172,7 +172,7 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
try:
|
||||
prefix = str(IPNetwork(value.strip()).cidr)
|
||||
qs_filter |= Q(prefix__net_contains_or_equals=prefix)
|
||||
except (AddrFormatError, ValueError):
|
||||
except AddrFormatError:
|
||||
pass
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
@@ -183,7 +183,7 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
try:
|
||||
query = str(IPNetwork(value).cidr)
|
||||
return queryset.filter(prefix__net_contained_or_equal=query)
|
||||
except (AddrFormatError, ValueError):
|
||||
except AddrFormatError:
|
||||
return queryset.none()
|
||||
|
||||
def filter_mask_length(self, queryset, name, value):
|
||||
@@ -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
|
||||
@@ -259,7 +259,7 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
try:
|
||||
ipaddress = str(IPNetwork(value.strip()))
|
||||
qs_filter |= Q(address__net_host=ipaddress)
|
||||
except (AddrFormatError, ValueError):
|
||||
except AddrFormatError:
|
||||
pass
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
@@ -270,7 +270,7 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
try:
|
||||
query = str(IPNetwork(value.strip()).cidr)
|
||||
return queryset.filter(address__net_host_contained=query)
|
||||
except (AddrFormatError, ValueError):
|
||||
except AddrFormatError:
|
||||
return queryset.none()
|
||||
|
||||
def filter_mask_length(self, queryset, name, value):
|
||||
@@ -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)',
|
||||
)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django import forms
|
||||
from django.core.exceptions import MultipleObjectsReturned
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db.models import Count
|
||||
|
||||
from dcim.models import Site, Rack, Device, Interface
|
||||
@@ -9,13 +9,12 @@ from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFi
|
||||
from tenancy.forms import TenancyForm
|
||||
from tenancy.models import Tenant
|
||||
from utilities.forms import (
|
||||
APISelect, BootstrapMixin, BulkEditNullBooleanSelect, ChainedModelChoiceField, CSVChoiceField,
|
||||
ExpandableIPAddressField, FilterChoiceField, FlexibleModelChoiceField, Livesearch, ReturnURLForm, SlugField,
|
||||
add_blank_choice,
|
||||
APISelect, BootstrapMixin, BulkEditNullBooleanSelect, BulkImportForm, ChainedModelChoiceField, CSVDataField,
|
||||
ExpandableIPAddressField, FilterChoiceField, Livesearch, ReturnURLForm, SlugField, 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,
|
||||
)
|
||||
|
||||
|
||||
@@ -49,23 +48,17 @@ class VRFForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
||||
}
|
||||
|
||||
|
||||
class VRFCSVForm(forms.ModelForm):
|
||||
tenant = forms.ModelChoiceField(
|
||||
queryset=Tenant.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text='Name of assigned tenant',
|
||||
error_messages={
|
||||
'invalid_choice': 'Tenant not found.',
|
||||
}
|
||||
)
|
||||
class VRFFromCSVForm(forms.ModelForm):
|
||||
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
|
||||
error_messages={'invalid_choice': 'Tenant not found.'})
|
||||
|
||||
class Meta:
|
||||
model = VRF
|
||||
fields = ['name', 'rd', 'tenant', 'enforce_unique', 'description']
|
||||
help_texts = {
|
||||
'name': 'VRF name',
|
||||
}
|
||||
|
||||
|
||||
class VRFImportForm(BootstrapMixin, BulkImportForm):
|
||||
csv = CSVDataField(csv_form=VRFFromCSVForm)
|
||||
|
||||
|
||||
class VRFBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
@@ -123,21 +116,19 @@ class AggregateForm(BootstrapMixin, CustomFieldForm):
|
||||
}
|
||||
|
||||
|
||||
class AggregateCSVForm(forms.ModelForm):
|
||||
rir = forms.ModelChoiceField(
|
||||
queryset=RIR.objects.all(),
|
||||
to_field_name='name',
|
||||
help_text='Name of parent RIR',
|
||||
error_messages={
|
||||
'invalid_choice': 'RIR not found.',
|
||||
}
|
||||
)
|
||||
class AggregateFromCSVForm(forms.ModelForm):
|
||||
rir = forms.ModelChoiceField(queryset=RIR.objects.all(), to_field_name='name',
|
||||
error_messages={'invalid_choice': 'RIR not found.'})
|
||||
|
||||
class Meta:
|
||||
model = Aggregate
|
||||
fields = ['prefix', 'rir', 'date_added', 'description']
|
||||
|
||||
|
||||
class AggregateImportForm(BootstrapMixin, BulkImportForm):
|
||||
csv = CSVDataField(csv_form=AggregateFromCSVForm)
|
||||
|
||||
|
||||
class AggregateBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=Aggregate.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
rir = forms.ModelChoiceField(queryset=RIR.objects.all(), required=False, label='RIR')
|
||||
@@ -181,18 +172,6 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
||||
required=False,
|
||||
label='Site',
|
||||
widget=forms.Select(
|
||||
attrs={'filter-for': 'vlan_group', 'nullable': 'true'}
|
||||
)
|
||||
)
|
||||
vlan_group = ChainedModelChoiceField(
|
||||
queryset=VLANGroup.objects.all(),
|
||||
chains=(
|
||||
('site', 'site'),
|
||||
),
|
||||
required=False,
|
||||
label='VLAN group',
|
||||
widget=APISelect(
|
||||
api_url='/api/ipam/vlan-groups/?site_id={{site}}',
|
||||
attrs={'filter-for': 'vlan', 'nullable': 'true'}
|
||||
)
|
||||
)
|
||||
@@ -200,12 +179,11 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
||||
queryset=VLAN.objects.all(),
|
||||
chains=(
|
||||
('site', 'site'),
|
||||
('group', 'vlan_group'),
|
||||
),
|
||||
required=False,
|
||||
label='VLAN',
|
||||
widget=APISelect(
|
||||
api_url='/api/ipam/vlans/?site_id={{site}}&group_id={{vlan_group}}', display_field='display_name'
|
||||
api_url='/api/ipam/vlans/?site_id={{site}}', display_field='display_name'
|
||||
)
|
||||
)
|
||||
|
||||
@@ -214,108 +192,74 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
||||
fields = ['prefix', 'vrf', 'site', 'vlan', 'status', 'role', 'is_pool', 'description', 'tenant_group', 'tenant']
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
# Initialize helper selectors
|
||||
instance = kwargs.get('instance')
|
||||
initial = kwargs.get('initial', {})
|
||||
if instance and instance.vlan is not None:
|
||||
initial['vlan_group'] = instance.vlan.group
|
||||
kwargs['initial'] = initial
|
||||
|
||||
super(PrefixForm, self).__init__(*args, **kwargs)
|
||||
|
||||
self.fields['vrf'].empty_label = 'Global'
|
||||
|
||||
|
||||
class PrefixCSVForm(forms.ModelForm):
|
||||
vrf = forms.ModelChoiceField(
|
||||
queryset=VRF.objects.all(),
|
||||
required=False,
|
||||
to_field_name='rd',
|
||||
help_text='Route distinguisher of parent VRF',
|
||||
error_messages={
|
||||
'invalid_choice': 'VRF not found.',
|
||||
}
|
||||
)
|
||||
tenant = forms.ModelChoiceField(
|
||||
queryset=Tenant.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text='Name of assigned tenant',
|
||||
error_messages={
|
||||
'invalid_choice': 'Tenant not found.',
|
||||
}
|
||||
)
|
||||
site = forms.ModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text='Name of parent site',
|
||||
error_messages={
|
||||
'invalid_choice': 'Site not found.',
|
||||
}
|
||||
)
|
||||
vlan_group = forms.CharField(
|
||||
help_text='Group name of assigned VLAN',
|
||||
required=False
|
||||
)
|
||||
vlan_vid = forms.IntegerField(
|
||||
help_text='Numeric ID of assigned VLAN',
|
||||
required=False
|
||||
)
|
||||
status = CSVChoiceField(
|
||||
choices=IPADDRESS_STATUS_CHOICES,
|
||||
help_text='Operational status'
|
||||
)
|
||||
role = forms.ModelChoiceField(
|
||||
queryset=Role.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text='Functional role',
|
||||
error_messages={
|
||||
'invalid_choice': 'Invalid role.',
|
||||
}
|
||||
)
|
||||
class PrefixFromCSVForm(forms.ModelForm):
|
||||
vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, to_field_name='rd',
|
||||
error_messages={'invalid_choice': 'VRF not found.'})
|
||||
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
|
||||
error_messages={'invalid_choice': 'Tenant not found.'})
|
||||
site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False, to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Site not found.'})
|
||||
vlan_group_name = forms.CharField(required=False)
|
||||
vlan_vid = forms.IntegerField(required=False)
|
||||
status = forms.CharField()
|
||||
role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False, to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Invalid role.'})
|
||||
|
||||
class Meta:
|
||||
model = Prefix
|
||||
fields = [
|
||||
'prefix', 'vrf', 'tenant', 'site', 'vlan_group', 'vlan_vid', 'status', 'role', 'is_pool', 'description',
|
||||
'prefix', 'vrf', 'tenant', 'site', 'vlan_group_name', 'vlan_vid', 'status', 'role', 'is_pool',
|
||||
'description',
|
||||
]
|
||||
|
||||
def clean(self):
|
||||
|
||||
super(PrefixCSVForm, self).clean()
|
||||
super(PrefixFromCSVForm, self).clean()
|
||||
|
||||
site = self.cleaned_data.get('site')
|
||||
vlan_group = self.cleaned_data.get('vlan_group')
|
||||
vlan_group_name = self.cleaned_data.get('vlan_group_name')
|
||||
vlan_vid = self.cleaned_data.get('vlan_vid')
|
||||
vlan_group = None
|
||||
|
||||
# Validate VLAN group
|
||||
if vlan_group_name:
|
||||
try:
|
||||
vlan_group = VLANGroup.objects.get(site=site, name=vlan_group_name)
|
||||
except VLANGroup.DoesNotExist:
|
||||
if site:
|
||||
self.add_error('vlan_group_name', "Invalid VLAN group ({} - {}).".format(site, vlan_group_name))
|
||||
else:
|
||||
self.add_error('vlan_group_name', "Invalid global VLAN group ({}).".format(vlan_group_name))
|
||||
|
||||
# Validate VLAN
|
||||
if vlan_group and vlan_vid:
|
||||
if vlan_vid:
|
||||
try:
|
||||
self.instance.vlan = VLAN.objects.get(site=site, group__name=vlan_group, vid=vlan_vid)
|
||||
self.instance.vlan = VLAN.objects.get(site=site, group=vlan_group, vid=vlan_vid)
|
||||
except VLAN.DoesNotExist:
|
||||
if site:
|
||||
raise forms.ValidationError("VLAN {} not found in site {} group {}".format(
|
||||
vlan_vid, site, vlan_group
|
||||
))
|
||||
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)
|
||||
except VLAN.DoesNotExist:
|
||||
if site:
|
||||
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))
|
||||
self.add_error('vlan_vid', "Invalid VLAN ID ({}) for site {}.".format(vlan_vid, site))
|
||||
elif vlan_group:
|
||||
self.add_error('vlan_vid', "Invalid VLAN ID ({}) for group {}.".format(vlan_vid, vlan_group_name))
|
||||
elif not vlan_group_name:
|
||||
self.add_error('vlan_vid', "Invalid global VLAN ID ({}).".format(vlan_vid))
|
||||
except VLAN.MultipleObjectsReturned:
|
||||
self.add_error('vlan_vid', "Multiple VLANs found ({} - VID {})".format(site, vlan_vid))
|
||||
|
||||
def clean_status(self):
|
||||
status_choices = {s[1].lower(): s[0] for s in PREFIX_STATUS_CHOICES}
|
||||
try:
|
||||
return status_choices[self.cleaned_data['status'].lower()]
|
||||
except KeyError:
|
||||
raise ValidationError("Invalid status: {}".format(self.cleaned_data['status']))
|
||||
|
||||
|
||||
class PrefixImportForm(BootstrapMixin, BulkImportForm):
|
||||
csv = CSVDataField(csv_form=PrefixFromCSVForm)
|
||||
|
||||
|
||||
class PrefixBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
@@ -484,7 +428,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 +441,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,67 +506,30 @@ 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)
|
||||
self.fields['vrf'].empty_label = 'Global'
|
||||
|
||||
|
||||
class IPAddressCSVForm(forms.ModelForm):
|
||||
vrf = forms.ModelChoiceField(
|
||||
queryset=VRF.objects.all(),
|
||||
required=False,
|
||||
to_field_name='rd',
|
||||
help_text='Route distinguisher of the assigned VRF',
|
||||
error_messages={
|
||||
'invalid_choice': 'VRF not found.',
|
||||
}
|
||||
)
|
||||
tenant = forms.ModelChoiceField(
|
||||
queryset=Tenant.objects.all(),
|
||||
to_field_name='name',
|
||||
required=False,
|
||||
help_text='Name of the assigned tenant',
|
||||
error_messages={
|
||||
'invalid_choice': 'Tenant not found.',
|
||||
}
|
||||
)
|
||||
status = CSVChoiceField(
|
||||
choices=IPADDRESS_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,
|
||||
to_field_name='name',
|
||||
help_text='Name or ID of assigned device',
|
||||
error_messages={
|
||||
'invalid_choice': 'Device not found.',
|
||||
}
|
||||
)
|
||||
interface_name = forms.CharField(
|
||||
help_text='Name of assigned interface',
|
||||
required=False
|
||||
)
|
||||
is_primary = forms.BooleanField(
|
||||
help_text='Make this the primary IP for the assigned device',
|
||||
required=False
|
||||
)
|
||||
class IPAddressFromCSVForm(forms.ModelForm):
|
||||
vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, to_field_name='rd',
|
||||
error_messages={'invalid_choice': 'VRF not found.'})
|
||||
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
|
||||
error_messages={'invalid_choice': 'Tenant not found.'})
|
||||
status = forms.CharField()
|
||||
device = forms.ModelChoiceField(queryset=Device.objects.all(), required=False, to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Device not found.'})
|
||||
interface_name = forms.CharField(required=False)
|
||||
is_primary = forms.BooleanField(required=False)
|
||||
|
||||
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):
|
||||
|
||||
super(IPAddressCSVForm, self).clean()
|
||||
|
||||
device = self.cleaned_data.get('device')
|
||||
interface_name = self.cleaned_data.get('interface_name')
|
||||
is_primary = self.cleaned_data.get('is_primary')
|
||||
@@ -630,39 +537,43 @@ class IPAddressCSVForm(forms.ModelForm):
|
||||
# Validate interface
|
||||
if device and interface_name:
|
||||
try:
|
||||
self.instance.interface = Interface.objects.get(device=device, name=interface_name)
|
||||
Interface.objects.get(device=device, name=interface_name)
|
||||
except Interface.DoesNotExist:
|
||||
raise forms.ValidationError("Invalid interface {} for device {}".format(interface_name, device))
|
||||
self.add_error('interface_name', "Invalid interface ({}) for {}".format(interface_name, device))
|
||||
elif device and not interface_name:
|
||||
raise forms.ValidationError("Device set ({}) but interface missing".format(device))
|
||||
self.add_error('interface_name', "Device set ({}) but interface missing".format(device))
|
||||
elif interface_name and not device:
|
||||
raise forms.ValidationError("Interface set ({}) but device missing or invalid".format(interface_name))
|
||||
self.add_error('device', "Interface set ({}) but device missing or invalid".format(interface_name))
|
||||
|
||||
# Validate is_primary
|
||||
if is_primary and not device:
|
||||
raise forms.ValidationError("No device specified; cannot set as primary IP")
|
||||
self.add_error('is_primary', "No device specified; cannot set as primary IP")
|
||||
|
||||
def clean_status(self):
|
||||
status_choices = {s[1].lower(): s[0] for s in IPADDRESS_STATUS_CHOICES}
|
||||
try:
|
||||
return status_choices[self.cleaned_data['status'].lower()]
|
||||
except KeyError:
|
||||
raise ValidationError("Invalid status: {}".format(self.cleaned_data['status']))
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
|
||||
# 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(IPAddressFromCSVForm, self).save(*args, **kwargs)
|
||||
|
||||
|
||||
class IPAddressImportForm(BootstrapMixin, BulkImportForm):
|
||||
csv = CSVDataField(csv_form=IPAddressFromCSVForm)
|
||||
|
||||
|
||||
class IPAddressBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
@@ -670,11 +581,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 +594,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 +614,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)
|
||||
|
||||
|
||||
#
|
||||
@@ -771,67 +673,60 @@ class VLANForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
||||
}
|
||||
|
||||
|
||||
class VLANCSVForm(forms.ModelForm):
|
||||
class VLANFromCSVForm(forms.ModelForm):
|
||||
site = forms.ModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text='Name of parent site',
|
||||
error_messages={
|
||||
'invalid_choice': 'Site not found.',
|
||||
}
|
||||
)
|
||||
group_name = forms.CharField(
|
||||
help_text='Name of VLAN group',
|
||||
required=False
|
||||
queryset=Site.objects.all(), required=False, to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Site not found.'}
|
||||
)
|
||||
group_name = forms.CharField(required=False)
|
||||
tenant = forms.ModelChoiceField(
|
||||
queryset=Tenant.objects.all(),
|
||||
to_field_name='name',
|
||||
required=False,
|
||||
help_text='Name of assigned tenant',
|
||||
error_messages={
|
||||
'invalid_choice': 'Tenant not found.',
|
||||
}
|
||||
)
|
||||
status = CSVChoiceField(
|
||||
choices=VLAN_STATUS_CHOICES,
|
||||
help_text='Operational status'
|
||||
Tenant.objects.all(), to_field_name='name', required=False,
|
||||
error_messages={'invalid_choice': 'Tenant not found.'}
|
||||
)
|
||||
status = forms.CharField()
|
||||
role = forms.ModelChoiceField(
|
||||
queryset=Role.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text='Functional role',
|
||||
error_messages={
|
||||
'invalid_choice': 'Invalid role.',
|
||||
}
|
||||
queryset=Role.objects.all(), required=False, to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Invalid role.'}
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = VLAN
|
||||
fields = ['site', 'group_name', 'vid', 'name', 'tenant', 'status', 'role', 'description']
|
||||
help_texts = {
|
||||
'vid': 'Numeric VLAN ID (1-4095)',
|
||||
'name': 'VLAN name',
|
||||
}
|
||||
|
||||
def clean(self):
|
||||
|
||||
super(VLANCSVForm, self).clean()
|
||||
super(VLANFromCSVForm, self).clean()
|
||||
|
||||
site = self.cleaned_data.get('site')
|
||||
# Validate VLANGroup
|
||||
group_name = self.cleaned_data.get('group_name')
|
||||
|
||||
# Validate VLAN group
|
||||
if group_name:
|
||||
try:
|
||||
self.instance.group = VLANGroup.objects.get(site=site, name=group_name)
|
||||
VLANGroup.objects.get(site=self.cleaned_data.get('site'), name=group_name)
|
||||
except VLANGroup.DoesNotExist:
|
||||
if site:
|
||||
raise forms.ValidationError("VLAN group {} not found for site {}".format(group_name, site))
|
||||
else:
|
||||
raise forms.ValidationError("Global VLAN group {} not found".format(group_name))
|
||||
self.add_error('group_name', "Invalid VLAN group {}.".format(group_name))
|
||||
|
||||
def clean_status(self):
|
||||
status_choices = {s[1].lower(): s[0] for s in VLAN_STATUS_CHOICES}
|
||||
try:
|
||||
return status_choices[self.cleaned_data['status'].lower()]
|
||||
except KeyError:
|
||||
raise ValidationError("Invalid status: {}".format(self.cleaned_data['status']))
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
|
||||
vlan = super(VLANFromCSVForm, self).save(commit=False)
|
||||
|
||||
# Assign VLANGroup by site and name
|
||||
if self.cleaned_data['group_name']:
|
||||
vlan.group = VLANGroup.objects.get(site=self.cleaned_data['site'], name=self.cleaned_data['group_name'])
|
||||
|
||||
if kwargs.get('commit'):
|
||||
vlan.save()
|
||||
return vlan
|
||||
|
||||
|
||||
class VLANImportForm(BootstrapMixin, BulkImportForm):
|
||||
csv = CSVDataField(csv_form=VLANFromCSVForm)
|
||||
|
||||
|
||||
class VLANBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
|
||||
@@ -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'),
|
||||
),
|
||||
]
|
||||
@@ -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):
|
||||
"""
|
||||
@@ -35,15 +89,13 @@ class VRF(CreatedUpdatedModel, CustomFieldModel):
|
||||
description = models.CharField(max_length=100, blank=True)
|
||||
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
|
||||
|
||||
csv_headers = ['name', 'rd', 'tenant', 'enforce_unique', 'description']
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
verbose_name = 'VRF'
|
||||
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 +109,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):
|
||||
@@ -100,8 +146,6 @@ class Aggregate(CreatedUpdatedModel, CustomFieldModel):
|
||||
description = models.CharField(max_length=100, blank=True)
|
||||
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
|
||||
|
||||
csv_headers = ['prefix', 'rir', 'date_added', 'description']
|
||||
|
||||
class Meta:
|
||||
ordering = ['family', 'prefix']
|
||||
|
||||
@@ -156,11 +200,15 @@ class Aggregate(CreatedUpdatedModel, CustomFieldModel):
|
||||
|
||||
def get_utilization(self):
|
||||
"""
|
||||
Determine the prefix utilization of the aggregate and return it as a percentage.
|
||||
Determine the utilization rate of the aggregate prefix 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
|
||||
@@ -249,10 +297,6 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel):
|
||||
|
||||
objects = PrefixQuerySet.as_manager()
|
||||
|
||||
csv_headers = [
|
||||
'prefix', 'vrf', 'tenant', 'site', 'vlan_group', 'vlan_vid', 'status', 'role', 'is_pool', 'description',
|
||||
]
|
||||
|
||||
class Meta:
|
||||
ordering = ['vrf', 'family', 'prefix']
|
||||
verbose_name_plural = 'prefixes'
|
||||
@@ -263,6 +307,9 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel):
|
||||
def get_absolute_url(self):
|
||||
return reverse('ipam:prefix', args=[self.pk])
|
||||
|
||||
def get_duplicates(self):
|
||||
return Prefix.objects.filter(vrf=self.vrf, prefix=str(self.prefix)).exclude(pk=self.pk)
|
||||
|
||||
def clean(self):
|
||||
|
||||
if self.prefix:
|
||||
@@ -310,64 +357,20 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel):
|
||||
self.description,
|
||||
])
|
||||
|
||||
def get_status_class(self):
|
||||
return STATUS_CHOICE_CLASSES[self.status]
|
||||
|
||||
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.
|
||||
"""
|
||||
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)
|
||||
|
||||
@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
|
||||
|
||||
def get_status_class(self):
|
||||
return STATUS_CHOICE_CLASSES[self.status]
|
||||
|
||||
|
||||
class IPAddressManager(models.Manager):
|
||||
|
||||
@@ -400,13 +403,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,10 +414,6 @@ class IPAddress(CreatedUpdatedModel, CustomFieldModel):
|
||||
|
||||
objects = IPAddressManager()
|
||||
|
||||
csv_headers = [
|
||||
'address', 'vrf', 'tenant', 'status', 'role', 'device', 'interface_name', 'is_primary', 'description',
|
||||
]
|
||||
|
||||
class Meta:
|
||||
ordering = ['family', 'address']
|
||||
verbose_name = 'IP address'
|
||||
@@ -459,19 +452,17 @@ class IPAddress(CreatedUpdatedModel, CustomFieldModel):
|
||||
def to_csv(self):
|
||||
|
||||
# Determine if this IP is primary for a Device
|
||||
is_primary = False
|
||||
if self.family == 4 and getattr(self, 'primary_ip4_for', False):
|
||||
is_primary = True
|
||||
elif self.family == 6 and getattr(self, 'primary_ip6_for', False):
|
||||
is_primary = True
|
||||
else:
|
||||
is_primary = False
|
||||
|
||||
return csv_format([
|
||||
self.address,
|
||||
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,
|
||||
@@ -507,7 +498,9 @@ class VLANGroup(models.Model):
|
||||
verbose_name_plural = 'VLAN groups'
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
if self.site is None:
|
||||
return self.name
|
||||
return '{} - {}'.format(self.site.name, self.name)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return "{}?group_id={}".format(reverse('ipam:vlan_list'), self.pk)
|
||||
@@ -536,8 +529,6 @@ class VLAN(CreatedUpdatedModel, CustomFieldModel):
|
||||
description = models.CharField(max_length=100, blank=True)
|
||||
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
|
||||
|
||||
csv_headers = ['site', 'group_name', 'vid', 'name', 'tenant', 'status', 'role', 'description']
|
||||
|
||||
class Meta:
|
||||
ordering = ['site', 'group', 'vid']
|
||||
unique_together = [
|
||||
|
||||
@@ -34,7 +34,7 @@ RIR_ACTIONS = """
|
||||
|
||||
UTILIZATION_GRAPH = """
|
||||
{% load helpers %}
|
||||
{% if record.pk %}{% utilization_graph value %}{% else %}—{% endif %}
|
||||
{% utilization_graph value %}
|
||||
"""
|
||||
|
||||
ROLE_ACTIONS = """
|
||||
@@ -241,7 +241,6 @@ 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')
|
||||
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')
|
||||
@@ -249,7 +248,7 @@ class PrefixTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Prefix
|
||||
fields = ('pk', 'prefix', 'status', 'vrf', 'get_utilization', 'tenant', 'site', 'vlan', 'role', 'description')
|
||||
fields = ('pk', 'prefix', 'status', 'vrf', 'tenant', 'site', 'vlan', 'role', 'description')
|
||||
row_attrs = {
|
||||
'class': lambda record: 'success' if not record.pk else '',
|
||||
}
|
||||
@@ -299,7 +298,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 +327,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')
|
||||
|
||||
|
||||
#
|
||||
|
||||
@@ -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):
|
||||
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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
|
||||
@@ -134,8 +130,9 @@ class VRFDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
|
||||
class VRFBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
permission_required = 'ipam.add_vrf'
|
||||
model_form = forms.VRFCSVForm
|
||||
form = forms.VRFImportForm
|
||||
table = tables.VRFTable
|
||||
template_name = 'ipam/vrf_import.html'
|
||||
default_return_url = 'ipam:vrf_list'
|
||||
|
||||
|
||||
@@ -243,8 +240,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 +249,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 +325,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
|
||||
@@ -352,8 +341,9 @@ class AggregateDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
|
||||
class AggregateBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
permission_required = 'ipam.add_aggregate'
|
||||
model_form = forms.AggregateCSVForm
|
||||
form = forms.AggregateImportForm
|
||||
table = tables.AggregateTable
|
||||
template_name = 'ipam/aggregate_import.html'
|
||||
default_return_url = 'ipam:aggregate_list'
|
||||
|
||||
|
||||
@@ -383,8 +373,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 +382,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 +521,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
|
||||
@@ -556,8 +538,9 @@ class PrefixDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
|
||||
class PrefixBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
permission_required = 'ipam.add_prefix'
|
||||
model_form = forms.PrefixCSVForm
|
||||
form = forms.PrefixImportForm
|
||||
table = tables.PrefixTable
|
||||
template_name = 'ipam/prefix_import.html'
|
||||
default_return_url = 'ipam:prefix_list'
|
||||
|
||||
|
||||
@@ -632,25 +615,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
|
||||
@@ -661,10 +640,24 @@ class IPAddressBulkCreateView(PermissionRequiredMixin, BulkCreateView):
|
||||
|
||||
class IPAddressBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
permission_required = 'ipam.add_ipaddress'
|
||||
model_form = forms.IPAddressCSVForm
|
||||
form = forms.IPAddressImportForm
|
||||
table = tables.IPAddressTable
|
||||
template_name = 'ipam/ipaddress_import.html'
|
||||
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 +687,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 +696,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 +732,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
|
||||
@@ -763,8 +748,9 @@ class VLANDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
|
||||
class VLANBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
permission_required = 'ipam.add_vlan'
|
||||
model_form = forms.VLANCSVForm
|
||||
form = forms.VLANImportForm
|
||||
table = tables.VLANTable
|
||||
template_name = 'ipam/vlan_import.html'
|
||||
default_return_url = 'ipam:vlan_list'
|
||||
|
||||
|
||||
@@ -788,8 +774,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 +789,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
|
||||
|
||||
@@ -58,11 +58,6 @@ CORS_ORIGIN_REGEX_WHITELIST = [
|
||||
# r'^(https?://)?(\w+\.)?example\.com$',
|
||||
]
|
||||
|
||||
# Set to True to enable server debugging. WARNING: Debugging introduces a substantial performance penalty and may reveal
|
||||
# sensitive information about your installation. Only enable debugging while performing testing. Never enable debugging
|
||||
# on a production system.
|
||||
DEBUG = False
|
||||
|
||||
# Email settings
|
||||
EMAIL = {
|
||||
'SERVER': 'localhost',
|
||||
@@ -77,10 +72,6 @@ EMAIL = {
|
||||
# (all prefixes and IP addresses not assigned to a VRF), set ENFORCE_GLOBAL_UNIQUE to True.
|
||||
ENFORCE_GLOBAL_UNIQUE = False
|
||||
|
||||
# Enable custom logging. Please see the Django documentation for detailed guidance on configuring custom logs:
|
||||
# https://docs.djangoproject.com/en/1.11/topics/logging/
|
||||
LOGGING = {}
|
||||
|
||||
# Setting this to True will permit only authenticated users to access any part of NetBox. By default, anonymous users
|
||||
# are permitted to access most data in NetBox (excluding secrets) but not make any changes.
|
||||
LOGIN_REQUIRED = False
|
||||
@@ -88,11 +79,6 @@ LOGIN_REQUIRED = False
|
||||
# Setting this to True will display a "maintenance mode" banner at the top of every page.
|
||||
MAINTENANCE_MODE = False
|
||||
|
||||
# An API consumer can request an arbitrary number of objects =by appending the "limit" parameter to the URL (e.g.
|
||||
# "?limit=1000"). This setting defines the maximum limit. Setting it to 0 or None will allow an API consumer to request
|
||||
# all objects by specifying "?limit=0".
|
||||
MAX_PAGE_SIZE = 1000
|
||||
|
||||
# Credentials that NetBox will use to access live devices (future use).
|
||||
NETBOX_USERNAME = ''
|
||||
NETBOX_PASSWORD = ''
|
||||
|
||||
@@ -13,9 +13,9 @@ except ImportError:
|
||||
)
|
||||
|
||||
|
||||
VERSION = '2.1-beta'
|
||||
VERSION = '2.0.4'
|
||||
|
||||
# Import required configuration parameters
|
||||
# Import local configuration
|
||||
ALLOWED_HOSTS = DATABASE = SECRET_KEY = None
|
||||
for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']:
|
||||
try:
|
||||
@@ -25,35 +25,32 @@ for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']:
|
||||
"Mandatory setting {} is missing from configuration.py.".format(setting)
|
||||
)
|
||||
|
||||
# Import optional configuration parameters
|
||||
# Default configurations
|
||||
ADMINS = getattr(configuration, 'ADMINS', [])
|
||||
BANNER_BOTTOM = getattr(configuration, 'BANNER_BOTTOM', False)
|
||||
BANNER_TOP = getattr(configuration, 'BANNER_TOP', False)
|
||||
DEBUG = getattr(configuration, 'DEBUG', False)
|
||||
EMAIL = getattr(configuration, 'EMAIL', {})
|
||||
LOGIN_REQUIRED = getattr(configuration, 'LOGIN_REQUIRED', False)
|
||||
BASE_PATH = getattr(configuration, 'BASE_PATH', '')
|
||||
if BASE_PATH:
|
||||
BASE_PATH = BASE_PATH.strip('/') + '/' # Enforce trailing slash only
|
||||
CORS_ORIGIN_ALLOW_ALL = getattr(configuration, 'CORS_ORIGIN_ALLOW_ALL', False)
|
||||
CORS_ORIGIN_REGEX_WHITELIST = getattr(configuration, 'CORS_ORIGIN_REGEX_WHITELIST', [])
|
||||
CORS_ORIGIN_WHITELIST = getattr(configuration, 'CORS_ORIGIN_WHITELIST', [])
|
||||
DATE_FORMAT = getattr(configuration, 'DATE_FORMAT', 'N j, Y')
|
||||
DATETIME_FORMAT = getattr(configuration, 'DATETIME_FORMAT', 'N j, Y g:i a')
|
||||
DEBUG = getattr(configuration, 'DEBUG', False)
|
||||
ENFORCE_GLOBAL_UNIQUE = getattr(configuration, 'ENFORCE_GLOBAL_UNIQUE', False)
|
||||
EMAIL = getattr(configuration, 'EMAIL', {})
|
||||
LOGGING = getattr(configuration, 'LOGGING', {})
|
||||
LOGIN_REQUIRED = getattr(configuration, 'LOGIN_REQUIRED', False)
|
||||
MAINTENANCE_MODE = getattr(configuration, 'MAINTENANCE_MODE', False)
|
||||
MAX_PAGE_SIZE = getattr(configuration, 'MAX_PAGE_SIZE', 1000)
|
||||
PAGINATE_COUNT = getattr(configuration, 'PAGINATE_COUNT', 50)
|
||||
PREFER_IPV4 = getattr(configuration, 'PREFER_IPV4', False)
|
||||
NETBOX_USERNAME = getattr(configuration, 'NETBOX_USERNAME', '')
|
||||
NETBOX_PASSWORD = getattr(configuration, 'NETBOX_PASSWORD', '')
|
||||
SHORT_DATE_FORMAT = getattr(configuration, 'SHORT_DATE_FORMAT', 'Y-m-d')
|
||||
SHORT_DATETIME_FORMAT = getattr(configuration, 'SHORT_DATETIME_FORMAT', 'Y-m-d H:i')
|
||||
SHORT_TIME_FORMAT = getattr(configuration, 'SHORT_TIME_FORMAT', 'H:i:s')
|
||||
TIME_FORMAT = getattr(configuration, 'TIME_FORMAT', 'g:i a')
|
||||
TIME_ZONE = getattr(configuration, 'TIME_ZONE', 'UTC')
|
||||
|
||||
DATE_FORMAT = getattr(configuration, 'DATE_FORMAT', 'N j, Y')
|
||||
SHORT_DATE_FORMAT = getattr(configuration, 'SHORT_DATE_FORMAT', 'Y-m-d')
|
||||
TIME_FORMAT = getattr(configuration, 'TIME_FORMAT', 'g:i a')
|
||||
SHORT_TIME_FORMAT = getattr(configuration, 'SHORT_TIME_FORMAT', 'H:i:s')
|
||||
DATETIME_FORMAT = getattr(configuration, 'DATETIME_FORMAT', 'N j, Y g:i a')
|
||||
SHORT_DATETIME_FORMAT = getattr(configuration, 'SHORT_DATETIME_FORMAT', 'Y-m-d H:i')
|
||||
BANNER_TOP = getattr(configuration, 'BANNER_TOP', False)
|
||||
BANNER_BOTTOM = getattr(configuration, 'BANNER_BOTTOM', False)
|
||||
PREFER_IPV4 = getattr(configuration, 'PREFER_IPV4', False)
|
||||
ENFORCE_GLOBAL_UNIQUE = getattr(configuration, 'ENFORCE_GLOBAL_UNIQUE', False)
|
||||
CORS_ORIGIN_ALLOW_ALL = getattr(configuration, 'CORS_ORIGIN_ALLOW_ALL', False)
|
||||
CORS_ORIGIN_WHITELIST = getattr(configuration, 'CORS_ORIGIN_WHITELIST', [])
|
||||
CORS_ORIGIN_REGEX_WHITELIST = getattr(configuration, 'CORS_ORIGIN_REGEX_WHITELIST', [])
|
||||
CSRF_TRUSTED_ORIGINS = ALLOWED_HOSTS
|
||||
|
||||
# Attempt to import LDAP configuration if it has been defined
|
||||
@@ -211,7 +208,7 @@ REST_FRAMEWORK = {
|
||||
'DEFAULT_FILTER_BACKENDS': (
|
||||
'rest_framework.filters.DjangoFilterBackend',
|
||||
),
|
||||
'DEFAULT_PAGINATION_CLASS': 'utilities.api.OptionalLimitOffsetPagination',
|
||||
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
|
||||
'DEFAULT_PERMISSION_CLASSES': (
|
||||
'utilities.api.TokenPermissions',
|
||||
),
|
||||
|
||||
@@ -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('');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -30,7 +30,6 @@ class SecretRoleViewSet(ModelViewSet):
|
||||
queryset = SecretRole.objects.all()
|
||||
serializer_class = serializers.SecretRoleSerializer
|
||||
permission_classes = [IsAuthenticated]
|
||||
filter_class = filters.SecretRoleFilter
|
||||
|
||||
|
||||
#
|
||||
|
||||
@@ -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,11 +27,12 @@ class SecretFilter(django_filters.FilterSet):
|
||||
label='Role (slug)',
|
||||
)
|
||||
device_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='device',
|
||||
queryset=Device.objects.all(),
|
||||
label='Device (ID)',
|
||||
)
|
||||
device = django_filters.ModelMultipleChoiceFilter(
|
||||
name='device__name',
|
||||
name='device',
|
||||
queryset=Device.objects.all(),
|
||||
to_field_name='name',
|
||||
label='Device (name)',
|
||||
|
||||
@@ -7,7 +7,7 @@ from django import forms
|
||||
from django.db.models import Count
|
||||
|
||||
from dcim.models import Device
|
||||
from utilities.forms import BootstrapMixin, BulkEditForm, FilterChoiceField, FlexibleModelChoiceField, SlugField
|
||||
from utilities.forms import BootstrapMixin, BulkEditForm, BulkImportForm, CSVDataField, FilterChoiceField, SlugField
|
||||
from .models import Secret, SecretRole, UserKey
|
||||
|
||||
|
||||
@@ -65,40 +65,27 @@ class SecretForm(BootstrapMixin, forms.ModelForm):
|
||||
})
|
||||
|
||||
|
||||
class SecretCSVForm(forms.ModelForm):
|
||||
device = FlexibleModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
to_field_name='name',
|
||||
help_text='Device name or ID',
|
||||
error_messages={
|
||||
'invalid_choice': 'Device not found.',
|
||||
}
|
||||
)
|
||||
role = forms.ModelChoiceField(
|
||||
queryset=SecretRole.objects.all(),
|
||||
to_field_name='name',
|
||||
help_text='Name of assigned role',
|
||||
error_messages={
|
||||
'invalid_choice': 'Invalid secret role.',
|
||||
}
|
||||
)
|
||||
plaintext = forms.CharField(
|
||||
help_text='Plaintext secret data'
|
||||
)
|
||||
class SecretFromCSVForm(forms.ModelForm):
|
||||
device = forms.ModelChoiceField(queryset=Device.objects.all(), required=False, to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Device not found.'})
|
||||
role = forms.ModelChoiceField(queryset=SecretRole.objects.all(), to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Invalid secret role.'})
|
||||
plaintext = forms.CharField()
|
||||
|
||||
class Meta:
|
||||
model = Secret
|
||||
fields = ['device', 'role', 'name', 'plaintext']
|
||||
help_texts = {
|
||||
'name': 'Name or username',
|
||||
}
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
s = super(SecretCSVForm, self).save(*args, **kwargs)
|
||||
s = super(SecretFromCSVForm, self).save(*args, **kwargs)
|
||||
s.plaintext = str(self.cleaned_data['plaintext'])
|
||||
return s
|
||||
|
||||
|
||||
class SecretImportForm(BootstrapMixin, BulkImportForm):
|
||||
csv = CSVDataField(csv_form=SecretFromCSVForm, widget=forms.Textarea(attrs={'class': 'requires-session-key'}))
|
||||
|
||||
|
||||
class SecretBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(queryset=Secret.objects.all(), widget=forms.MultipleHiddenInput)
|
||||
role = forms.ModelChoiceField(queryset=SecretRole.objects.all(), required=False)
|
||||
|
||||
@@ -291,7 +291,6 @@ class Secret(CreatedUpdatedModel):
|
||||
hash = models.CharField(max_length=128, editable=False)
|
||||
|
||||
plaintext = None
|
||||
csv_headers = ['device', 'role', 'name', 'plaintext']
|
||||
|
||||
class Meta:
|
||||
ordering = ['device', 'role', 'name']
|
||||
|
||||
@@ -10,13 +10,13 @@ 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'),
|
||||
|
||||
# Secrets
|
||||
url(r'^secrets/$', views.SecretListView.as_view(), name='secret_list'),
|
||||
url(r'^secrets/import/$', views.SecretBulkImportView.as_view(), name='secret_import'),
|
||||
url(r'^secrets/import/$', views.secret_import, name='secret_import'),
|
||||
url(r'^secrets/edit/$', views.SecretBulkEditView.as_view(), name='secret_bulk_edit'),
|
||||
url(r'^secrets/delete/$', views.SecretBulkDeleteView.as_view(), name='secret_bulk_delete'),
|
||||
url(r'^secrets/(?P<pk>\d+)/$', views.SecretView.as_view(), name='secret'),
|
||||
|
||||
@@ -12,9 +12,7 @@ from django.utils.decorators import method_decorator
|
||||
from django.views.generic import View
|
||||
|
||||
from dcim.models import Device
|
||||
from utilities.views import (
|
||||
BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
|
||||
)
|
||||
from utilities.views import BulkDeleteView, BulkEditView, ObjectDeleteView, ObjectEditView, ObjectListView
|
||||
from . import filters, forms, tables
|
||||
from .decorators import userkey_required
|
||||
from .models import SecretRole, Secret, SessionKey
|
||||
@@ -40,8 +38,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 +47,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
|
||||
@@ -191,50 +185,58 @@ class SecretDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
default_return_url = 'secrets:secret_list'
|
||||
|
||||
|
||||
class SecretBulkImportView(BulkImportView):
|
||||
permission_required = 'ipam.add_vlan'
|
||||
model_form = forms.SecretCSVForm
|
||||
table = tables.SecretTable
|
||||
default_return_url = 'secrets:secret_list'
|
||||
@permission_required('secrets.add_secret')
|
||||
@userkey_required()
|
||||
def secret_import(request):
|
||||
|
||||
master_key = None
|
||||
session_key = request.COOKIES.get('session_key', None)
|
||||
|
||||
def _save_obj(self, obj_form):
|
||||
"""
|
||||
Encrypt each object before saving it to the database.
|
||||
"""
|
||||
obj = obj_form.save(commit=False)
|
||||
obj.encrypt(self.master_key)
|
||||
obj.save()
|
||||
return obj
|
||||
if request.method == 'POST':
|
||||
form = forms.SecretImportForm(request.POST)
|
||||
|
||||
def post(self, request):
|
||||
if session_key is None:
|
||||
form.add_error(None, "No session key was provided with the request. Unable to encrypt secret data.")
|
||||
|
||||
# Grab the session key from cookies.
|
||||
session_key = request.COOKIES.get('session_key')
|
||||
if session_key:
|
||||
if form.is_valid():
|
||||
|
||||
# Attempt to derive the master key using the provided session key.
|
||||
new_secrets = []
|
||||
|
||||
session_key = base64.b64decode(session_key)
|
||||
master_key = None
|
||||
try:
|
||||
sk = SessionKey.objects.get(userkey__user=request.user)
|
||||
self.master_key = sk.get_master_key(base64.b64decode(session_key))
|
||||
master_key = sk.get_master_key(session_key)
|
||||
except SessionKey.DoesNotExist:
|
||||
messages.error(request, "No session key found for this user.")
|
||||
form.add_error(None, "No session key found for this user.")
|
||||
|
||||
if self.master_key is not None:
|
||||
return super(SecretBulkImportView, self).post(request)
|
||||
if master_key is None:
|
||||
form.add_error(None, "Invalid private key! Unable to encrypt secret data.")
|
||||
else:
|
||||
messages.error(request, "Invalid private key! Unable to encrypt secret data.")
|
||||
try:
|
||||
with transaction.atomic():
|
||||
for secret in form.cleaned_data['csv']:
|
||||
secret.encrypt(master_key)
|
||||
secret.save()
|
||||
new_secrets.append(secret)
|
||||
|
||||
else:
|
||||
messages.error(request, "No session key was provided with the request. Unable to encrypt secret data.")
|
||||
table = tables.SecretTable(new_secrets)
|
||||
messages.success(request, "Imported {} new secrets.".format(len(new_secrets)))
|
||||
|
||||
return render(request, self.template_name, {
|
||||
'form': self._import_form(request.POST),
|
||||
'fields': self.model_form().fields,
|
||||
'obj_type': self.model_form._meta.model._meta.verbose_name,
|
||||
'return_url': self.default_return_url,
|
||||
})
|
||||
return render(request, 'import_success.html', {
|
||||
'table': table,
|
||||
'return_url': 'secrets:secret_list',
|
||||
})
|
||||
|
||||
except IntegrityError as e:
|
||||
form.add_error('csv', "Record {}: {}".format(len(new_secrets) + 1, e.__cause__))
|
||||
|
||||
else:
|
||||
form = forms.SecretImportForm()
|
||||
|
||||
return render(request, 'secrets/secret_import.html', {
|
||||
'form': form,
|
||||
'return_url': 'secrets:secret_list',
|
||||
})
|
||||
|
||||
|
||||
class SecretBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
|
||||
55
netbox/templates/circuits/circuit_import.html
Normal file
55
netbox/templates/circuits/circuit_import.html
Normal file
@@ -0,0 +1,55 @@
|
||||
{% extends 'utilities/obj_import.html' %}
|
||||
|
||||
{% block title %}Circuit Import{% endblock %}
|
||||
|
||||
{% block instructions %}
|
||||
<h4>CSV Format</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Description</th>
|
||||
<th>Example</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Circuit ID</td>
|
||||
<td>Alphanumeric circuit identifier</td>
|
||||
<td>IC-603122</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Provider</td>
|
||||
<td>Name of circuit provider</td>
|
||||
<td>TeliaSonera</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Type</td>
|
||||
<td>Circuit type</td>
|
||||
<td>Transit</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Tenant</td>
|
||||
<td>Name of tenant (optional)</td>
|
||||
<td>Strickland Propane</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Install Date</td>
|
||||
<td>Date in YYYY-MM-DD format (optional)</td>
|
||||
<td>2016-02-23</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Commit rate</td>
|
||||
<td>Commited rate in Kbps (optional)</td>
|
||||
<td>2000</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Description</td>
|
||||
<td>Short description (optional)</td>
|
||||
<td>Primary for voice</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Example</h4>
|
||||
<pre>IC-603122,TeliaSonera,Transit,Strickland Propane,2016-02-23,2000,Primary for voice</pre>
|
||||
{% endblock %}
|
||||
@@ -181,7 +181,7 @@
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="6" class="text-muted">None</td>
|
||||
<td class="text-muted">None</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
|
||||
45
netbox/templates/circuits/provider_import.html
Normal file
45
netbox/templates/circuits/provider_import.html
Normal file
@@ -0,0 +1,45 @@
|
||||
{% extends 'utilities/obj_import.html' %}
|
||||
|
||||
{% block title %}Provider Import{% endblock %}
|
||||
|
||||
{% block instructions %}
|
||||
<h4>CSV Format</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Description</th>
|
||||
<th>Example</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>Provider's proper name</td>
|
||||
<td>Level 3</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Slug</td>
|
||||
<td>URL-friendly name</td>
|
||||
<td>level3</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>ASN</td>
|
||||
<td>Autonomous system number (optional)</td>
|
||||
<td>3356</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Account</td>
|
||||
<td>Account number (optional)</td>
|
||||
<td>08931544</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Portal URL</td>
|
||||
<td>Customer service portal URL (optional)</td>
|
||||
<td>https://mylevel3.net</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Example</h4>
|
||||
<pre>Level 3,level3,3356,08931544,https://mylevel3.net</pre>
|
||||
{% endblock %}
|
||||
@@ -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 %}
|
||||
45
netbox/templates/dcim/console_connections_import.html
Normal file
45
netbox/templates/dcim/console_connections_import.html
Normal file
@@ -0,0 +1,45 @@
|
||||
{% extends 'utilities/obj_import.html' %}
|
||||
|
||||
{% block title %}Console Connections Import{% endblock %}
|
||||
|
||||
{% block instructions %}
|
||||
<h4>CSV Format</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Description</th>
|
||||
<th>Example</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Console server</td>
|
||||
<td>Device name or {ID}</td>
|
||||
<td>abc1-cs3</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Console server port</td>
|
||||
<td>Full CS port name</td>
|
||||
<td>Port 35</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Device</td>
|
||||
<td>Device name or {ID}</td>
|
||||
<td>abc1-switch7</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Console Port</td>
|
||||
<td>Console port name</td>
|
||||
<td>Console</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Connection Status</td>
|
||||
<td>"planned" or "connected"</td>
|
||||
<td>planned</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Example</h4>
|
||||
<pre>abc1-cs3,Port 35,abc1-switch7,Console,planned</pre>
|
||||
{% endblock %}
|
||||
@@ -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')
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,5 +1,103 @@
|
||||
{% extends 'utilities/obj_import.html' %}
|
||||
{% extends '_base.html' %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block tabs %}
|
||||
{% include 'dcim/inc/device_import_header.html' %}
|
||||
{% block title %}Device Import{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% include 'dcim/inc/device_import_header.html' %}
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<form action="." method="post" class="form">
|
||||
{% csrf_token %}
|
||||
{% render_form form %}
|
||||
<div class="form-group">
|
||||
<div class="col-md-12 text-right">
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
{% if return_url %}
|
||||
<a href="{% url return_url %}" class="btn btn-default">Cancel</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<h4>CSV Format</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Description</th>
|
||||
<th>Example</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>Device name (optional)</td>
|
||||
<td>rack101_sw1</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Device role</td>
|
||||
<td>Functional role of device</td>
|
||||
<td>ToR Switch</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Tenant</td>
|
||||
<td>Name of tenant (optional)</td>
|
||||
<td>Pied Piper</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Device manufacturer</td>
|
||||
<td>Hardware manufacturer</td>
|
||||
<td>Juniper</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Device model</td>
|
||||
<td>Hardware model</td>
|
||||
<td>EX4300-48T</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Platform</td>
|
||||
<td>Software running on device (optional)</td>
|
||||
<td>Juniper Junos</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Serial number</td>
|
||||
<td>Physical serial number (optional)</td>
|
||||
<td>CAB00577291</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Asset tag</td>
|
||||
<td>Unique alphanumeric tag (optional)</td>
|
||||
<td>ABC123456</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Status</td>
|
||||
<td>Current status</td>
|
||||
<td>Active</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Site</td>
|
||||
<td>Site name</td>
|
||||
<td>Ashburn-VA</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Rack</td>
|
||||
<td>Rack name (optional)</td>
|
||||
<td>R101</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Position (U)</td>
|
||||
<td>Lowest-numbered rack unit occupied by the device (optional)</td>
|
||||
<td>21</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Face</td>
|
||||
<td>Rack face; front or rear (required if position is set)</td>
|
||||
<td>Rear</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Example</h4>
|
||||
<pre>rack101_sw1,ToR Switch,Pied Piper,Juniper,EX4300-48T,Juniper Junos,CAB00577291,ABC123456,Active,Ashburn-VA,R101,21,Rear</pre>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,5 +1,93 @@
|
||||
{% extends 'utilities/obj_import.html' %}
|
||||
{% extends '_base.html' %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block tabs %}
|
||||
{% include 'dcim/inc/device_import_header.html' with active_tab='child_import' %}
|
||||
{% block title %}Device Import{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% include 'dcim/inc/device_import_header.html' with active_tab='child_import' %}
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<form action="." method="post" class="form">
|
||||
{% csrf_token %}
|
||||
{% render_form form %}
|
||||
<div class="form-group">
|
||||
<div class="col-md-12 text-right">
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
{% if return_url %}
|
||||
<a href="{% url return_url %}" class="btn btn-default">Cancel</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<h4>CSV Format</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Description</th>
|
||||
<th>Example</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>Device name (optional)</td>
|
||||
<td>Blade12</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Device role</td>
|
||||
<td>Functional role of device</td>
|
||||
<td>Blade Server</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Tenant</td>
|
||||
<td>Name of tenant (optional)</td>
|
||||
<td>Pied Piper</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Device manufacturer</td>
|
||||
<td>Hardware manufacturer</td>
|
||||
<td>Dell</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Device model</td>
|
||||
<td>Hardware model</td>
|
||||
<td>BS2000T</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Platform</td>
|
||||
<td>Software running on device (optional)</td>
|
||||
<td>Linux</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Serial number</td>
|
||||
<td>Physical serial number (optional)</td>
|
||||
<td>CAB00577291</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Asset tag</td>
|
||||
<td>Unique alphanumeric tag (optional)</td>
|
||||
<td>ABC123456</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Status</td>
|
||||
<td>Current status</td>
|
||||
<td>Active</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Parent device</td>
|
||||
<td>Parent device</td>
|
||||
<td>Server101</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Device bay</td>
|
||||
<td>Device bay name</td>
|
||||
<td>Slot 4</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Example</h4>
|
||||
<pre>Blade12,Blade Server,Pied Piper,Dell,BS2000T,Linux,CAB00577291,ABC123456,Active,Server101,Slot4</pre>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
<h1>Device Import</h1>
|
||||
<ul class="nav nav-tabs" style="margin-bottom: 20px">
|
||||
<li role="presentation"{% if not active_tab %} class="active"{% endif %}><a href="{% url 'dcim:device_import' %}">Racked Devices</a></li>
|
||||
<li role="presentation"{% if active_tab == 'child_import' %} class="active"{% endif %}><a href="{% url 'dcim:device_import_child' %}">Child Devices</a></li>
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
<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 %}
|
||||
{% if perms.dcim.change_inventory_item %}
|
||||
<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>
|
||||
{% endif %}
|
||||
{% if perms.dcim.delete_inventoryitem %}
|
||||
{% if perms.dcim.delete_inventory_item %}
|
||||
<a href="{% url 'dcim:inventoryitem_delete' pk=item.pk %}?return_url={% url 'dcim:device_inventory' pk=device.pk %}" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash" aria-hidden="true"></span></a>
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
45
netbox/templates/dcim/interface_connections_import.html
Normal file
45
netbox/templates/dcim/interface_connections_import.html
Normal file
@@ -0,0 +1,45 @@
|
||||
{% extends 'utilities/obj_import.html' %}
|
||||
|
||||
{% block title %}Interface Connections Import{% endblock %}
|
||||
|
||||
{% block instructions %}
|
||||
<h4>CSV Format</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Description</th>
|
||||
<th>Example</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Device A</td>
|
||||
<td>Device name or {ID}</td>
|
||||
<td>abc1-core1</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Interface A</td>
|
||||
<td>Interface name</td>
|
||||
<td>xe-0/0/6</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Device B</td>
|
||||
<td>Device name or {ID}</td>
|
||||
<td>abc1-switch7</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Interface B</td>
|
||||
<td>Interface name</td>
|
||||
<td>xe-0/0/0</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Connection Status</td>
|
||||
<td>"planned" or "connected"</td>
|
||||
<td>planned</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Example</h4>
|
||||
<pre>abc1-core1,xe-0/0/6,abc1-switch7,xe-0/0/0,planned</pre>
|
||||
{% endblock %}
|
||||
45
netbox/templates/dcim/power_connections_import.html
Normal file
45
netbox/templates/dcim/power_connections_import.html
Normal file
@@ -0,0 +1,45 @@
|
||||
{% extends 'utilities/obj_import.html' %}
|
||||
|
||||
{% block title %}Power Connections Import{% endblock %}
|
||||
|
||||
{% block instructions %}
|
||||
<h4>CSV Format</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Description</th>
|
||||
<th>Example</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>PDU</td>
|
||||
<td>Device name or {ID}</td>
|
||||
<td>abc1-pdu1</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Power Outlet</td>
|
||||
<td>Power outlet name</td>
|
||||
<td>AC4</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Device</td>
|
||||
<td>Device name or {ID}</td>
|
||||
<td>abc1-switch7</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Power Port</td>
|
||||
<td>Power port name</td>
|
||||
<td>PSU0</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Connection Status</td>
|
||||
<td>"planned" or "connected"</td>
|
||||
<td>connected</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Example</h4>
|
||||
<pre>abc1-pdu1,AC4,abc1-switch7,PSU0,connected</pre>
|
||||
{% endblock %}
|
||||
@@ -14,8 +14,7 @@
|
||||
{% for rack in page %}
|
||||
<div style="display: inline-block; width: 266px">
|
||||
<div class="rack_header">
|
||||
<strong><a href="{% url 'dcim:rack' pk=rack.pk %}">{{ rack.name|truncatechars:"25" }}</a></strong>
|
||||
<p><small class="text-muted">{{ rack.facility_id|truncatechars:"30" }}</small></p>
|
||||
<h4><a href="{% url 'dcim:rack' pk=rack.pk %}">{{ rack.name }}</a></h4>
|
||||
</div>
|
||||
{% if face_id %}
|
||||
{% include 'dcim/inc/rack_elevation.html' with primary_face=rack.get_rear_elevation secondary_face=rack.get_front_elevation face_id=1 %}
|
||||
@@ -24,8 +23,7 @@
|
||||
{% endif %}
|
||||
<div class="clearfix"></div>
|
||||
<div class="rack_header">
|
||||
<strong><a href="{% url 'dcim:rack' pk=rack.pk %}">{{ rack.name|truncatechars:"25" }}</a></strong>
|
||||
<p><small class="text-muted">{{ rack.facility_id|truncatechars:"30" }}</small></p>
|
||||
<h4><a href="{% url 'dcim:rack' pk=rack.pk %}">{{ rack.name }}</a></h4>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
70
netbox/templates/dcim/rack_import.html
Normal file
70
netbox/templates/dcim/rack_import.html
Normal file
@@ -0,0 +1,70 @@
|
||||
{% extends 'utilities/obj_import.html' %}
|
||||
|
||||
{% block title %}Rack Import{% endblock %}
|
||||
|
||||
{% block instructions %}
|
||||
<h4>CSV Format</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Description</th>
|
||||
<th>Example</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Site</td>
|
||||
<td>Name of the assigned site</td>
|
||||
<td>DC-4</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Group</td>
|
||||
<td>Rack group name (optional)</td>
|
||||
<td>Cage 1400</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>Internal rack name</td>
|
||||
<td>R101</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Facility ID</td>
|
||||
<td>Rack ID assigned by the facility (optional)</td>
|
||||
<td>J12.100</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Tenant</td>
|
||||
<td>Name of tenant (optional)</td>
|
||||
<td>Pied Piper</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Role</td>
|
||||
<td>Functional role (optional)</td>
|
||||
<td>Compute</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Type</td>
|
||||
<td>Rack type (optional)</td>
|
||||
<td>4-post cabinet</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Width</td>
|
||||
<td>Rail-to-rail width (19 or 23 inches)</td>
|
||||
<td>19</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Height</td>
|
||||
<td>Height in rack units</td>
|
||||
<td>42</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Descending units</td>
|
||||
<td>Units are numbered top-to-bottom</td>
|
||||
<td>False</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Example</h4>
|
||||
<pre>DC-4,Cage 1400,R101,J12.100,Pied Piper,Compute,4-post cabinet,19,42,False</pre>
|
||||
{% endblock %}
|
||||
81
netbox/templates/dcim/site_import.html
Normal file
81
netbox/templates/dcim/site_import.html
Normal file
@@ -0,0 +1,81 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}Site Import{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Site Import</h1>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<form action="." method="post" class="form">
|
||||
{% csrf_token %}
|
||||
{% render_form form %}
|
||||
<div class="form-group">
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
<a href="{% url return_url %}" class="btn btn-default">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h4>CSV Format</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Description</th>
|
||||
<th>Example</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>Site's proper name</td>
|
||||
<td>ASH-4 South</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Slug</td>
|
||||
<td>URL-friendly name</td>
|
||||
<td>ash4-south</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Region</td>
|
||||
<td>Name of region (optional)</td>
|
||||
<td>North America</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Tenant</td>
|
||||
<td>Name of tenant (optional)</td>
|
||||
<td>Pied Piper</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Facility</td>
|
||||
<td>Name of the hosting facility (optional)</td>
|
||||
<td>Equinix DC6</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>ASN</td>
|
||||
<td>Autonomous system number (optional)</td>
|
||||
<td>65000</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Contact Name</td>
|
||||
<td>Name of administrative contact (optional)</td>
|
||||
<td>Hank Hill</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Contact Phone</td>
|
||||
<td>Phone number (optional)</td>
|
||||
<td>+1-214-555-1234</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Contact E-mail</td>
|
||||
<td>E-mail address (optional)</td>
|
||||
<td>hhill@example.com</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Example</h4>
|
||||
<pre>ASH-4 South,ash4-south,North America,Pied Piper,Equinix DC6,65000,Hank Hill,+1-214-555-1234,hhill@example.com</pre>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -57,12 +57,6 @@
|
||||
<a href="{% url 'ipam:aggregate_list' %}?rir={{ aggregate.rir.slug }}">{{ aggregate.rir }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Utilization</td>
|
||||
<td>
|
||||
{{ aggregate.get_utilization }}%
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Date Added</td>
|
||||
<td>
|
||||
|
||||
40
netbox/templates/ipam/aggregate_import.html
Normal file
40
netbox/templates/ipam/aggregate_import.html
Normal file
@@ -0,0 +1,40 @@
|
||||
{% extends 'utilities/obj_import.html' %}
|
||||
|
||||
{% block title %}Aggregate Import{% endblock %}
|
||||
|
||||
{% block instructions %}
|
||||
<h4>CSV Format</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Description</th>
|
||||
<th>Example</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Prefix</td>
|
||||
<td>IPv4 or IPv6 network</td>
|
||||
<td>172.16.0.0/12</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>RIR</td>
|
||||
<td>Name of RIR</td>
|
||||
<td>RFC 1918</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Date Added</td>
|
||||
<td>Date in YYYY-MM-DD format (optional)</td>
|
||||
<td>2016-02-23</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Description</td>
|
||||
<td>Short description (optional)</td>
|
||||
<td>Private IPv4 space</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Example</h4>
|
||||
<pre>172.16.0.0/12,RFC 1918,2016-02-23,Private IPv4 space</pre>
|
||||
{% endblock %}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
60
netbox/templates/ipam/ipaddress_import.html
Normal file
60
netbox/templates/ipam/ipaddress_import.html
Normal file
@@ -0,0 +1,60 @@
|
||||
{% extends 'utilities/obj_import.html' %}
|
||||
|
||||
{% block title %}IP Address Import{% endblock %}
|
||||
|
||||
{% block instructions %}
|
||||
<h4>CSV Format</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Description</th>
|
||||
<th>Example</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Address</td>
|
||||
<td>IPv4 or IPv6 address</td>
|
||||
<td>192.0.2.42/24</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>VRF</td>
|
||||
<td>VRF route distinguisher (optional)</td>
|
||||
<td>65000:123</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Tenant</td>
|
||||
<td>Name of tenant (optional)</td>
|
||||
<td>ABC01</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Status</td>
|
||||
<td>Current status</td>
|
||||
<td>Active</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Device</td>
|
||||
<td>Device name (optional)</td>
|
||||
<td>switch12</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Interface</td>
|
||||
<td>Interface name (optional)</td>
|
||||
<td>ge-0/0/31</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Is Primary</td>
|
||||
<td>If "true", IP will be primary for device (optional)</td>
|
||||
<td>True</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Description</td>
|
||||
<td>Short description (optional)</td>
|
||||
<td>Management IP</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Example</h4>
|
||||
<pre>192.0.2.42/24,65000:123,ABC01,Active,switch12,ge-0/0/31,True,Management IP</pre>
|
||||
{% endblock %}
|
||||
@@ -121,8 +121,8 @@
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Utilization</td>
|
||||
<td><a href="{% url 'ipam:prefix_ipaddresses' pk=prefix.pk %}">{{ ipaddress_count }} IP addresses</a> ({{ prefix.get_utilization }}%)</td>
|
||||
<td>IP Addresses</td>
|
||||
<td><a href="{% url 'ipam:prefix_ipaddresses' pk=prefix.pk %}">{{ ipaddress_count }}</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -8,19 +8,13 @@
|
||||
{% render_field form.prefix %}
|
||||
{% render_field form.status %}
|
||||
{% render_field form.vrf %}
|
||||
{% render_field form.site %}
|
||||
{% render_field form.vlan %}
|
||||
{% render_field form.role %}
|
||||
{% render_field form.description %}
|
||||
{% render_field form.is_pool %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>Site/VLAN Assignment</strong></div>
|
||||
<div class="panel-body">
|
||||
{% render_field form.site %}
|
||||
{% render_field form.vlan_group %}
|
||||
{% render_field form.vlan %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>Tenancy</strong></div>
|
||||
<div class="panel-body">
|
||||
|
||||
70
netbox/templates/ipam/prefix_import.html
Normal file
70
netbox/templates/ipam/prefix_import.html
Normal file
@@ -0,0 +1,70 @@
|
||||
{% extends 'utilities/obj_import.html' %}
|
||||
|
||||
{% block title %}Prefix Import{% endblock %}
|
||||
|
||||
{% block instructions %}
|
||||
<h4>CSV Format</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Description</th>
|
||||
<th>Example</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Prefix</td>
|
||||
<td>IPv4 or IPv6 network</td>
|
||||
<td>192.168.42.0/24</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>VRF</td>
|
||||
<td>VRF route distinguisher (optional)</td>
|
||||
<td>65000:123</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Tenant</td>
|
||||
<td>Name of tenant (optional)</td>
|
||||
<td>ABC01</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Site</td>
|
||||
<td>Name of assigned site (optional)</td>
|
||||
<td>HQ</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>VLAN Group</td>
|
||||
<td>Name of group for VLAN selection (optional)</td>
|
||||
<td>Customers</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>VLAN ID</td>
|
||||
<td>Numeric VLAN ID (optional)</td>
|
||||
<td>801</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Status</td>
|
||||
<td>Current status</td>
|
||||
<td>Active</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Role</td>
|
||||
<td>Functional role (optional)</td>
|
||||
<td>Customer</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Is a pool</td>
|
||||
<td>True if all IPs are considered usable</td>
|
||||
<td>False</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Description</td>
|
||||
<td>Short description (optional)</td>
|
||||
<td>7th floor WiFi</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Example</h4>
|
||||
<pre>192.168.42.0/24,65000:123,ABC01,HQ,Customers,801,Active,Customer,False,7th floor WiFi</pre>
|
||||
{% endblock %}
|
||||
60
netbox/templates/ipam/vlan_import.html
Normal file
60
netbox/templates/ipam/vlan_import.html
Normal file
@@ -0,0 +1,60 @@
|
||||
{% extends 'utilities/obj_import.html' %}
|
||||
|
||||
{% block title %}VLAN Import{% endblock %}
|
||||
|
||||
{% block instructions %}
|
||||
<h4>CSV Format</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Description</th>
|
||||
<th>Example</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Site</td>
|
||||
<td>Name of assigned site (optional)</td>
|
||||
<td>LAS2</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Group</td>
|
||||
<td>Name of VLAN group (optional)</td>
|
||||
<td>Backend Network</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>ID</td>
|
||||
<td>Configured VLAN ID</td>
|
||||
<td>1400</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>Configured VLAN name</td>
|
||||
<td>Cameras</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Tenant</td>
|
||||
<td>Name of tenant (optional)</td>
|
||||
<td>Internal</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Status</td>
|
||||
<td>Current status</td>
|
||||
<td>Active</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Role</td>
|
||||
<td>Functional role (optional)</td>
|
||||
<td>Security</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Description</td>
|
||||
<td>Short description (optional)</td>
|
||||
<td>Security team only</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Example</h4>
|
||||
<pre>LAS2,Backend Network,1400,Cameras,Internal,Active,Security,Security team only</pre>
|
||||
{% endblock %}
|
||||
45
netbox/templates/ipam/vrf_import.html
Normal file
45
netbox/templates/ipam/vrf_import.html
Normal file
@@ -0,0 +1,45 @@
|
||||
{% extends 'utilities/obj_import.html' %}
|
||||
|
||||
{% block title %}VRF Import{% endblock %}
|
||||
|
||||
{% block instructions %}
|
||||
<h4>CSV Format</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Description</th>
|
||||
<th>Example</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>Name of VRF</td>
|
||||
<td>Customer_ABC</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>RD</td>
|
||||
<td>Route distinguisher</td>
|
||||
<td>65000:123456</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Tenant</td>
|
||||
<td>Name of tenant (optional)</td>
|
||||
<td>ABC01</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Enforce uniqueness</td>
|
||||
<td>Prevent duplicate prefixes/IP addresses</td>
|
||||
<td>True</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Description</td>
|
||||
<td>Short description (optional)</td>
|
||||
<td>Native VRF for customer ABC</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Example</h4>
|
||||
<pre>Customer_ABC,65000:123456,ABC01,True,Native VRF for customer ABC</pre>
|
||||
{% endblock %}
|
||||
40
netbox/templates/tenancy/tenant_import.html
Normal file
40
netbox/templates/tenancy/tenant_import.html
Normal file
@@ -0,0 +1,40 @@
|
||||
{% extends 'utilities/obj_import.html' %}
|
||||
|
||||
{% block title %}Tenant Import{% endblock %}
|
||||
|
||||
{% block instructions %}
|
||||
<h4>CSV Format</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Description</th>
|
||||
<th>Example</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>Tenant name</td>
|
||||
<td>WIDG01</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Slug</td>
|
||||
<td>URL-friendly name</td>
|
||||
<td>widg01</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Group</td>
|
||||
<td>Tenant group (optional)</td>
|
||||
<td>Customers</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Description</td>
|
||||
<td>Long-form name or other text (optional)</td>
|
||||
<td>Widgets Inc.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Example</h4>
|
||||
<pre>WIDG01,widg01,Customers,Widgets Inc.</pre>
|
||||
{% endblock %}
|
||||
@@ -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 %}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load helpers %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block content %}
|
||||
<h1>{% block title %}{{ obj_type|bettertitle }} Import{% endblock %}</h1>
|
||||
{% block tabs %}{% endblock %}
|
||||
<h1>{% block title %}{% endblock %}</h1>
|
||||
<div class="row">
|
||||
<div class="col-md-7">
|
||||
<div class="col-md-6">
|
||||
{% if form.non_field_errors %}
|
||||
<div class="panel panel-danger">
|
||||
<div class="panel-heading"><strong>Errors</strong></div>
|
||||
@@ -28,33 +26,8 @@
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
{% if fields %}
|
||||
<h4 class="text-center">CSV Format</h4>
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Required</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
{% for name, field in fields.items %}
|
||||
<tr>
|
||||
<td><code>{{ name }}</code></td>
|
||||
<td>{% if field.required %}<i class="glyphicon glyphicon-ok" title="Required"></i>{% endif %}</td>
|
||||
<td>
|
||||
{{ field.help_text|default:field.label }}
|
||||
{% if field.choices %}
|
||||
<br /><small class="text-muted">Choices: {{ field.choices|example_choices }}</small>
|
||||
{% elif field|widget_type == 'dateinput' %}
|
||||
<br /><small class="text-muted">Format: YYYY-MM-DD</small>
|
||||
{% elif field|widget_type == 'checkboxinput' %}
|
||||
<br /><small class="text-muted">Specify "true" or "false"</small>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% endif %}
|
||||
<div class="col-md-6">
|
||||
{% block instructions %}{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)',
|
||||
)
|
||||
|
||||
@@ -5,7 +5,8 @@ from django.db.models import Count
|
||||
|
||||
from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
|
||||
from utilities.forms import (
|
||||
APISelect, BootstrapMixin, ChainedFieldsMixin, ChainedModelChoiceField, CommentField, FilterChoiceField, SlugField,
|
||||
APISelect, BootstrapMixin, BulkImportForm, ChainedFieldsMixin, ChainedModelChoiceField, CommentField, CSVDataField,
|
||||
FilterChoiceField, SlugField,
|
||||
)
|
||||
from .models import Tenant, TenantGroup
|
||||
|
||||
@@ -35,25 +36,17 @@ class TenantForm(BootstrapMixin, CustomFieldForm):
|
||||
fields = ['name', 'slug', 'group', 'description', 'comments']
|
||||
|
||||
|
||||
class TenantCSVForm(forms.ModelForm):
|
||||
slug = SlugField()
|
||||
group = forms.ModelChoiceField(
|
||||
queryset=TenantGroup.objects.all(),
|
||||
required=False,
|
||||
to_field_name='name',
|
||||
help_text='Name of parent group',
|
||||
error_messages={
|
||||
'invalid_choice': 'Group not found.'
|
||||
}
|
||||
)
|
||||
class TenantFromCSVForm(forms.ModelForm):
|
||||
group = forms.ModelChoiceField(TenantGroup.objects.all(), required=False, to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Group not found.'})
|
||||
|
||||
class Meta:
|
||||
model = Tenant
|
||||
fields = ['name', 'slug', 'group', 'description', 'comments']
|
||||
help_texts = {
|
||||
'name': 'Tenant name',
|
||||
'comments': 'Free-form comments'
|
||||
}
|
||||
fields = ['name', 'slug', 'group', 'description']
|
||||
|
||||
|
||||
class TenantImportForm(BootstrapMixin, BulkImportForm):
|
||||
csv = CSVDataField(csv_form=TenantFromCSVForm)
|
||||
|
||||
|
||||
class TenantBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
|
||||
@@ -41,8 +41,6 @@ class Tenant(CreatedUpdatedModel, CustomFieldModel):
|
||||
comments = models.TextField(blank=True)
|
||||
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
|
||||
|
||||
csv_headers = ['name', 'slug', 'group', 'description']
|
||||
|
||||
class Meta:
|
||||
ordering = ['group', 'name']
|
||||
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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
|
||||
@@ -105,8 +97,9 @@ class TenantDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
|
||||
class TenantBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
permission_required = 'tenancy.add_tenant'
|
||||
model_form = forms.TenantCSVForm
|
||||
form = forms.TenantImportForm
|
||||
table = tables.TenantTable
|
||||
template_name = 'tenancy/tenant_import.html'
|
||||
default_return_url = 'tenancy:tenant_list'
|
||||
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user