mirror of
https://github.com/netbox-community/netbox.git
synced 2026-01-07 04:27:27 -06:00
Compare commits
154 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1bda56ea23 | ||
|
|
c7e9d90321 | ||
|
|
7476194bd1 | ||
|
|
1770c85689 | ||
|
|
e364659c6e | ||
|
|
9e26198afe | ||
|
|
f9b6ddc230 | ||
|
|
0991f94d06 | ||
|
|
32513083b1 | ||
|
|
336cdcddc5 | ||
|
|
4047c1a4e4 | ||
|
|
091cf390d2 | ||
|
|
05aaafc1cf | ||
|
|
5885b833cd | ||
|
|
106627da04 | ||
|
|
d73ea54e08 | ||
|
|
a45bfaf3da | ||
|
|
e85cc0d856 | ||
|
|
0f608f3a15 | ||
|
|
4ad5c6f864 | ||
|
|
be47b6a6c0 | ||
|
|
1f982c94ce | ||
|
|
12472a2612 | ||
|
|
f6a8d32880 | ||
|
|
bb2f86463e | ||
|
|
e8dafc02f7 | ||
|
|
0655834938 | ||
|
|
64a34ced72 | ||
|
|
d0dc505220 | ||
|
|
b2d3f3ff22 | ||
|
|
39730b6834 | ||
|
|
dd1991f2c6 | ||
|
|
2f32e11f53 | ||
|
|
280f55a875 | ||
|
|
dc68be5abf | ||
|
|
1ef90902bd | ||
|
|
6f37e97c67 | ||
|
|
e54c74d972 | ||
|
|
af9fa85cc1 | ||
|
|
74828e1409 | ||
|
|
dc77400ab1 | ||
|
|
2d0638821d | ||
|
|
8a87d60f29 | ||
|
|
530789b733 | ||
|
|
618d75566f | ||
|
|
5f66893038 | ||
|
|
e05d379101 | ||
|
|
41ea433e7c | ||
|
|
bfd7881b7b | ||
|
|
1f9806a480 | ||
|
|
5b43a108bc | ||
|
|
b253c8cc95 | ||
|
|
0fc9ed852e | ||
|
|
175c1f2720 | ||
|
|
a355783377 | ||
|
|
dafdbc9ddb | ||
|
|
14f5204548 | ||
|
|
5233463f0b | ||
|
|
1d4a416100 | ||
|
|
25ee796d5b | ||
|
|
e08107063a | ||
|
|
30d1605007 | ||
|
|
cd5a86bfcf | ||
|
|
a23da9f867 | ||
|
|
d5bb37b552 | ||
|
|
97b67d0f93 | ||
|
|
3f82be7192 | ||
|
|
adfcb5f7b6 | ||
|
|
5940feb64b | ||
|
|
229e6809d8 | ||
|
|
87e5687d03 | ||
|
|
5aba1d9aec | ||
|
|
68ebe85a98 | ||
|
|
789ac5dfd4 | ||
|
|
ceb8fee0cc | ||
|
|
421270f4a6 | ||
|
|
afdf5750b5 | ||
|
|
ea869d4ffc | ||
|
|
9d89eed873 | ||
|
|
c00eea7991 | ||
|
|
88239e0b0d | ||
|
|
ba8f48af65 | ||
|
|
9930e2745f | ||
|
|
da3879e928 | ||
|
|
7195b7c803 | ||
|
|
9b082eea14 | ||
|
|
a16218b311 | ||
|
|
f427c00d94 | ||
|
|
8bcd8c404d | ||
|
|
4d7f9c42c8 | ||
|
|
29a71fd903 | ||
|
|
cd263484c3 | ||
|
|
fcacac7c6f | ||
|
|
78d74261e9 | ||
|
|
16d694734b | ||
|
|
252ab0fbab | ||
|
|
8eb9c451a1 | ||
|
|
469c52be28 | ||
|
|
54fa51eeff | ||
|
|
5456af6867 | ||
|
|
180446c34d | ||
|
|
5c63a499d5 | ||
|
|
3a2c5b318a | ||
|
|
cfff69a715 | ||
|
|
08883d86ef | ||
|
|
8a849ebeff | ||
|
|
05a796faf1 | ||
|
|
9e1d03b383 | ||
|
|
0a929f2971 | ||
|
|
7878992570 | ||
|
|
4f95926cbd | ||
|
|
f3e997ea39 | ||
|
|
2b921c21ff | ||
|
|
50496b1a59 | ||
|
|
9736d63577 | ||
|
|
13add414c4 | ||
|
|
b032bc13db | ||
|
|
aaad428438 | ||
|
|
203895fc7e | ||
|
|
aab1fab445 | ||
|
|
e06221bc89 | ||
|
|
26a13edcf3 | ||
|
|
65b6fe576f | ||
|
|
4671829ad8 | ||
|
|
293be752ca | ||
|
|
0a6e4f31d5 | ||
|
|
e6c4ce51f7 | ||
|
|
3924063060 | ||
|
|
d122f9f700 | ||
|
|
d0649ba815 | ||
|
|
1ec09270a7 | ||
|
|
1ddd7415cb | ||
|
|
ec9d0d4008 | ||
|
|
08c8bd3049 | ||
|
|
2520d9f400 | ||
|
|
0e863ff9ca | ||
|
|
1b78f54c6b | ||
|
|
b732c24ec4 | ||
|
|
af604aba31 | ||
|
|
c82658440f | ||
|
|
7e660d4d8e | ||
|
|
4a8147f8a5 | ||
|
|
583830c652 | ||
|
|
95fdb549d7 | ||
|
|
a598f0e632 | ||
|
|
293dbd8a8b | ||
|
|
f03a378ce0 | ||
|
|
6aae8aee5b | ||
|
|
6d908d3e79 | ||
|
|
d5016c7133 | ||
|
|
b5a1b692bd | ||
|
|
834c396a22 | ||
|
|
bc18d241e8 | ||
|
|
5ff4e3b194 |
@@ -136,3 +136,8 @@ The response will return devices 1 through 100. The URL provided in the `next` a
|
|||||||
"results": [...]
|
"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,6 +83,34 @@ 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
|
## LOGIN_REQUIRED
|
||||||
|
|
||||||
Default: False
|
Default: False
|
||||||
@@ -99,6 +127,14 @@ 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_USERNAME
|
||||||
|
|
||||||
## NETBOX_PASSWORD
|
## NETBOX_PASSWORD
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ Each line of the **device patterns** field represents a hierarchical layer withi
|
|||||||
```
|
```
|
||||||
core-switch-[abcd]
|
core-switch-[abcd]
|
||||||
dist-switch\d
|
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.
|
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,5 +1,4 @@
|
|||||||
This guide explains how to implement LDAP authentication using an external server. User authentication will fall back to
|
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.
|
||||||
built-in Django users in the event of a failure.
|
|
||||||
|
|
||||||
# Requirements
|
# Requirements
|
||||||
|
|
||||||
@@ -29,6 +28,9 @@ Create a file in the same directory as `configuration.py` (typically `netbox/net
|
|||||||
|
|
||||||
## General Server Configuration
|
## 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
|
```python
|
||||||
import ldap
|
import ldap
|
||||||
|
|
||||||
@@ -52,6 +54,9 @@ LDAP_IGNORE_CERT_ERRORS = True
|
|||||||
|
|
||||||
## User Authentication
|
## User Authentication
|
||||||
|
|
||||||
|
!!! info
|
||||||
|
When using Windows Server, `2012 AUTH_LDAP_USER_DN_TEMPLATE` should be set to None.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from django_auth_ldap.config import LDAPSearch
|
from django_auth_ldap.config import LDAPSearch
|
||||||
|
|
||||||
@@ -99,3 +104,16 @@ AUTH_LDAP_FIND_GROUP_PERMS = True
|
|||||||
AUTH_LDAP_CACHE_GROUPS = True
|
AUTH_LDAP_CACHE_GROUPS = True
|
||||||
AUTH_LDAP_GROUP_CACHE_TIMEOUT = 3600
|
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",
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|||||||
33
docs/installation/migrating-to-python3.md
Normal file
33
docs/installation/migrating-to-python3.md
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# Migration
|
||||||
|
|
||||||
|
Remove Python 2 packages
|
||||||
|
|
||||||
|
```no-highlight
|
||||||
|
# apt-get remove --purge -y python-dev python-pip
|
||||||
|
```
|
||||||
|
|
||||||
|
Install Python 3 packages
|
||||||
|
|
||||||
|
```no-highlight
|
||||||
|
# apt-get install -y python3 python3-dev python3-pip
|
||||||
|
```
|
||||||
|
|
||||||
|
Install Python Packages
|
||||||
|
|
||||||
|
```no-highlight
|
||||||
|
# cd /opt/netbox
|
||||||
|
# pip3 install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
Gunicorn Update
|
||||||
|
|
||||||
|
```no-highlight
|
||||||
|
# pip uninstall gunicorn
|
||||||
|
# pip3 install gunicorn
|
||||||
|
```
|
||||||
|
|
||||||
|
Re-install LDAP Module (optional if using LDAP for auth)
|
||||||
|
|
||||||
|
```no-highlight
|
||||||
|
sudo pip3 install django-auth-ldap
|
||||||
|
```
|
||||||
@@ -1,12 +1,11 @@
|
|||||||
# Installation
|
# Installation
|
||||||
|
|
||||||
**Debian/Ubuntu**
|
**Ubuntu**
|
||||||
|
|
||||||
Python 3:
|
Python 3:
|
||||||
|
|
||||||
```no-highlight
|
```no-highlight
|
||||||
# apt-get install -y python3 python3-dev python3-pip libxml2-dev libxslt1-dev libffi-dev graphviz libpq-dev libssl-dev zlib1g-dev
|
# 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:
|
Python 2:
|
||||||
@@ -15,7 +14,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
|
# apt-get install -y python2.7 python-dev python-pip libxml2-dev libxslt1-dev libffi-dev graphviz libpq-dev libssl-dev zlib1g-dev
|
||||||
```
|
```
|
||||||
|
|
||||||
**CentOS/RHEL**
|
**CentOS**
|
||||||
|
|
||||||
Python 3:
|
Python 3:
|
||||||
|
|
||||||
@@ -57,13 +56,13 @@ Create the base directory for the NetBox installation. For this guide, we'll use
|
|||||||
|
|
||||||
If `git` is not already installed, install it:
|
If `git` is not already installed, install it:
|
||||||
|
|
||||||
**Debian/Ubuntu**
|
**Ubuntu**
|
||||||
|
|
||||||
```no-highlight
|
```no-highlight
|
||||||
# apt-get install -y git
|
# apt-get install -y git
|
||||||
```
|
```
|
||||||
|
|
||||||
**CentOS/RHEL**
|
**CentOS**
|
||||||
|
|
||||||
```no-highlight
|
```no-highlight
|
||||||
# yum install -y git
|
# yum install -y git
|
||||||
@@ -98,6 +97,14 @@ Python 2:
|
|||||||
# pip install -r requirements.txt
|
# pip install -r requirements.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### NAPALM Automation
|
||||||
|
|
||||||
|
As of v2.1.0, NetBox supports integration with the [NAPALM automation](https://napalm-automation.net/) library. NAPALM allows NetBox to fetch live data from devices and return it to a requester via its REST API. Installation of NAPALM is optional. To enable it, install the `napalm` package using pip or pip3:
|
||||||
|
|
||||||
|
```no-highlight
|
||||||
|
# pip install napalm
|
||||||
|
```
|
||||||
|
|
||||||
# Configuration
|
# Configuration
|
||||||
|
|
||||||
Move into the NetBox configuration directory and make a copy of `configuration.example.py` named `configuration.py`.
|
Move into the NetBox configuration directory and make a copy of `configuration.example.py` named `configuration.py`.
|
||||||
@@ -150,11 +157,14 @@ You may use the script located at `netbox/generate_secret_key.py` to generate a
|
|||||||
|
|
||||||
# Run Database Migrations
|
# Run Database Migrations
|
||||||
|
|
||||||
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):
|
!!! 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):
|
||||||
|
|
||||||
```no-highlight
|
```no-highlight
|
||||||
# cd /opt/netbox/netbox/
|
# cd /opt/netbox/netbox/
|
||||||
# ./manage.py migrate
|
# python manage.py migrate
|
||||||
Operations to perform:
|
Operations to perform:
|
||||||
Apply all migrations: dcim, sessions, admin, ipam, utilities, auth, circuits, contenttypes, extras, secrets, users
|
Apply all migrations: dcim, sessions, admin, ipam, utilities, auth, circuits, contenttypes, extras, secrets, users
|
||||||
Running migrations:
|
Running migrations:
|
||||||
@@ -172,7 +182,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:
|
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
|
```no-highlight
|
||||||
# ./manage.py createsuperuser
|
# python manage.py createsuperuser
|
||||||
Username: admin
|
Username: admin
|
||||||
Email address: admin@example.com
|
Email address: admin@example.com
|
||||||
Password:
|
Password:
|
||||||
@@ -183,7 +193,7 @@ Superuser created successfully.
|
|||||||
# Collect Static Files
|
# Collect Static Files
|
||||||
|
|
||||||
```no-highlight
|
```no-highlight
|
||||||
# ./manage.py collectstatic --no-input
|
# python manage.py collectstatic --no-input
|
||||||
|
|
||||||
You have requested to collect static files at the destination
|
You have requested to collect static files at the destination
|
||||||
location as specified in your settings:
|
location as specified in your settings:
|
||||||
@@ -204,7 +214,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.
|
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
|
```no-highlight
|
||||||
# ./manage.py loaddata initial_data
|
# python manage.py loaddata initial_data
|
||||||
Installed 43 object(s) from 4 fixture(s)
|
Installed 43 object(s) from 4 fixture(s)
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -213,7 +223,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:
|
At this point, NetBox should be able to run. We can verify this by starting a development instance:
|
||||||
|
|
||||||
```no-highlight
|
```no-highlight
|
||||||
# ./manage.py runserver 0.0.0.0:8000 --insecure
|
# python manage.py runserver 0.0.0.0:8000 --insecure
|
||||||
Performing system checks...
|
Performing system checks...
|
||||||
|
|
||||||
System check identified no issues (0 silenced).
|
System check identified no issues (0 silenced).
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
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).)
|
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
|
# Installation
|
||||||
|
|
||||||
**Debian/Ubuntu**
|
**Ubuntu**
|
||||||
|
|
||||||
```no-highlight
|
```no-highlight
|
||||||
# apt-get update
|
# apt-get update
|
||||||
# apt-get install -y postgresql libpq-dev
|
# apt-get install -y postgresql libpq-dev
|
||||||
```
|
```
|
||||||
|
|
||||||
**CentOS/RHEL**
|
**CentOS**
|
||||||
|
|
||||||
```no-highlight
|
```no-highlight
|
||||||
# yum install -y postgresql postgresql-server postgresql-devel
|
# yum install -y postgresql postgresql-server postgresql-devel
|
||||||
|
|||||||
@@ -52,6 +52,13 @@ Once the new code is in place, run the upgrade script (which may need to be run
|
|||||||
# ./upgrade.sh
|
# ./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:
|
This script:
|
||||||
|
|
||||||
* Installs or upgrades any new required Python packages
|
* Installs or upgrades any new required Python packages
|
||||||
@@ -64,7 +71,7 @@ This script:
|
|||||||
Your models have changes that are not yet reflected in a migration, and so won't be applied.
|
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.
|
Run 'manage.py makemigrations' to make new migrations, and then re-run 'manage.py migrate' to apply them.
|
||||||
|
|
||||||
This may occur due to semantic differences in environment, and can be safely ignored. Never attempt to create new migrations unless you are inentionally modifying the database schema.
|
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.
|
||||||
|
|
||||||
# Restart the WSGI Service
|
# 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.
|
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
|
!!! info
|
||||||
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.
|
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.
|
||||||
|
|
||||||
```no-highlight
|
```no-highlight
|
||||||
# apt-get install -y gunicorn supervisor
|
# apt-get install -y gunicorn supervisor
|
||||||
|
|||||||
194
docs/shell/intro.md
Normal file
194
docs/shell/intro.md
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
NetBox includes a Python shell withing which objects can be directly queried, created, modified, and deleted. To enter the shell, run the following command:
|
||||||
|
|
||||||
|
```
|
||||||
|
./manage.py nbshell
|
||||||
|
```
|
||||||
|
|
||||||
|
This will launch a customized version of [the built-in Django shell](https://docs.djangoproject.com/en/dev/ref/django-admin/#shell) with all relevant NetBox models pre-loaded. (If desired, the stock Django shell is also available by executing `./manage.py shell`.)
|
||||||
|
|
||||||
|
```
|
||||||
|
$ ./manage.py nbshell
|
||||||
|
### NetBox interactive shell (jstretch-laptop)
|
||||||
|
### Python 2.7.6 | Django 1.11.3 | NetBox 2.1.0-dev
|
||||||
|
### lsmodels() will show available models. Use help(<model>) for more info.
|
||||||
|
```
|
||||||
|
|
||||||
|
The function `lsmodels()` will print a list of all available NetBox models:
|
||||||
|
|
||||||
|
```
|
||||||
|
>>> lsmodels()
|
||||||
|
DCIM:
|
||||||
|
ConsolePort
|
||||||
|
ConsolePortTemplate
|
||||||
|
ConsoleServerPort
|
||||||
|
ConsoleServerPortTemplate
|
||||||
|
Device
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Querying Objects
|
||||||
|
|
||||||
|
Objects are retrieved by forming a [Django queryset](https://docs.djangoproject.com/en/dev/topics/db/queries/#retrieving-objects). The base queryset for an object takes the form `<model>.objects.all()`, which will return a (truncated) list of all objects of that type.
|
||||||
|
|
||||||
|
```
|
||||||
|
>>> Device.objects.all()
|
||||||
|
<QuerySet [<Device: TestDevice1>, <Device: TestDevice2>, <Device: TestDevice3>, <Device: TestDevice4>, <Device: TestDevice5>, '...(remaining elements truncated)...']>
|
||||||
|
```
|
||||||
|
|
||||||
|
Use a `for` loop to cycle through all objects in the list:
|
||||||
|
|
||||||
|
```
|
||||||
|
>>> for device in Device.objects.all():
|
||||||
|
... print(device.name, device.device_type)
|
||||||
|
...
|
||||||
|
(u'TestDevice1', <DeviceType: PacketThingy 9000>)
|
||||||
|
(u'TestDevice2', <DeviceType: PacketThingy 9000>)
|
||||||
|
(u'TestDevice3', <DeviceType: PacketThingy 9000>)
|
||||||
|
(u'TestDevice4', <DeviceType: PacketThingy 9000>)
|
||||||
|
(u'TestDevice5', <DeviceType: PacketThingy 9000>)
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
To count all objects matching the query, replace `all()` with `count()`:
|
||||||
|
|
||||||
|
```
|
||||||
|
>>> Device.objects.count()
|
||||||
|
1274
|
||||||
|
```
|
||||||
|
|
||||||
|
To retrieve a particular object (typically by its primary key or other unique field), use `get()`:
|
||||||
|
|
||||||
|
```
|
||||||
|
>>> Site.objects.get(pk=7)
|
||||||
|
<Site: Test Lab>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Filtering Querysets
|
||||||
|
|
||||||
|
In most cases, you want to retrieve only a specific subset of objects. To filter a queryset, replace `all()` with `filter()` and pass one or more keyword arguments. For example:
|
||||||
|
|
||||||
|
```
|
||||||
|
>>> Device.objects.filter(status=STATUS_ACTIVE)
|
||||||
|
<QuerySet [<Device: TestDevice1>, <Device: TestDevice2>, <Device: TestDevice3>, <Device: TestDevice8>, <Device: TestDevice9>, '...(remaining elements truncated)...']>
|
||||||
|
```
|
||||||
|
|
||||||
|
Querysets support slicing to return a specific range of objects.
|
||||||
|
|
||||||
|
```
|
||||||
|
>>> Device.objects.filter(status=STATUS_ACTIVE)[:3]
|
||||||
|
<QuerySet [<Device: TestDevice1>, <Device: TestDevice2>, <Device: TestDevice3>]>
|
||||||
|
```
|
||||||
|
|
||||||
|
The `count()` method can be appended to the queryset to return a count of objects rather than the full list.
|
||||||
|
|
||||||
|
```
|
||||||
|
>>> Device.objects.filter(status=STATUS_ACTIVE).count()
|
||||||
|
982
|
||||||
|
```
|
||||||
|
|
||||||
|
Relationships with other models can be traversed by concatenting field names with a double-underscore. For example, the following will return all devices assigned to the tenant named "Pied Piper."
|
||||||
|
|
||||||
|
```
|
||||||
|
>>> Device.objects.filter(tenant__name='Pied Piper')
|
||||||
|
```
|
||||||
|
|
||||||
|
This approach can span multiple levels of relations. For example, the following will return all IP addresses assigned to a device in North America:
|
||||||
|
|
||||||
|
```
|
||||||
|
>>> IPAddress.objects.filter(interface__device__site__region__slug='north-america')
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! note
|
||||||
|
While the above query is functional, it is very inefficient. There are ways to optimize such requests, however they are out of the scope of this document. For more information, see the [Django queryset method reference](https://docs.djangoproject.com/en/dev/ref/models/querysets/) documentation.
|
||||||
|
|
||||||
|
Reverse relationships can be traversed as well. For example, the following will find all devices with an interface named "em0":
|
||||||
|
|
||||||
|
```
|
||||||
|
>>> Device.objects.filter(interfaces__name='em0')
|
||||||
|
```
|
||||||
|
|
||||||
|
Character fields can be filtered against partial matches using the `contains` or `icontains` field lookup (the later of which is case-insensitive).
|
||||||
|
|
||||||
|
```
|
||||||
|
>>> Device.objects.filter(name__icontains='testdevice')
|
||||||
|
```
|
||||||
|
|
||||||
|
Similarly, numeric fields can be filtered by values less than, greater than, and/or equal to a given value.
|
||||||
|
|
||||||
|
```
|
||||||
|
>>> VLAN.objects.filter(vid__gt=2000)
|
||||||
|
```
|
||||||
|
|
||||||
|
Multiple filters can be combined to further refine a queryset.
|
||||||
|
|
||||||
|
```
|
||||||
|
>>> VLAN.objects.filter(vid__gt=2000, name__icontains='engineering')
|
||||||
|
```
|
||||||
|
|
||||||
|
To return the inverse of a filtered queryset, use `exclude()` instead of `filter()`.
|
||||||
|
|
||||||
|
```
|
||||||
|
>>> Device.objects.count()
|
||||||
|
4479
|
||||||
|
>>> Device.objects.filter(status=STATUS_ACTIVE).count()
|
||||||
|
4133
|
||||||
|
>>> Device.objects.exclude(status=STATUS_ACTIVE).count()
|
||||||
|
346
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! info
|
||||||
|
The examples above are intended only to provide a cursory introduction to queryset filtering. For an exhaustive list of the available filters, please consult the [Django queryset API docs](https://docs.djangoproject.com/en/dev/ref/models/querysets/).
|
||||||
|
|
||||||
|
## Creating and Updating Objects
|
||||||
|
|
||||||
|
New objects can be created by instantiating the desired model, defining values for all required attributes, and calling `save()` on the instance.
|
||||||
|
|
||||||
|
```
|
||||||
|
>>> lab1 = Site.objects.get(pk=7)
|
||||||
|
>>> myvlan = VLAN(vid=123, name='MyNewVLAN', site=lab1)
|
||||||
|
>>> myvlan.save()
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternatively, the above can be performed as a single operation:
|
||||||
|
|
||||||
|
```
|
||||||
|
>>> VLAN(vid=123, name='MyNewVLAN', site=Site.objects.get(pk=7)).save()
|
||||||
|
```
|
||||||
|
|
||||||
|
To modify an object, retrieve it, update the desired field(s), and call `save()` again.
|
||||||
|
|
||||||
|
```
|
||||||
|
>>> vlan = VLAN.objects.get(pk=1280)
|
||||||
|
>>> vlan.name
|
||||||
|
u'MyNewVLAN'
|
||||||
|
>>> vlan.name = 'BetterName'
|
||||||
|
>>> vlan.save()
|
||||||
|
>>> VLAN.objects.get(pk=1280).name
|
||||||
|
u'BetterName'
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! warning
|
||||||
|
The Django ORM provides methods to create/edit many objects at once, namely `bulk_create()` and `update()`. These are best avoided in most cases as they bypass a model's built-in validation and can easily lead to database corruption if not used carefully.
|
||||||
|
|
||||||
|
## Deleting Objects
|
||||||
|
|
||||||
|
To delete an object, simply call `delete()` on its instance. This will return a dictionary of all objects (including related objects) which have been deleted as a result of this operation.
|
||||||
|
|
||||||
|
```
|
||||||
|
>>> vlan
|
||||||
|
<VLAN: 123 (BetterName)>
|
||||||
|
>>> vlan.delete()
|
||||||
|
(1, {u'extras.CustomFieldValue': 0, u'ipam.VLAN': 1})
|
||||||
|
```
|
||||||
|
|
||||||
|
To delete multiple objects at once, call `delete()` on a filtered queryset. It's a good idea to always sanity-check the count of selected objects _before_ deleting them.
|
||||||
|
|
||||||
|
```
|
||||||
|
>>> Device.objects.filter(name__icontains='test').count()
|
||||||
|
27
|
||||||
|
>>> Device.objects.filter(name__icontains='test').delete()
|
||||||
|
(35, {u'extras.CustomFieldValue': 0, u'dcim.DeviceBay': 0, u'secrets.Secret': 0, u'dcim.InterfaceConnection': 4, u'extras.ImageAttachment': 0, u'dcim.Device': 27, u'dcim.Interface': 4, u'dcim.ConsolePort': 0, u'dcim.PowerPort': 0})
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! warning
|
||||||
|
Deletions are immediate and irreversible. Always think very carefully before calling `delete()` on an instance or queryset.
|
||||||
@@ -8,6 +8,7 @@ pages:
|
|||||||
- 'Web Server': 'installation/web-server.md'
|
- 'Web Server': 'installation/web-server.md'
|
||||||
- 'LDAP (Optional)': 'installation/ldap.md'
|
- 'LDAP (Optional)': 'installation/ldap.md'
|
||||||
- 'Upgrading': 'installation/upgrading.md'
|
- 'Upgrading': 'installation/upgrading.md'
|
||||||
|
- 'Migrating to Python3': 'installation/migrating-to-python3.md'
|
||||||
- 'Configuration':
|
- 'Configuration':
|
||||||
- 'Mandatory Settings': 'configuration/mandatory-settings.md'
|
- 'Mandatory Settings': 'configuration/mandatory-settings.md'
|
||||||
- 'Optional Settings': 'configuration/optional-settings.md'
|
- 'Optional Settings': 'configuration/optional-settings.md'
|
||||||
@@ -23,6 +24,8 @@ pages:
|
|||||||
- 'Authentication': 'api/authentication.md'
|
- 'Authentication': 'api/authentication.md'
|
||||||
- 'Working with Secrets': 'api/working-with-secrets.md'
|
- 'Working with Secrets': 'api/working-with-secrets.md'
|
||||||
- 'Examples': 'api/examples.md'
|
- 'Examples': 'api/examples.md'
|
||||||
|
- 'Shell':
|
||||||
|
- 'Introduction': 'shell/intro.md'
|
||||||
|
|
||||||
markdown_extensions:
|
markdown_extensions:
|
||||||
- admonition:
|
- admonition:
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from circuits.models import Provider, Circuit, CircuitTermination, CircuitType
|
|||||||
from dcim.api.serializers import NestedSiteSerializer, InterfaceSerializer
|
from dcim.api.serializers import NestedSiteSerializer, InterfaceSerializer
|
||||||
from extras.api.customfields import CustomFieldModelSerializer
|
from extras.api.customfields import CustomFieldModelSerializer
|
||||||
from tenancy.api.serializers import NestedTenantSerializer
|
from tenancy.api.serializers import NestedTenantSerializer
|
||||||
|
from utilities.api import ModelValidationMixin
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -44,7 +45,7 @@ class WritableProviderSerializer(CustomFieldModelSerializer):
|
|||||||
# Circuit types
|
# Circuit types
|
||||||
#
|
#
|
||||||
|
|
||||||
class CircuitTypeSerializer(serializers.ModelSerializer):
|
class CircuitTypeSerializer(ModelValidationMixin, serializers.ModelSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CircuitType
|
model = CircuitType
|
||||||
@@ -110,7 +111,7 @@ class CircuitTerminationSerializer(serializers.ModelSerializer):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class WritableCircuitTerminationSerializer(serializers.ModelSerializer):
|
class WritableCircuitTerminationSerializer(ModelValidationMixin, serializers.ModelSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CircuitTermination
|
model = CircuitTermination
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ class ProviderViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
|
|||||||
class CircuitTypeViewSet(ModelViewSet):
|
class CircuitTypeViewSet(ModelViewSet):
|
||||||
queryset = CircuitType.objects.all()
|
queryset = CircuitType.objects.all()
|
||||||
serializer_class = serializers.CircuitTypeSerializer
|
serializer_class = serializers.CircuitTypeSerializer
|
||||||
|
filter_class = filters.CircuitTypeFilter
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
|||||||
10
netbox/circuits/constants.py
Normal file
10
netbox/circuits/constants.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
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:
|
class Meta:
|
||||||
model = Provider
|
model = Provider
|
||||||
fields = ['name', 'account', 'asn']
|
fields = ['name', 'slug', 'asn', 'account']
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
if not value.strip():
|
if not value.strip():
|
||||||
@@ -39,10 +39,19 @@ class ProviderFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
return queryset.filter(
|
return queryset.filter(
|
||||||
Q(name__icontains=value) |
|
Q(name__icontains=value) |
|
||||||
Q(account__icontains=value) |
|
Q(account__icontains=value) |
|
||||||
|
Q(noc_contact__icontains=value) |
|
||||||
|
Q(admin_contact__icontains=value) |
|
||||||
Q(comments__icontains=value)
|
Q(comments__icontains=value)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CircuitTypeFilter(django_filters.FilterSet):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = CircuitType
|
||||||
|
fields = ['name', 'slug']
|
||||||
|
|
||||||
|
|
||||||
class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||||
id__in = NumericInFilter(name='id', lookup_expr='in')
|
id__in = NumericInFilter(name='id', lookup_expr='in')
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
@@ -50,7 +59,6 @@ class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
label='Search',
|
label='Search',
|
||||||
)
|
)
|
||||||
provider_id = django_filters.ModelMultipleChoiceFilter(
|
provider_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
name='provider',
|
|
||||||
queryset=Provider.objects.all(),
|
queryset=Provider.objects.all(),
|
||||||
label='Provider (ID)',
|
label='Provider (ID)',
|
||||||
)
|
)
|
||||||
@@ -61,7 +69,6 @@ class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
label='Provider (slug)',
|
label='Provider (slug)',
|
||||||
)
|
)
|
||||||
type_id = django_filters.ModelMultipleChoiceFilter(
|
type_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
name='type',
|
|
||||||
queryset=CircuitType.objects.all(),
|
queryset=CircuitType.objects.all(),
|
||||||
label='Circuit type (ID)',
|
label='Circuit type (ID)',
|
||||||
)
|
)
|
||||||
@@ -72,7 +79,6 @@ class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
label='Circuit type (slug)',
|
label='Circuit type (slug)',
|
||||||
)
|
)
|
||||||
tenant_id = NullableModelMultipleChoiceFilter(
|
tenant_id = NullableModelMultipleChoiceFilter(
|
||||||
name='tenant',
|
|
||||||
queryset=Tenant.objects.all(),
|
queryset=Tenant.objects.all(),
|
||||||
label='Tenant (ID)',
|
label='Tenant (ID)',
|
||||||
)
|
)
|
||||||
@@ -96,7 +102,7 @@ class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Circuit
|
model = Circuit
|
||||||
fields = ['install_date']
|
fields = ['cid', 'install_date', 'commit_rate']
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
if not value.strip():
|
if not value.strip():
|
||||||
@@ -111,12 +117,34 @@ class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
|
|
||||||
|
|
||||||
class CircuitTerminationFilter(django_filters.FilterSet):
|
class CircuitTerminationFilter(django_filters.FilterSet):
|
||||||
|
q = django_filters.CharFilter(
|
||||||
|
method='search',
|
||||||
|
label='Search',
|
||||||
|
)
|
||||||
circuit_id = django_filters.ModelMultipleChoiceFilter(
|
circuit_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
name='circuit',
|
|
||||||
queryset=Circuit.objects.all(),
|
queryset=Circuit.objects.all(),
|
||||||
label='Circuit',
|
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:
|
class Meta:
|
||||||
model = CircuitTermination
|
model = CircuitTermination
|
||||||
fields = ['term_side', 'site']
|
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()
|
||||||
|
|||||||
@@ -3,13 +3,13 @@ from __future__ import unicode_literals
|
|||||||
from django import forms
|
from django import forms
|
||||||
from django.db.models import Count
|
from django.db.models import Count
|
||||||
|
|
||||||
from dcim.models import Site, Device, Interface, Rack, VIRTUAL_IFACE_TYPES
|
from dcim.models import Site, Device, Interface, Rack
|
||||||
from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
|
from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
|
||||||
from tenancy.forms import TenancyForm
|
from tenancy.forms import TenancyForm
|
||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
from utilities.forms import (
|
from utilities.forms import (
|
||||||
APISelect, BootstrapMixin, BulkImportForm, ChainedFieldsMixin, ChainedModelChoiceField, CommentField, CSVDataField,
|
APISelect, BootstrapMixin, ChainedFieldsMixin, ChainedModelChoiceField, CommentField, FilterChoiceField,
|
||||||
FilterChoiceField, Livesearch, SmallTextarea, SlugField,
|
SmallTextarea, SlugField,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .models import Circuit, CircuitTermination, CircuitType, Provider
|
from .models import Circuit, CircuitTermination, CircuitType, Provider
|
||||||
@@ -39,15 +39,18 @@ class ProviderForm(BootstrapMixin, CustomFieldForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class ProviderFromCSVForm(forms.ModelForm):
|
class ProviderCSVForm(forms.ModelForm):
|
||||||
|
slug = SlugField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Provider
|
model = Provider
|
||||||
fields = ['name', 'slug', 'asn', 'account', 'portal_url']
|
fields = ['name', 'slug', 'asn', 'account', 'portal_url', 'comments']
|
||||||
|
help_texts = {
|
||||||
|
'name': 'Provider name',
|
||||||
class ProviderImportForm(BootstrapMixin, BulkImportForm):
|
'asn': '32-bit autonomous system number',
|
||||||
csv = CSVDataField(csv_form=ProviderFromCSVForm)
|
'portal_url': 'Portal URL',
|
||||||
|
'comments': 'Free-form comments',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class ProviderBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
class ProviderBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||||
@@ -102,21 +105,36 @@ class CircuitForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class CircuitFromCSVForm(forms.ModelForm):
|
class CircuitCSVForm(forms.ModelForm):
|
||||||
provider = forms.ModelChoiceField(Provider.objects.all(), to_field_name='name',
|
provider = forms.ModelChoiceField(
|
||||||
error_messages={'invalid_choice': 'Provider not found.'})
|
queryset=Provider.objects.all(),
|
||||||
type = forms.ModelChoiceField(CircuitType.objects.all(), to_field_name='name',
|
to_field_name='name',
|
||||||
error_messages={'invalid_choice': 'Invalid circuit type.'})
|
help_text='Name of parent provider',
|
||||||
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
|
error_messages={
|
||||||
error_messages={'invalid_choice': 'Tenant not found.'})
|
'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 Meta:
|
class Meta:
|
||||||
model = Circuit
|
model = Circuit
|
||||||
fields = ['cid', 'provider', 'type', 'tenant', 'install_date', 'commit_rate', 'description']
|
fields = ['cid', 'provider', 'type', 'tenant', 'install_date', 'commit_rate', 'description', 'comments']
|
||||||
|
|
||||||
|
|
||||||
class CircuitImportForm(BootstrapMixin, BulkImportForm):
|
|
||||||
csv = CSVDataField(csv_form=CircuitFromCSVForm)
|
|
||||||
|
|
||||||
|
|
||||||
class CircuitBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
class CircuitBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||||
@@ -192,7 +210,7 @@ class CircuitTerminationForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
interface = ChainedModelChoiceField(
|
interface = ChainedModelChoiceField(
|
||||||
queryset=Interface.objects.exclude(form_factor__in=VIRTUAL_IFACE_TYPES).select_related(
|
queryset=Interface.objects.connectable().select_related(
|
||||||
'circuit_termination', 'connected_as_a', 'connected_as_b'
|
'circuit_termination', 'connected_as_a', 'connected_as_b'
|
||||||
),
|
),
|
||||||
chains=(
|
chains=(
|
||||||
@@ -226,7 +244,7 @@ class CircuitTerminationForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm
|
|||||||
# Initialize helper selectors
|
# Initialize helper selectors
|
||||||
instance = kwargs.get('instance')
|
instance = kwargs.get('instance')
|
||||||
if instance and instance.interface is not None:
|
if instance and instance.interface is not None:
|
||||||
initial = kwargs.get('initial', {})
|
initial = kwargs.get('initial', {}).copy()
|
||||||
initial['rack'] = instance.interface.device.rack
|
initial['rack'] = instance.interface.device.rack
|
||||||
initial['device'] = instance.interface.device
|
initial['device'] = instance.interface.device
|
||||||
kwargs['initial'] = initial
|
kwargs['initial'] = initial
|
||||||
@@ -234,6 +252,11 @@ class CircuitTerminationForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm
|
|||||||
super(CircuitTerminationForm, self).__init__(*args, **kwargs)
|
super(CircuitTerminationForm, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
# Mark connected interfaces as disabled
|
# Mark connected interfaces as disabled
|
||||||
self.fields['interface'].choices = [
|
self.fields['interface'].choices = []
|
||||||
(iface.id, {'label': iface.name, 'disabled': iface.is_connected}) for iface in self.fields['interface'].queryset
|
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'),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|||||||
@@ -10,14 +10,7 @@ from extras.models import CustomFieldModel, CustomFieldValue
|
|||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
from utilities.utils import csv_format
|
from utilities.utils import csv_format
|
||||||
from utilities.models import CreatedUpdatedModel
|
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):
|
def humanize_speed(speed):
|
||||||
@@ -52,6 +45,8 @@ class Provider(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
comments = models.TextField(blank=True)
|
comments = models.TextField(blank=True)
|
||||||
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
|
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:
|
class Meta:
|
||||||
ordering = ['name']
|
ordering = ['name']
|
||||||
|
|
||||||
@@ -107,6 +102,8 @@ class Circuit(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
comments = models.TextField(blank=True)
|
comments = models.TextField(blank=True)
|
||||||
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
|
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:
|
class Meta:
|
||||||
ordering = ['provider', 'cid']
|
ordering = ['provider', 'cid']
|
||||||
unique_together = ['provider', 'cid']
|
unique_together = ['provider', 'cid']
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from __future__ import unicode_literals
|
|||||||
import django_tables2 as tables
|
import django_tables2 as tables
|
||||||
from django_tables2.utils import Accessor
|
from django_tables2.utils import Accessor
|
||||||
|
|
||||||
from utilities.tables import BaseTable, SearchTable, ToggleColumn
|
from utilities.tables import BaseTable, ToggleColumn
|
||||||
from .models import Circuit, CircuitType, Provider
|
from .models import Circuit, CircuitType, Provider
|
||||||
|
|
||||||
|
|
||||||
@@ -21,19 +21,18 @@ CIRCUITTYPE_ACTIONS = """
|
|||||||
class ProviderTable(BaseTable):
|
class ProviderTable(BaseTable):
|
||||||
pk = ToggleColumn()
|
pk = ToggleColumn()
|
||||||
name = tables.LinkColumn()
|
name = tables.LinkColumn()
|
||||||
circuit_count = tables.Column(accessor=Accessor('count_circuits'), verbose_name='Circuits')
|
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = Provider
|
model = Provider
|
||||||
fields = ('pk', 'name', 'asn', 'account', 'circuit_count')
|
fields = ('pk', 'name', 'asn', 'account',)
|
||||||
|
|
||||||
|
|
||||||
class ProviderSearchTable(SearchTable):
|
class ProviderDetailTable(ProviderTable):
|
||||||
name = tables.LinkColumn()
|
circuit_count = tables.Column(accessor=Accessor('count_circuits'), verbose_name='Circuits')
|
||||||
|
|
||||||
class Meta(SearchTable.Meta):
|
class Meta(ProviderTable.Meta):
|
||||||
model = Provider
|
model = Provider
|
||||||
fields = ('name', 'asn', 'account')
|
fields = ('pk', 'name', 'asn', 'account', 'circuit_count')
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -74,19 +73,3 @@ class CircuitTable(BaseTable):
|
|||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = Circuit
|
model = Circuit
|
||||||
fields = ('pk', 'cid', 'type', 'provider', 'tenant', 'a_side', 'z_side', 'description')
|
fields = ('pk', 'cid', 'type', 'provider', 'tenant', 'a_side', 'z_side', 'description')
|
||||||
|
|
||||||
|
|
||||||
class CircuitSearchTable(SearchTable):
|
|
||||||
cid = tables.LinkColumn(verbose_name='ID')
|
|
||||||
provider = tables.LinkColumn('circuits:provider', args=[Accessor('provider.slug')])
|
|
||||||
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')])
|
|
||||||
a_side = tables.LinkColumn(
|
|
||||||
'dcim:site', accessor=Accessor('termination_a.site'), args=[Accessor('termination_a.site.slug')]
|
|
||||||
)
|
|
||||||
z_side = tables.LinkColumn(
|
|
||||||
'dcim:site', accessor=Accessor('termination_z.site'), args=[Accessor('termination_z.site.slug')]
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta(SearchTable.Meta):
|
|
||||||
model = Circuit
|
|
||||||
fields = ('cid', 'type', 'provider', 'tenant', 'a_side', 'z_side', 'description')
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ urlpatterns = [
|
|||||||
|
|
||||||
# Providers
|
# Providers
|
||||||
url(r'^providers/$', views.ProviderListView.as_view(), name='provider_list'),
|
url(r'^providers/$', views.ProviderListView.as_view(), name='provider_list'),
|
||||||
url(r'^providers/add/$', views.ProviderEditView.as_view(), name='provider_add'),
|
url(r'^providers/add/$', views.ProviderCreateView.as_view(), name='provider_add'),
|
||||||
url(r'^providers/import/$', views.ProviderBulkImportView.as_view(), name='provider_import'),
|
url(r'^providers/import/$', views.ProviderBulkImportView.as_view(), name='provider_import'),
|
||||||
url(r'^providers/edit/$', views.ProviderBulkEditView.as_view(), name='provider_bulk_edit'),
|
url(r'^providers/edit/$', views.ProviderBulkEditView.as_view(), name='provider_bulk_edit'),
|
||||||
url(r'^providers/delete/$', views.ProviderBulkDeleteView.as_view(), name='provider_bulk_delete'),
|
url(r'^providers/delete/$', views.ProviderBulkDeleteView.as_view(), name='provider_bulk_delete'),
|
||||||
@@ -20,13 +20,13 @@ urlpatterns = [
|
|||||||
|
|
||||||
# Circuit types
|
# Circuit types
|
||||||
url(r'^circuit-types/$', views.CircuitTypeListView.as_view(), name='circuittype_list'),
|
url(r'^circuit-types/$', views.CircuitTypeListView.as_view(), name='circuittype_list'),
|
||||||
url(r'^circuit-types/add/$', views.CircuitTypeEditView.as_view(), name='circuittype_add'),
|
url(r'^circuit-types/add/$', views.CircuitTypeCreateView.as_view(), name='circuittype_add'),
|
||||||
url(r'^circuit-types/delete/$', views.CircuitTypeBulkDeleteView.as_view(), name='circuittype_bulk_delete'),
|
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'),
|
url(r'^circuit-types/(?P<slug>[\w-]+)/edit/$', views.CircuitTypeEditView.as_view(), name='circuittype_edit'),
|
||||||
|
|
||||||
# Circuits
|
# Circuits
|
||||||
url(r'^circuits/$', views.CircuitListView.as_view(), name='circuit_list'),
|
url(r'^circuits/$', views.CircuitListView.as_view(), name='circuit_list'),
|
||||||
url(r'^circuits/add/$', views.CircuitEditView.as_view(), name='circuit_add'),
|
url(r'^circuits/add/$', views.CircuitCreateView.as_view(), name='circuit_add'),
|
||||||
url(r'^circuits/import/$', views.CircuitBulkImportView.as_view(), name='circuit_import'),
|
url(r'^circuits/import/$', views.CircuitBulkImportView.as_view(), name='circuit_import'),
|
||||||
url(r'^circuits/edit/$', views.CircuitBulkEditView.as_view(), name='circuit_bulk_edit'),
|
url(r'^circuits/edit/$', views.CircuitBulkEditView.as_view(), name='circuit_bulk_edit'),
|
||||||
url(r'^circuits/delete/$', views.CircuitBulkDeleteView.as_view(), name='circuit_bulk_delete'),
|
url(r'^circuits/delete/$', views.CircuitBulkDeleteView.as_view(), name='circuit_bulk_delete'),
|
||||||
@@ -36,7 +36,7 @@ urlpatterns = [
|
|||||||
url(r'^circuits/(?P<pk>\d+)/terminations/swap/$', views.circuit_terminations_swap, name='circuit_terminations_swap'),
|
url(r'^circuits/(?P<pk>\d+)/terminations/swap/$', views.circuit_terminations_swap, name='circuit_terminations_swap'),
|
||||||
|
|
||||||
# Circuit terminations
|
# Circuit terminations
|
||||||
url(r'^circuits/(?P<circuit>\d+)/terminations/add/$', views.CircuitTerminationEditView.as_view(), name='circuittermination_add'),
|
url(r'^circuits/(?P<circuit>\d+)/terminations/add/$', views.CircuitTerminationCreateView.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+)/edit/$', views.CircuitTerminationEditView.as_view(), name='circuittermination_edit'),
|
||||||
url(r'^circuit-terminations/(?P<pk>\d+)/delete/$', views.CircuitTerminationDeleteView.as_view(), name='circuittermination_delete'),
|
url(r'^circuit-terminations/(?P<pk>\d+)/delete/$', views.CircuitTerminationDeleteView.as_view(), name='circuittermination_delete'),
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ class ProviderListView(ObjectListView):
|
|||||||
queryset = Provider.objects.annotate(count_circuits=Count('circuits'))
|
queryset = Provider.objects.annotate(count_circuits=Count('circuits'))
|
||||||
filter = filters.ProviderFilter
|
filter = filters.ProviderFilter
|
||||||
filter_form = forms.ProviderFilterForm
|
filter_form = forms.ProviderFilterForm
|
||||||
table = tables.ProviderTable
|
table = tables.ProviderDetailTable
|
||||||
template_name = 'circuits/provider_list.html'
|
template_name = 'circuits/provider_list.html'
|
||||||
|
|
||||||
|
|
||||||
@@ -49,14 +49,18 @@ class ProviderView(View):
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
class ProviderEditView(PermissionRequiredMixin, ObjectEditView):
|
class ProviderCreateView(PermissionRequiredMixin, ObjectEditView):
|
||||||
permission_required = 'circuits.change_provider'
|
permission_required = 'circuits.add_provider'
|
||||||
model = Provider
|
model = Provider
|
||||||
form_class = forms.ProviderForm
|
form_class = forms.ProviderForm
|
||||||
template_name = 'circuits/provider_edit.html'
|
template_name = 'circuits/provider_edit.html'
|
||||||
default_return_url = 'circuits:provider_list'
|
default_return_url = 'circuits:provider_list'
|
||||||
|
|
||||||
|
|
||||||
|
class ProviderEditView(ProviderCreateView):
|
||||||
|
permission_required = 'circuits.change_provider'
|
||||||
|
|
||||||
|
|
||||||
class ProviderDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
class ProviderDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||||
permission_required = 'circuits.delete_provider'
|
permission_required = 'circuits.delete_provider'
|
||||||
model = Provider
|
model = Provider
|
||||||
@@ -65,9 +69,8 @@ class ProviderDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
|||||||
|
|
||||||
class ProviderBulkImportView(PermissionRequiredMixin, BulkImportView):
|
class ProviderBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||||
permission_required = 'circuits.add_provider'
|
permission_required = 'circuits.add_provider'
|
||||||
form = forms.ProviderImportForm
|
model_form = forms.ProviderCSVForm
|
||||||
table = tables.ProviderTable
|
table = tables.ProviderTable
|
||||||
template_name = 'circuits/provider_import.html'
|
|
||||||
default_return_url = 'circuits:provider_list'
|
default_return_url = 'circuits:provider_list'
|
||||||
|
|
||||||
|
|
||||||
@@ -75,8 +78,8 @@ class ProviderBulkEditView(PermissionRequiredMixin, BulkEditView):
|
|||||||
permission_required = 'circuits.change_provider'
|
permission_required = 'circuits.change_provider'
|
||||||
cls = Provider
|
cls = Provider
|
||||||
filter = filters.ProviderFilter
|
filter = filters.ProviderFilter
|
||||||
|
table = tables.ProviderTable
|
||||||
form = forms.ProviderBulkEditForm
|
form = forms.ProviderBulkEditForm
|
||||||
template_name = 'circuits/provider_bulk_edit.html'
|
|
||||||
default_return_url = 'circuits:provider_list'
|
default_return_url = 'circuits:provider_list'
|
||||||
|
|
||||||
|
|
||||||
@@ -84,6 +87,7 @@ class ProviderBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
|||||||
permission_required = 'circuits.delete_provider'
|
permission_required = 'circuits.delete_provider'
|
||||||
cls = Provider
|
cls = Provider
|
||||||
filter = filters.ProviderFilter
|
filter = filters.ProviderFilter
|
||||||
|
table = tables.ProviderTable
|
||||||
default_return_url = 'circuits:provider_list'
|
default_return_url = 'circuits:provider_list'
|
||||||
|
|
||||||
|
|
||||||
@@ -97,8 +101,8 @@ class CircuitTypeListView(ObjectListView):
|
|||||||
template_name = 'circuits/circuittype_list.html'
|
template_name = 'circuits/circuittype_list.html'
|
||||||
|
|
||||||
|
|
||||||
class CircuitTypeEditView(PermissionRequiredMixin, ObjectEditView):
|
class CircuitTypeCreateView(PermissionRequiredMixin, ObjectEditView):
|
||||||
permission_required = 'circuits.change_circuittype'
|
permission_required = 'circuits.add_circuittype'
|
||||||
model = CircuitType
|
model = CircuitType
|
||||||
form_class = forms.CircuitTypeForm
|
form_class = forms.CircuitTypeForm
|
||||||
|
|
||||||
@@ -106,9 +110,15 @@ class CircuitTypeEditView(PermissionRequiredMixin, ObjectEditView):
|
|||||||
return reverse('circuits:circuittype_list')
|
return reverse('circuits:circuittype_list')
|
||||||
|
|
||||||
|
|
||||||
|
class CircuitTypeEditView(CircuitTypeCreateView):
|
||||||
|
permission_required = 'circuits.change_circuittype'
|
||||||
|
|
||||||
|
|
||||||
class CircuitTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
class CircuitTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
permission_required = 'circuits.delete_circuittype'
|
permission_required = 'circuits.delete_circuittype'
|
||||||
cls = CircuitType
|
cls = CircuitType
|
||||||
|
queryset = CircuitType.objects.annotate(circuit_count=Count('circuits'))
|
||||||
|
table = tables.CircuitTypeTable
|
||||||
default_return_url = 'circuits:circuittype_list'
|
default_return_url = 'circuits:circuittype_list'
|
||||||
|
|
||||||
|
|
||||||
@@ -147,14 +157,18 @@ class CircuitView(View):
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
class CircuitEditView(PermissionRequiredMixin, ObjectEditView):
|
class CircuitCreateView(PermissionRequiredMixin, ObjectEditView):
|
||||||
permission_required = 'circuits.change_circuit'
|
permission_required = 'circuits.add_circuit'
|
||||||
model = Circuit
|
model = Circuit
|
||||||
form_class = forms.CircuitForm
|
form_class = forms.CircuitForm
|
||||||
template_name = 'circuits/circuit_edit.html'
|
template_name = 'circuits/circuit_edit.html'
|
||||||
default_return_url = 'circuits:circuit_list'
|
default_return_url = 'circuits:circuit_list'
|
||||||
|
|
||||||
|
|
||||||
|
class CircuitEditView(CircuitCreateView):
|
||||||
|
permission_required = 'circuits.change_circuit'
|
||||||
|
|
||||||
|
|
||||||
class CircuitDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
class CircuitDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||||
permission_required = 'circuits.delete_circuit'
|
permission_required = 'circuits.delete_circuit'
|
||||||
model = Circuit
|
model = Circuit
|
||||||
@@ -163,25 +177,27 @@ class CircuitDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
|||||||
|
|
||||||
class CircuitBulkImportView(PermissionRequiredMixin, BulkImportView):
|
class CircuitBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||||
permission_required = 'circuits.add_circuit'
|
permission_required = 'circuits.add_circuit'
|
||||||
form = forms.CircuitImportForm
|
model_form = forms.CircuitCSVForm
|
||||||
table = tables.CircuitTable
|
table = tables.CircuitTable
|
||||||
template_name = 'circuits/circuit_import.html'
|
|
||||||
default_return_url = 'circuits:circuit_list'
|
default_return_url = 'circuits:circuit_list'
|
||||||
|
|
||||||
|
|
||||||
class CircuitBulkEditView(PermissionRequiredMixin, BulkEditView):
|
class CircuitBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||||
permission_required = 'circuits.change_circuit'
|
permission_required = 'circuits.change_circuit'
|
||||||
cls = Circuit
|
cls = Circuit
|
||||||
|
queryset = Circuit.objects.select_related('provider', 'type', 'tenant').prefetch_related('terminations__site')
|
||||||
filter = filters.CircuitFilter
|
filter = filters.CircuitFilter
|
||||||
|
table = tables.CircuitTable
|
||||||
form = forms.CircuitBulkEditForm
|
form = forms.CircuitBulkEditForm
|
||||||
template_name = 'circuits/circuit_bulk_edit.html'
|
|
||||||
default_return_url = 'circuits:circuit_list'
|
default_return_url = 'circuits:circuit_list'
|
||||||
|
|
||||||
|
|
||||||
class CircuitBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
class CircuitBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
permission_required = 'circuits.delete_circuit'
|
permission_required = 'circuits.delete_circuit'
|
||||||
cls = Circuit
|
cls = Circuit
|
||||||
|
queryset = Circuit.objects.select_related('provider', 'type', 'tenant').prefetch_related('terminations__site')
|
||||||
filter = filters.CircuitFilter
|
filter = filters.CircuitFilter
|
||||||
|
table = tables.CircuitTable
|
||||||
default_return_url = 'circuits:circuit_list'
|
default_return_url = 'circuits:circuit_list'
|
||||||
|
|
||||||
|
|
||||||
@@ -234,8 +250,8 @@ def circuit_terminations_swap(request, pk):
|
|||||||
# Circuit terminations
|
# Circuit terminations
|
||||||
#
|
#
|
||||||
|
|
||||||
class CircuitTerminationEditView(PermissionRequiredMixin, ObjectEditView):
|
class CircuitTerminationCreateView(PermissionRequiredMixin, ObjectEditView):
|
||||||
permission_required = 'circuits.change_circuittermination'
|
permission_required = 'circuits.add_circuittermination'
|
||||||
model = CircuitTermination
|
model = CircuitTermination
|
||||||
form_class = forms.CircuitTerminationForm
|
form_class = forms.CircuitTerminationForm
|
||||||
template_name = 'circuits/circuittermination_edit.html'
|
template_name = 'circuits/circuittermination_edit.html'
|
||||||
@@ -249,6 +265,10 @@ class CircuitTerminationEditView(PermissionRequiredMixin, ObjectEditView):
|
|||||||
return obj.circuit.get_absolute_url()
|
return obj.circuit.get_absolute_url()
|
||||||
|
|
||||||
|
|
||||||
|
class CircuitTerminationEditView(CircuitTerminationCreateView):
|
||||||
|
permission_required = 'circuits.change_circuittermination'
|
||||||
|
|
||||||
|
|
||||||
class CircuitTerminationDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
class CircuitTerminationDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||||
permission_required = 'circuits.delete_circuittermination'
|
permission_required = 'circuits.delete_circuittermination'
|
||||||
model = CircuitTermination
|
model = CircuitTermination
|
||||||
|
|||||||
@@ -1,19 +1,21 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.validators import UniqueTogetherValidator
|
from rest_framework.validators import UniqueTogetherValidator
|
||||||
|
|
||||||
from ipam.models import IPAddress
|
from ipam.models import IPAddress
|
||||||
|
from circuits.models import Circuit, CircuitTermination
|
||||||
from dcim.models import (
|
from dcim.models import (
|
||||||
CONNECTION_STATUS_CHOICES, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device,
|
CONNECTION_STATUS_CHOICES, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device,
|
||||||
DeviceBay, DeviceBayTemplate, DeviceType, DeviceRole, IFACE_FF_CHOICES, IFACE_ORDERING_CHOICES, Interface,
|
DeviceBay, DeviceBayTemplate, DeviceType, DeviceRole, IFACE_FF_CHOICES, IFACE_ORDERING_CHOICES, Interface,
|
||||||
InterfaceConnection, InterfaceTemplate, Manufacturer, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort,
|
InterfaceConnection, InterfaceTemplate, Manufacturer, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate,
|
||||||
PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RACK_FACE_CHOICES, RACK_TYPE_CHOICES,
|
PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RACK_FACE_CHOICES, RACK_TYPE_CHOICES,
|
||||||
RACK_WIDTH_CHOICES, Region, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHOICES,
|
RACK_WIDTH_CHOICES, Region, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHOICES,
|
||||||
)
|
)
|
||||||
from extras.api.customfields import CustomFieldModelSerializer
|
from extras.api.customfields import CustomFieldModelSerializer
|
||||||
from tenancy.api.serializers import NestedTenantSerializer
|
from tenancy.api.serializers import NestedTenantSerializer
|
||||||
from utilities.api import ChoiceFieldSerializer
|
from utilities.api import ChoiceFieldSerializer, ModelValidationMixin
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -36,7 +38,7 @@ class RegionSerializer(serializers.ModelSerializer):
|
|||||||
fields = ['id', 'name', 'slug', 'parent']
|
fields = ['id', 'name', 'slug', 'parent']
|
||||||
|
|
||||||
|
|
||||||
class WritableRegionSerializer(serializers.ModelSerializer):
|
class WritableRegionSerializer(ModelValidationMixin, serializers.ModelSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Region
|
model = Region
|
||||||
@@ -98,7 +100,7 @@ class NestedRackGroupSerializer(serializers.ModelSerializer):
|
|||||||
fields = ['id', 'url', 'name', 'slug']
|
fields = ['id', 'url', 'name', 'slug']
|
||||||
|
|
||||||
|
|
||||||
class WritableRackGroupSerializer(serializers.ModelSerializer):
|
class WritableRackGroupSerializer(ModelValidationMixin, serializers.ModelSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = RackGroup
|
model = RackGroup
|
||||||
@@ -109,7 +111,7 @@ class WritableRackGroupSerializer(serializers.ModelSerializer):
|
|||||||
# Rack roles
|
# Rack roles
|
||||||
#
|
#
|
||||||
|
|
||||||
class RackRoleSerializer(serializers.ModelSerializer):
|
class RackRoleSerializer(ModelValidationMixin, serializers.ModelSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = RackRole
|
model = RackRole
|
||||||
@@ -174,6 +176,9 @@ class WritableRackSerializer(CustomFieldModelSerializer):
|
|||||||
validator.set_context(self)
|
validator.set_context(self)
|
||||||
validator(data)
|
validator(data)
|
||||||
|
|
||||||
|
# Enforce model validation
|
||||||
|
super(WritableRackSerializer, self).validate(data)
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
@@ -211,7 +216,7 @@ class RackReservationSerializer(serializers.ModelSerializer):
|
|||||||
fields = ['id', 'rack', 'units', 'created', 'user', 'description']
|
fields = ['id', 'rack', 'units', 'created', 'user', 'description']
|
||||||
|
|
||||||
|
|
||||||
class WritableRackReservationSerializer(serializers.ModelSerializer):
|
class WritableRackReservationSerializer(ModelValidationMixin, serializers.ModelSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = RackReservation
|
model = RackReservation
|
||||||
@@ -222,7 +227,7 @@ class WritableRackReservationSerializer(serializers.ModelSerializer):
|
|||||||
# Manufacturers
|
# Manufacturers
|
||||||
#
|
#
|
||||||
|
|
||||||
class ManufacturerSerializer(serializers.ModelSerializer):
|
class ManufacturerSerializer(ModelValidationMixin, serializers.ModelSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Manufacturer
|
model = Manufacturer
|
||||||
@@ -287,7 +292,7 @@ class ConsolePortTemplateSerializer(serializers.ModelSerializer):
|
|||||||
fields = ['id', 'device_type', 'name']
|
fields = ['id', 'device_type', 'name']
|
||||||
|
|
||||||
|
|
||||||
class WritableConsolePortTemplateSerializer(serializers.ModelSerializer):
|
class WritableConsolePortTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ConsolePortTemplate
|
model = ConsolePortTemplate
|
||||||
@@ -306,7 +311,7 @@ class ConsoleServerPortTemplateSerializer(serializers.ModelSerializer):
|
|||||||
fields = ['id', 'device_type', 'name']
|
fields = ['id', 'device_type', 'name']
|
||||||
|
|
||||||
|
|
||||||
class WritableConsoleServerPortTemplateSerializer(serializers.ModelSerializer):
|
class WritableConsoleServerPortTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ConsoleServerPortTemplate
|
model = ConsoleServerPortTemplate
|
||||||
@@ -325,7 +330,7 @@ class PowerPortTemplateSerializer(serializers.ModelSerializer):
|
|||||||
fields = ['id', 'device_type', 'name']
|
fields = ['id', 'device_type', 'name']
|
||||||
|
|
||||||
|
|
||||||
class WritablePowerPortTemplateSerializer(serializers.ModelSerializer):
|
class WritablePowerPortTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = PowerPortTemplate
|
model = PowerPortTemplate
|
||||||
@@ -344,7 +349,7 @@ class PowerOutletTemplateSerializer(serializers.ModelSerializer):
|
|||||||
fields = ['id', 'device_type', 'name']
|
fields = ['id', 'device_type', 'name']
|
||||||
|
|
||||||
|
|
||||||
class WritablePowerOutletTemplateSerializer(serializers.ModelSerializer):
|
class WritablePowerOutletTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = PowerOutletTemplate
|
model = PowerOutletTemplate
|
||||||
@@ -364,7 +369,7 @@ class InterfaceTemplateSerializer(serializers.ModelSerializer):
|
|||||||
fields = ['id', 'device_type', 'name', 'form_factor', 'mgmt_only']
|
fields = ['id', 'device_type', 'name', 'form_factor', 'mgmt_only']
|
||||||
|
|
||||||
|
|
||||||
class WritableInterfaceTemplateSerializer(serializers.ModelSerializer):
|
class WritableInterfaceTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = InterfaceTemplate
|
model = InterfaceTemplate
|
||||||
@@ -383,7 +388,7 @@ class DeviceBayTemplateSerializer(serializers.ModelSerializer):
|
|||||||
fields = ['id', 'device_type', 'name']
|
fields = ['id', 'device_type', 'name']
|
||||||
|
|
||||||
|
|
||||||
class WritableDeviceBayTemplateSerializer(serializers.ModelSerializer):
|
class WritableDeviceBayTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = DeviceBayTemplate
|
model = DeviceBayTemplate
|
||||||
@@ -394,7 +399,7 @@ class WritableDeviceBayTemplateSerializer(serializers.ModelSerializer):
|
|||||||
# Device roles
|
# Device roles
|
||||||
#
|
#
|
||||||
|
|
||||||
class DeviceRoleSerializer(serializers.ModelSerializer):
|
class DeviceRoleSerializer(ModelValidationMixin, serializers.ModelSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = DeviceRole
|
model = DeviceRole
|
||||||
@@ -413,11 +418,11 @@ class NestedDeviceRoleSerializer(serializers.ModelSerializer):
|
|||||||
# Platforms
|
# Platforms
|
||||||
#
|
#
|
||||||
|
|
||||||
class PlatformSerializer(serializers.ModelSerializer):
|
class PlatformSerializer(ModelValidationMixin, serializers.ModelSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Platform
|
model = Platform
|
||||||
fields = ['id', 'name', 'slug', 'rpc_client']
|
fields = ['id', 'name', 'slug', 'napalm_driver', 'rpc_client']
|
||||||
|
|
||||||
|
|
||||||
class NestedPlatformSerializer(serializers.ModelSerializer):
|
class NestedPlatformSerializer(serializers.ModelSerializer):
|
||||||
@@ -468,14 +473,10 @@ class DeviceSerializer(CustomFieldModelSerializer):
|
|||||||
device_bay = obj.parent_bay
|
device_bay = obj.parent_bay
|
||||||
except DeviceBay.DoesNotExist:
|
except DeviceBay.DoesNotExist:
|
||||||
return None
|
return None
|
||||||
return {
|
context = {'request': self.context['request']}
|
||||||
'id': device_bay.device.pk,
|
data = NestedDeviceSerializer(instance=device_bay.device, context=context).data
|
||||||
'name': device_bay.device.name,
|
data['device_bay'] = NestedDeviceBaySerializer(instance=device_bay, context=context).data
|
||||||
'device_bay': {
|
return data
|
||||||
'id': device_bay.pk,
|
|
||||||
'name': device_bay.name,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class WritableDeviceSerializer(CustomFieldModelSerializer):
|
class WritableDeviceSerializer(CustomFieldModelSerializer):
|
||||||
@@ -496,6 +497,9 @@ class WritableDeviceSerializer(CustomFieldModelSerializer):
|
|||||||
validator.set_context(self)
|
validator.set_context(self)
|
||||||
validator(data)
|
validator(data)
|
||||||
|
|
||||||
|
# Enforce model validation
|
||||||
|
super(WritableDeviceSerializer, self).validate(data)
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
@@ -512,7 +516,7 @@ class ConsoleServerPortSerializer(serializers.ModelSerializer):
|
|||||||
read_only_fields = ['connected_console']
|
read_only_fields = ['connected_console']
|
||||||
|
|
||||||
|
|
||||||
class WritableConsoleServerPortSerializer(serializers.ModelSerializer):
|
class WritableConsoleServerPortSerializer(ModelValidationMixin, serializers.ModelSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ConsoleServerPort
|
model = ConsoleServerPort
|
||||||
@@ -532,7 +536,7 @@ class ConsolePortSerializer(serializers.ModelSerializer):
|
|||||||
fields = ['id', 'device', 'name', 'cs_port', 'connection_status']
|
fields = ['id', 'device', 'name', 'cs_port', 'connection_status']
|
||||||
|
|
||||||
|
|
||||||
class WritableConsolePortSerializer(serializers.ModelSerializer):
|
class WritableConsolePortSerializer(ModelValidationMixin, serializers.ModelSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ConsolePort
|
model = ConsolePort
|
||||||
@@ -552,7 +556,7 @@ class PowerOutletSerializer(serializers.ModelSerializer):
|
|||||||
read_only_fields = ['connected_port']
|
read_only_fields = ['connected_port']
|
||||||
|
|
||||||
|
|
||||||
class WritablePowerOutletSerializer(serializers.ModelSerializer):
|
class WritablePowerOutletSerializer(ModelValidationMixin, serializers.ModelSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = PowerOutlet
|
model = PowerOutlet
|
||||||
@@ -572,7 +576,7 @@ class PowerPortSerializer(serializers.ModelSerializer):
|
|||||||
fields = ['id', 'device', 'name', 'power_outlet', 'connection_status']
|
fields = ['id', 'device', 'name', 'power_outlet', 'connection_status']
|
||||||
|
|
||||||
|
|
||||||
class WritablePowerPortSerializer(serializers.ModelSerializer):
|
class WritablePowerPortSerializer(ModelValidationMixin, serializers.ModelSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = PowerPort
|
model = PowerPort
|
||||||
@@ -591,28 +595,58 @@ class NestedInterfaceSerializer(serializers.ModelSerializer):
|
|||||||
fields = ['id', 'url', 'name']
|
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):
|
class InterfaceSerializer(serializers.ModelSerializer):
|
||||||
device = NestedDeviceSerializer()
|
device = NestedDeviceSerializer()
|
||||||
form_factor = ChoiceFieldSerializer(choices=IFACE_FF_CHOICES)
|
form_factor = ChoiceFieldSerializer(choices=IFACE_FF_CHOICES)
|
||||||
lag = NestedInterfaceSerializer()
|
lag = NestedInterfaceSerializer()
|
||||||
connection = serializers.SerializerMethodField(read_only=True)
|
is_connected = serializers.SerializerMethodField(read_only=True)
|
||||||
connected_interface = serializers.SerializerMethodField(read_only=True)
|
interface_connection = serializers.SerializerMethodField(read_only=True)
|
||||||
|
circuit_termination = InterfaceCircuitTerminationSerializer()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Interface
|
model = Interface
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'device', 'name', 'form_factor', 'lag', 'mac_address', 'mgmt_only', 'description', 'connection',
|
'id', 'device', 'name', 'form_factor', 'enabled', 'lag', 'mtu', 'mac_address', 'mgmt_only', 'description',
|
||||||
'connected_interface',
|
'is_connected', 'interface_connection', 'circuit_termination',
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_connection(self, obj):
|
def get_is_connected(self, obj):
|
||||||
|
"""
|
||||||
|
Return True if the interface has a connected interface or circuit termination.
|
||||||
|
"""
|
||||||
if obj.connection:
|
if obj.connection:
|
||||||
return NestedInterfaceConnectionSerializer(obj.connection, context=self.context).data
|
return True
|
||||||
return None
|
try:
|
||||||
|
circuit_termination = obj.circuit_termination
|
||||||
|
return True
|
||||||
|
except CircuitTermination.DoesNotExist:
|
||||||
|
pass
|
||||||
|
return False
|
||||||
|
|
||||||
def get_connected_interface(self, obj):
|
def get_interface_connection(self, obj):
|
||||||
if obj.connected_interface:
|
if obj.connection:
|
||||||
return PeerInterfaceSerializer(obj.connected_interface, context=self.context).data
|
return OrderedDict((
|
||||||
|
('interface', PeerInterfaceSerializer(obj.connected_interface, context=self.context).data),
|
||||||
|
('status', obj.connection.connection_status),
|
||||||
|
))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@@ -624,14 +658,19 @@ class PeerInterfaceSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Interface
|
model = Interface
|
||||||
fields = ['id', 'url', 'device', 'name', 'form_factor', 'lag', 'mac_address', 'mgmt_only', 'description']
|
fields = [
|
||||||
|
'id', 'url', 'device', 'name', 'form_factor', 'enabled', 'lag', 'mtu', 'mac_address', 'mgmt_only',
|
||||||
|
'description',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class WritableInterfaceSerializer(serializers.ModelSerializer):
|
class WritableInterfaceSerializer(ModelValidationMixin, serializers.ModelSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Interface
|
model = Interface
|
||||||
fields = ['id', 'device', 'name', 'form_factor', 'lag', 'mac_address', 'mgmt_only', 'description']
|
fields = [
|
||||||
|
'id', 'device', 'name', 'form_factor', 'enabled', 'lag', 'mtu', 'mac_address', 'mgmt_only', 'description',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -647,7 +686,15 @@ class DeviceBaySerializer(serializers.ModelSerializer):
|
|||||||
fields = ['id', 'device', 'name', 'installed_device']
|
fields = ['id', 'device', 'name', 'installed_device']
|
||||||
|
|
||||||
|
|
||||||
class WritableDeviceBaySerializer(serializers.ModelSerializer):
|
class NestedDeviceBaySerializer(serializers.ModelSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebay-detail')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = DeviceBay
|
||||||
|
fields = ['id', 'url', 'name']
|
||||||
|
|
||||||
|
|
||||||
|
class WritableDeviceBaySerializer(ModelValidationMixin, serializers.ModelSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = DeviceBay
|
model = DeviceBay
|
||||||
@@ -664,14 +711,20 @@ class InventoryItemSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = InventoryItem
|
model = InventoryItem
|
||||||
fields = ['id', 'device', 'parent', 'name', 'manufacturer', 'part_id', 'serial', 'discovered']
|
fields = [
|
||||||
|
'id', 'device', 'parent', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered',
|
||||||
|
'description',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class WritableInventoryItemSerializer(serializers.ModelSerializer):
|
class WritableInventoryItemSerializer(ModelValidationMixin, serializers.ModelSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = InventoryItem
|
model = InventoryItem
|
||||||
fields = ['id', 'device', 'parent', 'name', 'manufacturer', 'part_id', 'serial', 'discovered']
|
fields = [
|
||||||
|
'id', 'device', 'parent', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered',
|
||||||
|
'description',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -696,7 +749,7 @@ class NestedInterfaceConnectionSerializer(serializers.ModelSerializer):
|
|||||||
fields = ['id', 'url', 'connection_status']
|
fields = ['id', 'url', 'connection_status']
|
||||||
|
|
||||||
|
|
||||||
class WritableInterfaceConnectionSerializer(serializers.ModelSerializer):
|
class WritableInterfaceConnectionSerializer(ModelValidationMixin, serializers.ModelSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = InterfaceConnection
|
model = InterfaceConnection
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
from rest_framework.decorators import detail_route
|
from rest_framework.decorators import detail_route
|
||||||
from rest_framework.mixins import ListModelMixin
|
from rest_framework.mixins import ListModelMixin
|
||||||
@@ -7,6 +8,7 @@ from rest_framework.response import Response
|
|||||||
from rest_framework.viewsets import GenericViewSet, ModelViewSet, ViewSet
|
from rest_framework.viewsets import GenericViewSet, ModelViewSet, ViewSet
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.http import HttpResponseBadRequest, HttpResponseForbidden
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
|
|
||||||
from dcim.models import (
|
from dcim.models import (
|
||||||
@@ -32,6 +34,7 @@ class RegionViewSet(WritableSerializerMixin, ModelViewSet):
|
|||||||
queryset = Region.objects.all()
|
queryset = Region.objects.all()
|
||||||
serializer_class = serializers.RegionSerializer
|
serializer_class = serializers.RegionSerializer
|
||||||
write_serializer_class = serializers.WritableRegionSerializer
|
write_serializer_class = serializers.WritableRegionSerializer
|
||||||
|
filter_class = filters.RegionFilter
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -73,6 +76,7 @@ class RackGroupViewSet(WritableSerializerMixin, ModelViewSet):
|
|||||||
class RackRoleViewSet(ModelViewSet):
|
class RackRoleViewSet(ModelViewSet):
|
||||||
queryset = RackRole.objects.all()
|
queryset = RackRole.objects.all()
|
||||||
serializer_class = serializers.RackRoleSerializer
|
serializer_class = serializers.RackRoleSerializer
|
||||||
|
filter_class = filters.RackRoleFilter
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -128,6 +132,7 @@ class RackReservationViewSet(WritableSerializerMixin, ModelViewSet):
|
|||||||
class ManufacturerViewSet(ModelViewSet):
|
class ManufacturerViewSet(ModelViewSet):
|
||||||
queryset = Manufacturer.objects.all()
|
queryset = Manufacturer.objects.all()
|
||||||
serializer_class = serializers.ManufacturerSerializer
|
serializer_class = serializers.ManufacturerSerializer
|
||||||
|
filter_class = filters.ManufacturerFilter
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -194,6 +199,7 @@ class DeviceBayTemplateViewSet(WritableSerializerMixin, ModelViewSet):
|
|||||||
class DeviceRoleViewSet(ModelViewSet):
|
class DeviceRoleViewSet(ModelViewSet):
|
||||||
queryset = DeviceRole.objects.all()
|
queryset = DeviceRole.objects.all()
|
||||||
serializer_class = serializers.DeviceRoleSerializer
|
serializer_class = serializers.DeviceRoleSerializer
|
||||||
|
filter_class = filters.DeviceRoleFilter
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -203,6 +209,7 @@ class DeviceRoleViewSet(ModelViewSet):
|
|||||||
class PlatformViewSet(ModelViewSet):
|
class PlatformViewSet(ModelViewSet):
|
||||||
queryset = Platform.objects.all()
|
queryset = Platform.objects.all()
|
||||||
serializer_class = serializers.PlatformSerializer
|
serializer_class = serializers.PlatformSerializer
|
||||||
|
filter_class = filters.PlatformFilter
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -219,27 +226,64 @@ class DeviceViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
|
|||||||
write_serializer_class = serializers.WritableDeviceSerializer
|
write_serializer_class = serializers.WritableDeviceSerializer
|
||||||
filter_class = filters.DeviceFilter
|
filter_class = filters.DeviceFilter
|
||||||
|
|
||||||
@detail_route(url_path='lldp-neighbors')
|
@detail_route(url_path='napalm')
|
||||||
def lldp_neighbors(self, request, pk):
|
def napalm(self, request, pk):
|
||||||
"""
|
"""
|
||||||
Retrieve live LLDP neighbors of a device
|
Execute a NAPALM method on a Device
|
||||||
"""
|
"""
|
||||||
device = get_object_or_404(Device, pk=pk)
|
device = get_object_or_404(Device, pk=pk)
|
||||||
if not device.primary_ip:
|
if not device.primary_ip:
|
||||||
raise ServiceUnavailable("No IP configured for this device.")
|
raise ServiceUnavailable("This device does not have a primary IP address configured.")
|
||||||
|
if device.platform is None:
|
||||||
|
raise ServiceUnavailable("No platform is configured for this device.")
|
||||||
|
if not device.platform.napalm_driver:
|
||||||
|
raise ServiceUnavailable("No NAPALM driver is configured for this device's platform ().".format(
|
||||||
|
device.platform
|
||||||
|
))
|
||||||
|
|
||||||
RPC = device.get_rpc_client()
|
# Check that NAPALM is installed and verify the configured driver
|
||||||
if not RPC:
|
|
||||||
raise ServiceUnavailable("No RPC client available for this platform ({}).".format(device.platform))
|
|
||||||
|
|
||||||
# Connect to device and retrieve inventory info
|
|
||||||
try:
|
try:
|
||||||
with RPC(device, username=settings.NETBOX_USERNAME, password=settings.NETBOX_PASSWORD) as rpc_client:
|
import napalm
|
||||||
lldp_neighbors = rpc_client.get_lldp_neighbors()
|
from napalm_base.exceptions import ConnectAuthError, ModuleImportError
|
||||||
except:
|
except ImportError:
|
||||||
raise ServiceUnavailable("Error connecting to the remote device.")
|
raise ServiceUnavailable("NAPALM is not installed. Please see the documentation for instructions.")
|
||||||
|
try:
|
||||||
|
driver = napalm.get_network_driver(device.platform.napalm_driver)
|
||||||
|
except ModuleImportError:
|
||||||
|
raise ServiceUnavailable("NAPALM driver for platform {} not found: {}.".format(
|
||||||
|
device.platform, device.platform.napalm_driver
|
||||||
|
))
|
||||||
|
|
||||||
return Response(lldp_neighbors)
|
# Verify user permission
|
||||||
|
if not request.user.has_perm('dcim.napalm_read'):
|
||||||
|
return HttpResponseForbidden()
|
||||||
|
|
||||||
|
# Validate requested NAPALM methods
|
||||||
|
napalm_methods = request.GET.getlist('method')
|
||||||
|
for method in napalm_methods:
|
||||||
|
if not hasattr(driver, method):
|
||||||
|
return HttpResponseBadRequest("Unknown NAPALM method: {}".format(method))
|
||||||
|
elif not method.startswith('get_'):
|
||||||
|
return HttpResponseBadRequest("Unsupported NAPALM method: {}".format(method))
|
||||||
|
|
||||||
|
# Connect to the device and execute the requested methods
|
||||||
|
# TODO: Improve error handling
|
||||||
|
response = OrderedDict([(m, None) for m in napalm_methods])
|
||||||
|
ip_address = str(device.primary_ip.address.ip)
|
||||||
|
d = driver(
|
||||||
|
hostname=ip_address,
|
||||||
|
username=settings.NETBOX_USERNAME,
|
||||||
|
password=settings.NETBOX_PASSWORD
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
d.open()
|
||||||
|
for method in napalm_methods:
|
||||||
|
response[method] = getattr(d, method)()
|
||||||
|
except Exception as e:
|
||||||
|
raise ServiceUnavailable("Error connecting to the device: {}".format(e))
|
||||||
|
|
||||||
|
d.close()
|
||||||
|
return Response(response)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
|||||||
230
netbox/dcim/constants.py
Normal file
230
netbox/dcim/constants.py
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
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,6 +3,7 @@ from __future__ import unicode_literals
|
|||||||
import django_filters
|
import django_filters
|
||||||
from netaddr.core import AddrFormatError
|
from netaddr.core import AddrFormatError
|
||||||
|
|
||||||
|
from django.contrib.auth.models import User
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
|
||||||
from extras.filters import CustomFieldFilterSet
|
from extras.filters import CustomFieldFilterSet
|
||||||
@@ -11,11 +12,28 @@ from utilities.filters import NullableModelMultipleChoiceFilter, NumericInFilter
|
|||||||
from .models import (
|
from .models import (
|
||||||
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||||
DeviceBayTemplate, DeviceRole, DeviceType, STATUS_CHOICES, IFACE_FF_LAG, Interface, InterfaceConnection,
|
DeviceBayTemplate, DeviceRole, DeviceType, STATUS_CHOICES, IFACE_FF_LAG, Interface, InterfaceConnection,
|
||||||
InterfaceTemplate, Manufacturer, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort,
|
InterfaceTemplate, Manufacturer, InventoryItem, NONCONNECTABLE_IFACE_TYPES, Platform, PowerOutlet,
|
||||||
PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, Region, Site, VIRTUAL_IFACE_TYPES,
|
PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, Region, Site,
|
||||||
|
VIRTUAL_IFACE_TYPES, WIRELESS_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):
|
class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||||
id__in = NumericInFilter(name='id', lookup_expr='in')
|
id__in = NumericInFilter(name='id', lookup_expr='in')
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
@@ -23,23 +41,19 @@ class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
label='Search',
|
label='Search',
|
||||||
)
|
)
|
||||||
region_id = NullableModelMultipleChoiceFilter(
|
region_id = NullableModelMultipleChoiceFilter(
|
||||||
name='region',
|
|
||||||
queryset=Region.objects.all(),
|
queryset=Region.objects.all(),
|
||||||
label='Region (ID)',
|
label='Region (ID)',
|
||||||
)
|
)
|
||||||
region = NullableModelMultipleChoiceFilter(
|
region = NullableModelMultipleChoiceFilter(
|
||||||
name='region',
|
|
||||||
queryset=Region.objects.all(),
|
queryset=Region.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Region (slug)',
|
label='Region (slug)',
|
||||||
)
|
)
|
||||||
tenant_id = NullableModelMultipleChoiceFilter(
|
tenant_id = NullableModelMultipleChoiceFilter(
|
||||||
name='tenant',
|
|
||||||
queryset=Tenant.objects.all(),
|
queryset=Tenant.objects.all(),
|
||||||
label='Tenant (ID)',
|
label='Tenant (ID)',
|
||||||
)
|
)
|
||||||
tenant = NullableModelMultipleChoiceFilter(
|
tenant = NullableModelMultipleChoiceFilter(
|
||||||
name='tenant',
|
|
||||||
queryset=Tenant.objects.all(),
|
queryset=Tenant.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Tenant (slug)',
|
label='Tenant (slug)',
|
||||||
@@ -47,7 +61,7 @@ class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Site
|
model = Site
|
||||||
fields = ['q', 'name', 'facility', 'asn']
|
fields = ['q', 'name', 'slug', 'facility', 'asn', 'contact_name', 'contact_phone', 'contact_email']
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
if not value.strip():
|
if not value.strip():
|
||||||
@@ -57,6 +71,9 @@ class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
Q(facility__icontains=value) |
|
Q(facility__icontains=value) |
|
||||||
Q(physical_address__icontains=value) |
|
Q(physical_address__icontains=value) |
|
||||||
Q(shipping_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)
|
Q(comments__icontains=value)
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
@@ -68,7 +85,6 @@ class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
|
|
||||||
class RackGroupFilter(django_filters.FilterSet):
|
class RackGroupFilter(django_filters.FilterSet):
|
||||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
name='site',
|
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
label='Site (ID)',
|
label='Site (ID)',
|
||||||
)
|
)
|
||||||
@@ -81,7 +97,14 @@ class RackGroupFilter(django_filters.FilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = RackGroup
|
model = RackGroup
|
||||||
fields = ['name']
|
fields = ['site_id', 'name', 'slug']
|
||||||
|
|
||||||
|
|
||||||
|
class RackRoleFilter(django_filters.FilterSet):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = RackRole
|
||||||
|
fields = ['name', 'slug', 'color']
|
||||||
|
|
||||||
|
|
||||||
class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||||
@@ -91,7 +114,6 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
label='Search',
|
label='Search',
|
||||||
)
|
)
|
||||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
name='site',
|
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
label='Site (ID)',
|
label='Site (ID)',
|
||||||
)
|
)
|
||||||
@@ -102,7 +124,6 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
label='Site (slug)',
|
label='Site (slug)',
|
||||||
)
|
)
|
||||||
group_id = NullableModelMultipleChoiceFilter(
|
group_id = NullableModelMultipleChoiceFilter(
|
||||||
name='group',
|
|
||||||
queryset=RackGroup.objects.all(),
|
queryset=RackGroup.objects.all(),
|
||||||
label='Group (ID)',
|
label='Group (ID)',
|
||||||
)
|
)
|
||||||
@@ -113,7 +134,6 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
label='Group',
|
label='Group',
|
||||||
)
|
)
|
||||||
tenant_id = NullableModelMultipleChoiceFilter(
|
tenant_id = NullableModelMultipleChoiceFilter(
|
||||||
name='tenant',
|
|
||||||
queryset=Tenant.objects.all(),
|
queryset=Tenant.objects.all(),
|
||||||
label='Tenant (ID)',
|
label='Tenant (ID)',
|
||||||
)
|
)
|
||||||
@@ -124,7 +144,6 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
label='Tenant (slug)',
|
label='Tenant (slug)',
|
||||||
)
|
)
|
||||||
role_id = NullableModelMultipleChoiceFilter(
|
role_id = NullableModelMultipleChoiceFilter(
|
||||||
name='role',
|
|
||||||
queryset=RackRole.objects.all(),
|
queryset=RackRole.objects.all(),
|
||||||
label='Role (ID)',
|
label='Role (ID)',
|
||||||
)
|
)
|
||||||
@@ -137,7 +156,7 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Rack
|
model = Rack
|
||||||
fields = ['u_height']
|
fields = ['facility_id', 'type', 'width', 'u_height', 'desc_units']
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
if not value.strip():
|
if not value.strip():
|
||||||
@@ -155,6 +174,10 @@ class RackReservationFilter(django_filters.FilterSet):
|
|||||||
method='search',
|
method='search',
|
||||||
label='Search',
|
label='Search',
|
||||||
)
|
)
|
||||||
|
rack_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
queryset=Rack.objects.all(),
|
||||||
|
label='Rack (ID)',
|
||||||
|
)
|
||||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
name='rack__site',
|
name='rack__site',
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
@@ -177,15 +200,20 @@ class RackReservationFilter(django_filters.FilterSet):
|
|||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Group',
|
label='Group',
|
||||||
)
|
)
|
||||||
rack_id = django_filters.ModelMultipleChoiceFilter(
|
user_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
name='rack',
|
queryset=User.objects.all(),
|
||||||
queryset=Rack.objects.all(),
|
label='User (ID)',
|
||||||
label='Rack (ID)',
|
)
|
||||||
|
user = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
name='user',
|
||||||
|
queryset=User.objects.all(),
|
||||||
|
to_field_name='username',
|
||||||
|
label='User (name)',
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = RackReservation
|
model = RackReservation
|
||||||
fields = ['rack', 'user']
|
fields = ['created']
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
if not value.strip():
|
if not value.strip():
|
||||||
@@ -198,6 +226,13 @@ class RackReservationFilter(django_filters.FilterSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ManufacturerFilter(django_filters.FilterSet):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Manufacturer
|
||||||
|
fields = ['name', 'slug']
|
||||||
|
|
||||||
|
|
||||||
class DeviceTypeFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
class DeviceTypeFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||||
id__in = NumericInFilter(name='id', lookup_expr='in')
|
id__in = NumericInFilter(name='id', lookup_expr='in')
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
@@ -205,7 +240,6 @@ class DeviceTypeFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
label='Search',
|
label='Search',
|
||||||
)
|
)
|
||||||
manufacturer_id = django_filters.ModelMultipleChoiceFilter(
|
manufacturer_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
name='manufacturer',
|
|
||||||
queryset=Manufacturer.objects.all(),
|
queryset=Manufacturer.objects.all(),
|
||||||
label='Manufacturer (ID)',
|
label='Manufacturer (ID)',
|
||||||
)
|
)
|
||||||
@@ -219,7 +253,8 @@ class DeviceTypeFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = DeviceType
|
model = DeviceType
|
||||||
fields = [
|
fields = [
|
||||||
'model', 'part_number', 'u_height', 'is_console_server', 'is_pdu', 'is_network_device', 'subdevice_role',
|
'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'is_console_server', 'is_pdu',
|
||||||
|
'is_network_device', 'subdevice_role',
|
||||||
]
|
]
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
@@ -235,16 +270,9 @@ class DeviceTypeFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
|
|
||||||
class DeviceTypeComponentFilterSet(django_filters.FilterSet):
|
class DeviceTypeComponentFilterSet(django_filters.FilterSet):
|
||||||
devicetype_id = django_filters.ModelMultipleChoiceFilter(
|
devicetype_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
name='device_type',
|
|
||||||
queryset=DeviceType.objects.all(),
|
queryset=DeviceType.objects.all(),
|
||||||
label='Device type (ID)',
|
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):
|
class ConsolePortTemplateFilter(DeviceTypeComponentFilterSet):
|
||||||
@@ -279,7 +307,7 @@ class InterfaceTemplateFilter(DeviceTypeComponentFilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = InterfaceTemplate
|
model = InterfaceTemplate
|
||||||
fields = ['name', 'form_factor']
|
fields = ['name', 'form_factor', 'mgmt_only']
|
||||||
|
|
||||||
|
|
||||||
class DeviceBayTemplateFilter(DeviceTypeComponentFilterSet):
|
class DeviceBayTemplateFilter(DeviceTypeComponentFilterSet):
|
||||||
@@ -289,18 +317,73 @@ class DeviceBayTemplateFilter(DeviceTypeComponentFilterSet):
|
|||||||
fields = ['name']
|
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):
|
class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||||
id__in = NumericInFilter(name='id', lookup_expr='in')
|
id__in = NumericInFilter(name='id', lookup_expr='in')
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
method='search',
|
method='search',
|
||||||
label='Search',
|
label='Search',
|
||||||
)
|
)
|
||||||
mac_address = django_filters.CharFilter(
|
manufacturer_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
method='_mac_address',
|
name='device_type__manufacturer',
|
||||||
label='MAC address',
|
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)',
|
||||||
)
|
)
|
||||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
name='site',
|
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
label='Site (ID)',
|
label='Site (ID)',
|
||||||
)
|
)
|
||||||
@@ -320,60 +403,18 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
queryset=Rack.objects.all(),
|
queryset=Rack.objects.all(),
|
||||||
label='Rack (ID)',
|
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(
|
model = django_filters.ModelMultipleChoiceFilter(
|
||||||
name='device_type__slug',
|
name='device_type__slug',
|
||||||
queryset=DeviceType.objects.all(),
|
queryset=DeviceType.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Device model (slug)',
|
label='Device model (slug)',
|
||||||
)
|
)
|
||||||
platform_id = NullableModelMultipleChoiceFilter(
|
status = django_filters.MultipleChoiceFilter(
|
||||||
name='platform',
|
choices=STATUS_CHOICES
|
||||||
queryset=Platform.objects.all(),
|
|
||||||
label='Platform (ID)',
|
|
||||||
)
|
)
|
||||||
platform = NullableModelMultipleChoiceFilter(
|
is_full_depth = django_filters.BooleanFilter(
|
||||||
name='platform',
|
name='device_type__is_full_depth',
|
||||||
queryset=Platform.objects.all(),
|
label='Is full depth',
|
||||||
to_field_name='slug',
|
|
||||||
label='Platform (slug)',
|
|
||||||
)
|
)
|
||||||
is_console_server = django_filters.BooleanFilter(
|
is_console_server = django_filters.BooleanFilter(
|
||||||
name='device_type__is_console_server',
|
name='device_type__is_console_server',
|
||||||
@@ -387,13 +428,14 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
name='device_type__is_network_device',
|
name='device_type__is_network_device',
|
||||||
label='Is a network device',
|
label='Is a network device',
|
||||||
)
|
)
|
||||||
|
mac_address = django_filters.CharFilter(
|
||||||
|
method='_mac_address',
|
||||||
|
label='MAC address',
|
||||||
|
)
|
||||||
has_primary_ip = django_filters.BooleanFilter(
|
has_primary_ip = django_filters.BooleanFilter(
|
||||||
method='_has_primary_ip',
|
method='_has_primary_ip',
|
||||||
label='Has a primary IP',
|
label='Has a primary IP',
|
||||||
)
|
)
|
||||||
status = django_filters.MultipleChoiceFilter(
|
|
||||||
choices=STATUS_CHOICES
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Device
|
model = Device
|
||||||
@@ -433,13 +475,11 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
|
|
||||||
|
|
||||||
class DeviceComponentFilterSet(django_filters.FilterSet):
|
class DeviceComponentFilterSet(django_filters.FilterSet):
|
||||||
device_id = django_filters.ModelMultipleChoiceFilter(
|
device_id = django_filters.ModelChoiceFilter(
|
||||||
name='device',
|
|
||||||
queryset=Device.objects.all(),
|
queryset=Device.objects.all(),
|
||||||
label='Device (ID)',
|
label='Device (ID)',
|
||||||
)
|
)
|
||||||
device = django_filters.ModelMultipleChoiceFilter(
|
device = django_filters.ModelChoiceFilter(
|
||||||
name='device__name',
|
|
||||||
queryset=Device.objects.all(),
|
queryset=Device.objects.all(),
|
||||||
to_field_name='name',
|
to_field_name='name',
|
||||||
label='Device (name)',
|
label='Device (name)',
|
||||||
@@ -474,7 +514,21 @@ class PowerOutletFilter(DeviceComponentFilterSet):
|
|||||||
fields = ['name']
|
fields = ['name']
|
||||||
|
|
||||||
|
|
||||||
class InterfaceFilter(DeviceComponentFilterSet):
|
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)',
|
||||||
|
)
|
||||||
type = django_filters.CharFilter(
|
type = django_filters.CharFilter(
|
||||||
method='filter_type',
|
method='filter_type',
|
||||||
label='Interface type',
|
label='Interface type',
|
||||||
@@ -491,17 +545,24 @@ class InterfaceFilter(DeviceComponentFilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Interface
|
model = Interface
|
||||||
fields = ['name', 'form_factor']
|
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()
|
||||||
|
|
||||||
def filter_type(self, queryset, name, value):
|
def filter_type(self, queryset, name, value):
|
||||||
value = value.strip().lower()
|
value = value.strip().lower()
|
||||||
if value == 'physical':
|
return {
|
||||||
return queryset.exclude(form_factor__in=VIRTUAL_IFACE_TYPES)
|
'physical': queryset.exclude(form_factor__in=NONCONNECTABLE_IFACE_TYPES),
|
||||||
elif value == 'virtual':
|
'virtual': queryset.filter(form_factor__in=VIRTUAL_IFACE_TYPES),
|
||||||
return queryset.filter(form_factor__in=VIRTUAL_IFACE_TYPES)
|
'wireless': queryset.filter(form_factor__in=WIRELESS_IFACE_TYPES),
|
||||||
elif value == 'lag':
|
'lag': queryset.filter(form_factor=IFACE_FF_LAG),
|
||||||
return queryset.filter(form_factor=IFACE_FF_LAG)
|
}.get(value, queryset.none())
|
||||||
return queryset
|
|
||||||
|
|
||||||
def _mac_address(self, queryset, name, value):
|
def _mac_address(self, queryset, name, value):
|
||||||
value = value.strip()
|
value = value.strip()
|
||||||
@@ -521,10 +582,24 @@ class DeviceBayFilter(DeviceComponentFilterSet):
|
|||||||
|
|
||||||
|
|
||||||
class InventoryItemFilter(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:
|
class Meta:
|
||||||
model = InventoryItem
|
model = InventoryItem
|
||||||
fields = ['name']
|
fields = ['name', 'part_id', 'serial', 'asset_tag', 'discovered']
|
||||||
|
|
||||||
|
|
||||||
class ConsoleConnectionFilter(django_filters.FilterSet):
|
class ConsoleConnectionFilter(django_filters.FilterSet):
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import re
|
|||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib.postgres.forms.array import SimpleArrayField
|
from django.contrib.postgres.forms.array import SimpleArrayField
|
||||||
from django.core.exceptions import ValidationError
|
|
||||||
from django.db.models import Count, Q
|
from django.db.models import Count, Q
|
||||||
|
|
||||||
from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
|
from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
|
||||||
@@ -14,18 +13,18 @@ from tenancy.forms import TenancyForm
|
|||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
from utilities.forms import (
|
from utilities.forms import (
|
||||||
APISelect, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect,
|
APISelect, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect,
|
||||||
BulkImportForm, ChainedFieldsMixin, ChainedModelChoiceField, CommentField, CSVDataField, ExpandableNameField,
|
ChainedFieldsMixin, ChainedModelChoiceField, CommentField, ConfirmationForm, CSVChoiceField, ExpandableNameField,
|
||||||
FilterChoiceField, FlexibleModelChoiceField, Livesearch, SelectWithDisabled, SmallTextarea, SlugField,
|
FilterChoiceField, FlexibleModelChoiceField, Livesearch, SelectWithDisabled, SmallTextarea, SlugField,
|
||||||
FilterTreeNodeMultipleChoiceField,
|
FilterTreeNodeMultipleChoiceField,
|
||||||
)
|
)
|
||||||
from .formfields import MACAddressFormField
|
from .formfields import MACAddressFormField
|
||||||
from .models import (
|
from .models import (
|
||||||
DeviceBay, DeviceBayTemplate, CONNECTION_STATUS_CHOICES, CONNECTION_STATUS_PLANNED, CONNECTION_STATUS_CONNECTED,
|
DeviceBay, DeviceBayTemplate, CONNECTION_STATUS_CHOICES, CONNECTION_STATUS_CONNECTED, ConsolePort,
|
||||||
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceRole, DeviceType,
|
ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceRole, DeviceType, Interface,
|
||||||
Interface, IFACE_FF_CHOICES, IFACE_FF_LAG, IFACE_ORDERING_CHOICES, InterfaceConnection, InterfaceTemplate,
|
IFACE_FF_CHOICES, IFACE_FF_LAG, IFACE_ORDERING_CHOICES, InterfaceConnection, InterfaceTemplate, Manufacturer,
|
||||||
Manufacturer, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate,
|
InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, RACK_FACE_CHOICES,
|
||||||
RACK_TYPE_CHOICES, RACK_WIDTH_CHOICES, Rack, RackGroup, RackReservation, RackRole, Region, Site, STATUS_CHOICES,
|
RACK_TYPE_CHOICES, RACK_WIDTH_CHOICES, Rack, RackGroup, RackReservation, RackRole, RACK_WIDTH_19IN, RACK_WIDTH_23IN,
|
||||||
SUBDEVICE_ROLE_CHILD, SUBDEVICE_ROLE_PARENT, VIRTUAL_IFACE_TYPES,
|
Region, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHILD, SUBDEVICE_ROLE_PARENT,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -50,14 +49,6 @@ def get_device_by_name_or_pk(name):
|
|||||||
return device
|
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):
|
class DeviceComponentForm(BootstrapMixin, forms.Form):
|
||||||
"""
|
"""
|
||||||
Allow inclusion of the parent device as context for limiting field choices.
|
Allow inclusion of the parent device as context for limiting field choices.
|
||||||
@@ -107,27 +98,37 @@ class SiteForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class SiteFromCSVForm(forms.ModelForm):
|
class SiteCSVForm(forms.ModelForm):
|
||||||
region = forms.ModelChoiceField(
|
region = forms.ModelChoiceField(
|
||||||
Region.objects.all(), to_field_name='name', required=False, error_messages={
|
queryset=Region.objects.all(),
|
||||||
'invalid_choice': 'Tenant not found.'
|
required=False,
|
||||||
|
to_field_name='name',
|
||||||
|
help_text='Name of assigned region',
|
||||||
|
error_messages={
|
||||||
|
'invalid_choice': 'Region not found.',
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
tenant = forms.ModelChoiceField(
|
tenant = forms.ModelChoiceField(
|
||||||
Tenant.objects.all(), to_field_name='name', required=False, error_messages={
|
queryset=Tenant.objects.all(),
|
||||||
'invalid_choice': 'Tenant not found.'
|
required=False,
|
||||||
|
to_field_name='name',
|
||||||
|
help_text='Name of assigned tenant',
|
||||||
|
error_messages={
|
||||||
|
'invalid_choice': 'Tenant not found.',
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Site
|
model = Site
|
||||||
fields = [
|
fields = [
|
||||||
'name', 'slug', 'region', 'tenant', 'facility', 'asn', 'contact_name', 'contact_phone', 'contact_email',
|
'name', 'slug', 'region', 'tenant', 'facility', 'asn', 'physical_address', 'shipping_address',
|
||||||
|
'contact_name', 'contact_phone', 'contact_email', 'comments',
|
||||||
]
|
]
|
||||||
|
help_texts = {
|
||||||
|
'name': 'Site name',
|
||||||
class SiteImportForm(BootstrapMixin, BulkImportForm):
|
'slug': 'URL-friendly slug',
|
||||||
csv = CSVDataField(csv_form=SiteFromCSVForm)
|
'asn': '32-bit autonomous system number',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class SiteBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
class SiteBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||||
@@ -217,49 +218,73 @@ class RackForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class RackFromCSVForm(forms.ModelForm):
|
class RackCSVForm(forms.ModelForm):
|
||||||
site = forms.ModelChoiceField(queryset=Site.objects.all(), to_field_name='name',
|
site = forms.ModelChoiceField(
|
||||||
error_messages={'invalid_choice': 'Site not found.'})
|
queryset=Site.objects.all(),
|
||||||
group_name = forms.CharField(required=False)
|
to_field_name='name',
|
||||||
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
|
help_text='Name of parent site',
|
||||||
error_messages={'invalid_choice': 'Tenant not found.'})
|
error_messages={
|
||||||
role = forms.ModelChoiceField(RackRole.objects.all(), to_field_name='name', required=False,
|
'invalid_choice': 'Site not found.',
|
||||||
error_messages={'invalid_choice': 'Role not found.'})
|
}
|
||||||
type = forms.CharField(required=False)
|
)
|
||||||
|
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 Meta:
|
class Meta:
|
||||||
model = Rack
|
model = Rack
|
||||||
fields = ['site', 'group_name', 'name', 'facility_id', 'tenant', 'role', 'type', 'width', 'u_height',
|
fields = [
|
||||||
'desc_units']
|
'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',
|
||||||
|
}
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
|
||||||
|
super(RackCSVForm, self).clean()
|
||||||
|
|
||||||
site = self.cleaned_data.get('site')
|
site = self.cleaned_data.get('site')
|
||||||
group = self.cleaned_data.get('group_name')
|
group_name = self.cleaned_data.get('group_name')
|
||||||
|
|
||||||
# Validate rack group
|
# Validate rack group
|
||||||
if site and group:
|
if group_name:
|
||||||
try:
|
try:
|
||||||
self.instance.group = RackGroup.objects.get(site=site, name=group)
|
self.instance.group = RackGroup.objects.get(site=site, name=group_name)
|
||||||
except RackGroup.DoesNotExist:
|
except RackGroup.DoesNotExist:
|
||||||
self.add_error('group_name', "Invalid rack group ({})".format(group))
|
raise forms.ValidationError("Rack group {} not found for site {}".format(group_name, site))
|
||||||
|
|
||||||
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):
|
class RackBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||||
@@ -377,7 +402,9 @@ class DeviceTypeBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
|||||||
u_height = forms.IntegerField(min_value=1, required=False)
|
u_height = forms.IntegerField(min_value=1, required=False)
|
||||||
is_full_depth = forms.NullBooleanField(required=False, widget=BulkEditNullBooleanSelect, label='Is full depth')
|
is_full_depth = forms.NullBooleanField(required=False, widget=BulkEditNullBooleanSelect, label='Is full depth')
|
||||||
interface_ordering = forms.ChoiceField(choices=add_blank_choice(IFACE_ORDERING_CHOICES), required=False)
|
interface_ordering = forms.ChoiceField(choices=add_blank_choice(IFACE_ORDERING_CHOICES), required=False)
|
||||||
is_console_server = forms.NullBooleanField(required=False, widget=BulkEditNullBooleanSelect, label='Is full depth')
|
is_console_server = forms.NullBooleanField(
|
||||||
|
required=False, widget=BulkEditNullBooleanSelect, label='Is a console server'
|
||||||
|
)
|
||||||
is_pdu = forms.NullBooleanField(required=False, widget=BulkEditNullBooleanSelect, label='Is a PDU')
|
is_pdu = forms.NullBooleanField(required=False, widget=BulkEditNullBooleanSelect, label='Is a PDU')
|
||||||
is_network_device = forms.NullBooleanField(
|
is_network_device = forms.NullBooleanField(
|
||||||
required=False, widget=BulkEditNullBooleanSelect, label='Is a network device'
|
required=False, widget=BulkEditNullBooleanSelect, label='Is a network device'
|
||||||
@@ -531,7 +558,7 @@ class PlatformForm(BootstrapMixin, forms.ModelForm):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Platform
|
model = Platform
|
||||||
fields = ['name', 'slug', 'rpc_client']
|
fields = ['name', 'slug', 'napalm_driver', 'rpc_client']
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -605,7 +632,7 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
|||||||
instance = kwargs.get('instance')
|
instance = kwargs.get('instance')
|
||||||
# Using hasattr() instead of "is not None" to avoid RelatedObjectDoesNotExist on required field
|
# Using hasattr() instead of "is not None" to avoid RelatedObjectDoesNotExist on required field
|
||||||
if instance and hasattr(instance, 'device_type'):
|
if instance and hasattr(instance, 'device_type'):
|
||||||
initial = kwargs.get('initial', {})
|
initial = kwargs.get('initial', {}).copy()
|
||||||
initial['manufacturer'] = instance.device_type.manufacturer
|
initial['manufacturer'] = instance.device_type.manufacturer
|
||||||
kwargs['initial'] = initial
|
kwargs['initial'] = initial
|
||||||
|
|
||||||
@@ -663,32 +690,60 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
|||||||
self.initial['rack'] = self.instance.parent_bay.device.rack_id
|
self.initial['rack'] = self.instance.parent_bay.device.rack_id
|
||||||
|
|
||||||
|
|
||||||
class BaseDeviceFromCSVForm(forms.ModelForm):
|
class BaseDeviceCSVForm(forms.ModelForm):
|
||||||
device_role = forms.ModelChoiceField(
|
device_role = forms.ModelChoiceField(
|
||||||
queryset=DeviceRole.objects.all(), to_field_name='name',
|
queryset=DeviceRole.objects.all(),
|
||||||
error_messages={'invalid_choice': 'Invalid device role.'}
|
to_field_name='name',
|
||||||
|
help_text='Name of assigned role',
|
||||||
|
error_messages={
|
||||||
|
'invalid_choice': 'Invalid device role.',
|
||||||
|
}
|
||||||
)
|
)
|
||||||
tenant = forms.ModelChoiceField(
|
tenant = forms.ModelChoiceField(
|
||||||
Tenant.objects.all(), to_field_name='name', required=False,
|
queryset=Tenant.objects.all(),
|
||||||
error_messages={'invalid_choice': 'Tenant not found.'}
|
required=False,
|
||||||
|
to_field_name='name',
|
||||||
|
help_text='Name of assigned tenant',
|
||||||
|
error_messages={
|
||||||
|
'invalid_choice': 'Tenant not found.',
|
||||||
|
}
|
||||||
)
|
)
|
||||||
manufacturer = forms.ModelChoiceField(
|
manufacturer = forms.ModelChoiceField(
|
||||||
queryset=Manufacturer.objects.all(), to_field_name='name',
|
queryset=Manufacturer.objects.all(),
|
||||||
error_messages={'invalid_choice': 'Invalid manufacturer.'}
|
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'
|
||||||
)
|
)
|
||||||
model_name = forms.CharField()
|
|
||||||
platform = forms.ModelChoiceField(
|
platform = forms.ModelChoiceField(
|
||||||
queryset=Platform.objects.all(), required=False, to_field_name='name',
|
queryset=Platform.objects.all(),
|
||||||
error_messages={'invalid_choice': 'Invalid platform.'}
|
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'
|
||||||
)
|
)
|
||||||
status = forms.CharField()
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
fields = []
|
fields = []
|
||||||
model = Device
|
model = Device
|
||||||
|
help_texts = {
|
||||||
|
'name': 'Device name',
|
||||||
|
}
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
|
||||||
|
super(BaseDeviceCSVForm, self).clean()
|
||||||
|
|
||||||
manufacturer = self.cleaned_data.get('manufacturer')
|
manufacturer = self.cleaned_data.get('manufacturer')
|
||||||
model_name = self.cleaned_data.get('model_name')
|
model_name = self.cleaned_data.get('model_name')
|
||||||
|
|
||||||
@@ -697,70 +752,73 @@ class BaseDeviceFromCSVForm(forms.ModelForm):
|
|||||||
try:
|
try:
|
||||||
self.instance.device_type = DeviceType.objects.get(manufacturer=manufacturer, model=model_name)
|
self.instance.device_type = DeviceType.objects.get(manufacturer=manufacturer, model=model_name)
|
||||||
except DeviceType.DoesNotExist:
|
except DeviceType.DoesNotExist:
|
||||||
self.add_error('model_name', "Invalid device type ({} {})".format(manufacturer, model_name))
|
raise forms.ValidationError("Device type {} {} not found".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 DeviceFromCSVForm(BaseDeviceFromCSVForm):
|
class DeviceCSVForm(BaseDeviceCSVForm):
|
||||||
site = forms.ModelChoiceField(
|
site = forms.ModelChoiceField(
|
||||||
queryset=Site.objects.all(), to_field_name='name', error_messages={
|
queryset=Site.objects.all(),
|
||||||
|
to_field_name='name',
|
||||||
|
help_text='Name of parent site',
|
||||||
|
error_messages={
|
||||||
'invalid_choice': 'Invalid site name.',
|
'invalid_choice': 'Invalid site name.',
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
rack_name = forms.CharField(required=False)
|
rack_group = forms.CharField(
|
||||||
face = forms.CharField(required=False)
|
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'
|
||||||
|
)
|
||||||
|
|
||||||
class Meta(BaseDeviceFromCSVForm.Meta):
|
class Meta(BaseDeviceCSVForm.Meta):
|
||||||
fields = [
|
fields = [
|
||||||
'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag', 'status',
|
'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag', 'status',
|
||||||
'site', 'rack_name', 'position', 'face',
|
'site', 'rack_group', 'rack_name', 'position', 'face',
|
||||||
]
|
]
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
|
||||||
super(DeviceFromCSVForm, self).clean()
|
super(DeviceCSVForm, self).clean()
|
||||||
|
|
||||||
site = self.cleaned_data.get('site')
|
site = self.cleaned_data.get('site')
|
||||||
|
rack_group = self.cleaned_data.get('rack_group')
|
||||||
rack_name = self.cleaned_data.get('rack_name')
|
rack_name = self.cleaned_data.get('rack_name')
|
||||||
|
|
||||||
# Validate rack
|
# Validate rack
|
||||||
if site and rack_name:
|
if site and rack_group and rack_name:
|
||||||
try:
|
try:
|
||||||
self.instance.rack = Rack.objects.get(site=site, name=rack_name)
|
self.instance.rack = Rack.objects.get(site=site, group__name=rack_group, name=rack_name)
|
||||||
except Rack.DoesNotExist:
|
except Rack.DoesNotExist:
|
||||||
self.add_error('rack_name', "Invalid rack ({})".format(rack_name))
|
raise forms.ValidationError("Rack {} not found in site {} group {}".format(rack_name, site, rack_group))
|
||||||
|
elif site and rack_name:
|
||||||
def clean_face(self):
|
|
||||||
face = self.cleaned_data['face']
|
|
||||||
if not face:
|
|
||||||
return None
|
|
||||||
try:
|
try:
|
||||||
return {
|
self.instance.rack = Rack.objects.get(site=site, group__isnull=True, name=rack_name)
|
||||||
'front': 0,
|
except Rack.DoesNotExist:
|
||||||
'rear': 1,
|
raise forms.ValidationError("Rack {} not found in site {} (no group)".format(rack_name, site))
|
||||||
}[face.lower()]
|
|
||||||
except KeyError:
|
|
||||||
raise forms.ValidationError('Invalid rack face ({}); must be "front" or "rear".'.format(face))
|
|
||||||
|
|
||||||
|
|
||||||
class ChildDeviceFromCSVForm(BaseDeviceFromCSVForm):
|
class ChildDeviceCSVForm(BaseDeviceCSVForm):
|
||||||
parent = FlexibleModelChoiceField(
|
parent = FlexibleModelChoiceField(
|
||||||
queryset=Device.objects.all(),
|
queryset=Device.objects.all(),
|
||||||
to_field_name='name',
|
to_field_name='name',
|
||||||
required=False,
|
help_text='Name or ID of parent device',
|
||||||
error_messages={
|
error_messages={
|
||||||
'invalid_choice': 'Parent device not found.'
|
'invalid_choice': 'Parent device not found.',
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
device_bay_name = forms.CharField(required=False)
|
device_bay_name = forms.CharField(
|
||||||
|
help_text='Name of device bay',
|
||||||
|
)
|
||||||
|
|
||||||
class Meta(BaseDeviceFromCSVForm.Meta):
|
class Meta(BaseDeviceCSVForm.Meta):
|
||||||
fields = [
|
fields = [
|
||||||
'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag', 'status',
|
'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag', 'status',
|
||||||
'parent', 'device_bay_name',
|
'parent', 'device_bay_name',
|
||||||
@@ -768,7 +826,7 @@ class ChildDeviceFromCSVForm(BaseDeviceFromCSVForm):
|
|||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
|
||||||
super(ChildDeviceFromCSVForm, self).clean()
|
super(ChildDeviceCSVForm, self).clean()
|
||||||
|
|
||||||
parent = self.cleaned_data.get('parent')
|
parent = self.cleaned_data.get('parent')
|
||||||
device_bay_name = self.cleaned_data.get('device_bay_name')
|
device_bay_name = self.cleaned_data.get('device_bay_name')
|
||||||
@@ -776,22 +834,12 @@ class ChildDeviceFromCSVForm(BaseDeviceFromCSVForm):
|
|||||||
# Validate device bay
|
# Validate device bay
|
||||||
if parent and device_bay_name:
|
if parent and device_bay_name:
|
||||||
try:
|
try:
|
||||||
device_bay = DeviceBay.objects.get(device=parent, name=device_bay_name)
|
self.instance.parent_bay = DeviceBay.objects.get(device=parent, name=device_bay_name)
|
||||||
if device_bay.installed_device:
|
# Inherit site and rack from parent device
|
||||||
self.add_error('device_bay_name',
|
self.instance.site = parent.site
|
||||||
"Device bay ({} {}) is already occupied".format(parent, device_bay_name))
|
self.instance.rack = parent.rack
|
||||||
else:
|
|
||||||
self.instance.parent_bay = device_bay
|
|
||||||
except DeviceBay.DoesNotExist:
|
except DeviceBay.DoesNotExist:
|
||||||
self.add_error('device_bay_name', "Parent device/bay ({} {}) not found".format(parent, device_bay_name))
|
raise forms.ValidationError("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):
|
class DeviceBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||||
@@ -889,75 +937,84 @@ class ConsolePortCreateForm(DeviceComponentForm):
|
|||||||
name_pattern = ExpandableNameField(label='Name')
|
name_pattern = ExpandableNameField(label='Name')
|
||||||
|
|
||||||
|
|
||||||
class ConsoleConnectionCSVForm(forms.Form):
|
class ConsoleConnectionCSVForm(forms.ModelForm):
|
||||||
console_server = FlexibleModelChoiceField(
|
console_server = FlexibleModelChoiceField(
|
||||||
queryset=Device.objects.filter(device_type__is_console_server=True),
|
queryset=Device.objects.filter(device_type__is_console_server=True),
|
||||||
to_field_name='name',
|
to_field_name='name',
|
||||||
|
help_text='Console server name or ID',
|
||||||
error_messages={
|
error_messages={
|
||||||
'invalid_choice': 'Console server not found',
|
'invalid_choice': 'Console server not found',
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
cs_port = forms.CharField()
|
cs_port = forms.CharField(
|
||||||
device = FlexibleModelChoiceField(queryset=Device.objects.all(), to_field_name='name',
|
help_text='Console server port name'
|
||||||
error_messages={'invalid_choice': 'Device not found'})
|
)
|
||||||
console_port = forms.CharField()
|
device = FlexibleModelChoiceField(
|
||||||
status = forms.CharField(validators=[validate_connection_status])
|
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'
|
||||||
|
)
|
||||||
|
|
||||||
def clean(self):
|
class Meta:
|
||||||
|
model = ConsolePort
|
||||||
|
fields = ['console_server', 'cs_port', 'device', 'console_port', 'connection_status']
|
||||||
|
|
||||||
|
def clean_console_port(self):
|
||||||
|
|
||||||
|
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 server port
|
|
||||||
if self.cleaned_data.get('console_server'):
|
|
||||||
try:
|
try:
|
||||||
cs_port = ConsoleServerPort.objects.get(device=self.cleaned_data['console_server'],
|
# Retrieve console port by name
|
||||||
name=self.cleaned_data['cs_port'])
|
consoleport = ConsolePort.objects.get(
|
||||||
if ConsolePort.objects.filter(cs_port=cs_port):
|
device=self.cleaned_data['device'], name=console_port_name
|
||||||
raise forms.ValidationError("Console server port is already occupied (by {} {})"
|
)
|
||||||
.format(cs_port.connected_console.device, cs_port.connected_console))
|
# Check if the console port is already connected
|
||||||
except ConsoleServerPort.DoesNotExist:
|
if consoleport.cs_port is not None:
|
||||||
raise forms.ValidationError("Invalid console server port ({} {})"
|
raise forms.ValidationError("{} {} is already connected".format(
|
||||||
.format(self.cleaned_data['console_server'], self.cleaned_data['cs_port']))
|
self.cleaned_data['device'], console_port_name
|
||||||
|
))
|
||||||
# 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:
|
except ConsolePort.DoesNotExist:
|
||||||
raise forms.ValidationError("Invalid console port ({} {})"
|
raise forms.ValidationError("Invalid console port ({} {})".format(
|
||||||
.format(self.cleaned_data['device'], self.cleaned_data['console_port']))
|
self.cleaned_data['device'], console_port_name
|
||||||
|
))
|
||||||
|
|
||||||
|
self.instance = consoleport
|
||||||
|
return consoleport
|
||||||
|
|
||||||
class ConsoleConnectionImportForm(BootstrapMixin, BulkImportForm):
|
def clean_cs_port(self):
|
||||||
csv = CSVDataField(csv_form=ConsoleConnectionCSVForm)
|
|
||||||
|
|
||||||
def clean(self):
|
cs_port_name = self.cleaned_data.get('cs_port')
|
||||||
records = self.cleaned_data.get('csv')
|
if not self.cleaned_data.get('console_server') or not cs_port_name:
|
||||||
if not records:
|
return None
|
||||||
return
|
|
||||||
|
|
||||||
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):
|
return cs_port
|
||||||
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))
|
|
||||||
|
|
||||||
self.cleaned_data['csv'] = connection_list
|
|
||||||
|
|
||||||
|
|
||||||
class ConsolePortConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm):
|
class ConsolePortConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm):
|
||||||
@@ -1119,6 +1176,10 @@ class ConsoleServerPortConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ConsoleServerPortBulkDisconnectForm(ConfirmationForm):
|
||||||
|
pk = forms.ModelMultipleChoiceField(queryset=ConsoleServerPort.objects.all(), widget=forms.MultipleHiddenInput)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Power ports
|
# Power ports
|
||||||
#
|
#
|
||||||
@@ -1137,76 +1198,84 @@ class PowerPortCreateForm(DeviceComponentForm):
|
|||||||
name_pattern = ExpandableNameField(label='Name')
|
name_pattern = ExpandableNameField(label='Name')
|
||||||
|
|
||||||
|
|
||||||
class PowerConnectionCSVForm(forms.Form):
|
class PowerConnectionCSVForm(forms.ModelForm):
|
||||||
pdu = FlexibleModelChoiceField(
|
pdu = FlexibleModelChoiceField(
|
||||||
queryset=Device.objects.filter(device_type__is_pdu=True),
|
queryset=Device.objects.filter(device_type__is_pdu=True),
|
||||||
to_field_name='name',
|
to_field_name='name',
|
||||||
|
help_text='PDU name or ID',
|
||||||
error_messages={
|
error_messages={
|
||||||
'invalid_choice': 'PDU not found.',
|
'invalid_choice': 'PDU not found.',
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
power_outlet = forms.CharField()
|
power_outlet = forms.CharField(
|
||||||
device = FlexibleModelChoiceField(queryset=Device.objects.all(), to_field_name='name',
|
help_text='Power outlet name'
|
||||||
error_messages={'invalid_choice': 'Device not found'})
|
)
|
||||||
power_port = forms.CharField()
|
device = FlexibleModelChoiceField(
|
||||||
status = forms.CharField(validators=[validate_connection_status])
|
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'
|
||||||
|
)
|
||||||
|
|
||||||
def clean(self):
|
class Meta:
|
||||||
|
model = PowerPort
|
||||||
|
fields = ['pdu', 'power_outlet', 'device', 'power_port', 'connection_status']
|
||||||
|
|
||||||
|
def clean_power_port(self):
|
||||||
|
|
||||||
|
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 outlet
|
|
||||||
if self.cleaned_data.get('pdu'):
|
|
||||||
try:
|
try:
|
||||||
power_outlet = PowerOutlet.objects.get(device=self.cleaned_data['pdu'],
|
# Retrieve power port by name
|
||||||
name=self.cleaned_data['power_outlet'])
|
powerport = PowerPort.objects.get(
|
||||||
if PowerPort.objects.filter(power_outlet=power_outlet):
|
device=self.cleaned_data['device'], name=power_port_name
|
||||||
raise forms.ValidationError("Power outlet is already occupied (by {} {})"
|
)
|
||||||
.format(power_outlet.connected_port.device,
|
# Check if the power port is already connected
|
||||||
power_outlet.connected_port))
|
if powerport.power_outlet is not None:
|
||||||
except PowerOutlet.DoesNotExist:
|
raise forms.ValidationError("{} {} is already connected".format(
|
||||||
raise forms.ValidationError("Invalid PDU port ({} {})"
|
self.cleaned_data['device'], power_port_name
|
||||||
.format(self.cleaned_data['pdu'], self.cleaned_data['power_outlet']))
|
))
|
||||||
|
|
||||||
# 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:
|
except PowerPort.DoesNotExist:
|
||||||
raise forms.ValidationError("Invalid power port ({} {})"
|
raise forms.ValidationError("Invalid power port ({} {})".format(
|
||||||
.format(self.cleaned_data['device'], self.cleaned_data['power_port']))
|
self.cleaned_data['device'], power_port_name
|
||||||
|
))
|
||||||
|
|
||||||
|
self.instance = powerport
|
||||||
|
return powerport
|
||||||
|
|
||||||
class PowerConnectionImportForm(BootstrapMixin, BulkImportForm):
|
def clean_power_outlet(self):
|
||||||
csv = CSVDataField(csv_form=PowerConnectionCSVForm)
|
|
||||||
|
|
||||||
def clean(self):
|
power_outlet_name = self.cleaned_data.get('power_outlet')
|
||||||
records = self.cleaned_data.get('csv')
|
if not self.cleaned_data.get('pdu') or not power_outlet_name:
|
||||||
if not records:
|
return None
|
||||||
return
|
|
||||||
|
|
||||||
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):
|
return power_outlet
|
||||||
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))
|
|
||||||
|
|
||||||
self.cleaned_data['csv'] = connection_list
|
|
||||||
|
|
||||||
|
|
||||||
class PowerPortConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm):
|
class PowerPortConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm):
|
||||||
@@ -1368,6 +1437,10 @@ class PowerOutletConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.Form):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class PowerOutletBulkDisconnectForm(ConfirmationForm):
|
||||||
|
pk = forms.ModelMultipleChoiceField(queryset=PowerOutlet.objects.all(), widget=forms.MultipleHiddenInput)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Interfaces
|
# Interfaces
|
||||||
#
|
#
|
||||||
@@ -1376,7 +1449,7 @@ class InterfaceForm(BootstrapMixin, forms.ModelForm):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Interface
|
model = Interface
|
||||||
fields = ['device', 'name', 'form_factor', 'lag', 'mac_address', 'mgmt_only', 'description']
|
fields = ['device', 'name', 'form_factor', 'enabled', 'lag', 'mac_address', 'mtu', 'mgmt_only', 'description']
|
||||||
widgets = {
|
widgets = {
|
||||||
'device': forms.HiddenInput(),
|
'device': forms.HiddenInput(),
|
||||||
}
|
}
|
||||||
@@ -1398,12 +1471,19 @@ class InterfaceForm(BootstrapMixin, forms.ModelForm):
|
|||||||
class InterfaceCreateForm(DeviceComponentForm):
|
class InterfaceCreateForm(DeviceComponentForm):
|
||||||
name_pattern = ExpandableNameField(label='Name')
|
name_pattern = ExpandableNameField(label='Name')
|
||||||
form_factor = forms.ChoiceField(choices=IFACE_FF_CHOICES)
|
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')
|
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')
|
mac_address = MACAddressFormField(required=False, label='MAC Address')
|
||||||
mgmt_only = forms.BooleanField(required=False, label='OOB Management')
|
mgmt_only = forms.BooleanField(required=False, label='OOB Management')
|
||||||
description = forms.CharField(max_length=100, required=False)
|
description = forms.CharField(max_length=100, required=False)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
|
||||||
|
# Set interfaces enabled by default
|
||||||
|
kwargs['initial'] = kwargs.get('initial', {}).copy()
|
||||||
|
kwargs['initial'].update({'enabled': True})
|
||||||
|
|
||||||
super(InterfaceCreateForm, self).__init__(*args, **kwargs)
|
super(InterfaceCreateForm, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
# Limit LAG choices to interfaces belonging to this device
|
# Limit LAG choices to interfaces belonging to this device
|
||||||
@@ -1418,13 +1498,15 @@ class InterfaceCreateForm(DeviceComponentForm):
|
|||||||
class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
|
class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||||
pk = forms.ModelMultipleChoiceField(queryset=Interface.objects.all(), widget=forms.MultipleHiddenInput)
|
pk = forms.ModelMultipleChoiceField(queryset=Interface.objects.all(), widget=forms.MultipleHiddenInput)
|
||||||
device = forms.ModelChoiceField(queryset=Device.objects.all(), widget=forms.HiddenInput)
|
device = forms.ModelChoiceField(queryset=Device.objects.all(), widget=forms.HiddenInput)
|
||||||
lag = forms.ModelChoiceField(queryset=Interface.objects.all(), required=False, label='Parent LAG')
|
|
||||||
form_factor = forms.ChoiceField(choices=add_blank_choice(IFACE_FF_CHOICES), required=False)
|
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')
|
||||||
mgmt_only = forms.NullBooleanField(required=False, widget=BulkEditNullBooleanSelect, label='Management only')
|
mgmt_only = forms.NullBooleanField(required=False, widget=BulkEditNullBooleanSelect, label='Management only')
|
||||||
description = forms.CharField(max_length=100, required=False)
|
description = forms.CharField(max_length=100, required=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
nullable_fields = ['lag', 'description']
|
nullable_fields = ['lag', 'mtu', 'description']
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(InterfaceBulkEditForm, self).__init__(*args, **kwargs)
|
super(InterfaceBulkEditForm, self).__init__(*args, **kwargs)
|
||||||
@@ -1445,6 +1527,10 @@ class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
|
|||||||
self.fields['lag'].choices = []
|
self.fields['lag'].choices = []
|
||||||
|
|
||||||
|
|
||||||
|
class InterfaceBulkDisconnectForm(ConfirmationForm):
|
||||||
|
pk = forms.ModelMultipleChoiceField(queryset=Interface.objects.all(), widget=forms.MultipleHiddenInput)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Interface connections
|
# Interface connections
|
||||||
#
|
#
|
||||||
@@ -1499,7 +1585,7 @@ class InterfaceConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelFor
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
interface_b = ChainedModelChoiceField(
|
interface_b = ChainedModelChoiceField(
|
||||||
queryset=Interface.objects.exclude(form_factor__in=VIRTUAL_IFACE_TYPES).select_related(
|
queryset=Interface.objects.connectable().select_related(
|
||||||
'circuit_termination', 'connected_as_a', 'connected_as_b'
|
'circuit_termination', 'connected_as_a', 'connected_as_b'
|
||||||
),
|
),
|
||||||
chains=(
|
chains=(
|
||||||
@@ -1508,7 +1594,7 @@ class InterfaceConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelFor
|
|||||||
label='Interface',
|
label='Interface',
|
||||||
widget=APISelect(
|
widget=APISelect(
|
||||||
api_url='/api/dcim/interfaces/?device_id={{device_b}}&type=physical',
|
api_url='/api/dcim/interfaces/?device_id={{device_b}}&type=physical',
|
||||||
disabled_indicator='connection'
|
disabled_indicator='is_connected'
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1521,9 +1607,7 @@ class InterfaceConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelFor
|
|||||||
super(InterfaceConnectionForm, self).__init__(*args, **kwargs)
|
super(InterfaceConnectionForm, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
# Initialize interface A choices
|
# Initialize interface A choices
|
||||||
device_a_interfaces = Interface.objects.order_naturally().filter(device=device_a).exclude(
|
device_a_interfaces = Interface.objects.connectable().order_naturally().filter(device=device_a).select_related(
|
||||||
form_factor__in=VIRTUAL_IFACE_TYPES
|
|
||||||
).select_related(
|
|
||||||
'circuit_termination', 'connected_as_a', 'connected_as_b'
|
'circuit_termination', 'connected_as_a', 'connected_as_b'
|
||||||
)
|
)
|
||||||
self.fields['interface_a'].choices = [
|
self.fields['interface_a'].choices = [
|
||||||
@@ -1531,103 +1615,88 @@ class InterfaceConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelFor
|
|||||||
]
|
]
|
||||||
|
|
||||||
# Mark connected interfaces as disabled
|
# Mark connected interfaces as disabled
|
||||||
|
if self.data.get('device_b'):
|
||||||
self.fields['interface_b'].choices = [
|
self.fields['interface_b'].choices = [
|
||||||
(iface.id, {'label': iface.name, 'disabled': iface.is_connected}) for iface in self.fields['interface_b'].queryset
|
(iface.id, {'label': iface.name, 'disabled': iface.is_connected}) for iface in self.fields['interface_b'].queryset
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class InterfaceConnectionCSVForm(forms.Form):
|
class InterfaceConnectionCSVForm(forms.ModelForm):
|
||||||
device_a = FlexibleModelChoiceField(
|
device_a = FlexibleModelChoiceField(
|
||||||
queryset=Device.objects.all(),
|
queryset=Device.objects.all(),
|
||||||
to_field_name='name',
|
to_field_name='name',
|
||||||
|
help_text='Name or ID of device A',
|
||||||
error_messages={'invalid_choice': 'Device A not found.'}
|
error_messages={'invalid_choice': 'Device A not found.'}
|
||||||
)
|
)
|
||||||
interface_a = forms.CharField()
|
interface_a = forms.CharField(
|
||||||
|
help_text='Name of interface A'
|
||||||
|
)
|
||||||
device_b = FlexibleModelChoiceField(
|
device_b = FlexibleModelChoiceField(
|
||||||
queryset=Device.objects.all(),
|
queryset=Device.objects.all(),
|
||||||
to_field_name='name',
|
to_field_name='name',
|
||||||
|
help_text='Name or ID of device B',
|
||||||
error_messages={'invalid_choice': 'Device B not found.'}
|
error_messages={'invalid_choice': 'Device B not found.'}
|
||||||
)
|
)
|
||||||
interface_b = forms.CharField()
|
interface_b = forms.CharField(
|
||||||
status = forms.CharField(
|
help_text='Name of interface B'
|
||||||
validators=[validate_connection_status]
|
)
|
||||||
|
connection_status = CSVChoiceField(
|
||||||
|
choices=CONNECTION_STATUS_CHOICES,
|
||||||
|
help_text='Connection status'
|
||||||
)
|
)
|
||||||
|
|
||||||
def clean(self):
|
class Meta:
|
||||||
|
model = InterfaceConnection
|
||||||
|
fields = ['device_a', 'interface_a', 'device_b', 'interface_b', 'connection_status']
|
||||||
|
|
||||||
|
def clean_interface_a(self):
|
||||||
|
|
||||||
|
interface_name = self.cleaned_data.get('interface_a')
|
||||||
|
if not interface_name:
|
||||||
|
return None
|
||||||
|
|
||||||
# Validate interface A
|
|
||||||
if self.cleaned_data.get('device_a'):
|
|
||||||
try:
|
try:
|
||||||
interface_a = Interface.objects.get(device=self.cleaned_data['device_a'],
|
# Retrieve interface by name
|
||||||
name=self.cleaned_data['interface_a'])
|
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:
|
except Interface.DoesNotExist:
|
||||||
raise forms.ValidationError("Invalid interface ({} {})"
|
raise forms.ValidationError("Invalid interface ({} {})".format(
|
||||||
.format(self.cleaned_data['device_a'], self.cleaned_data['interface_a']))
|
self.cleaned_data['device_a'], interface_name
|
||||||
try:
|
))
|
||||||
InterfaceConnection.objects.get(Q(interface_a=interface_a) | Q(interface_b=interface_a))
|
|
||||||
raise forms.ValidationError("{} {} is already connected"
|
return interface
|
||||||
.format(self.cleaned_data['device_a'], self.cleaned_data['interface_a']))
|
|
||||||
except InterfaceConnection.DoesNotExist:
|
def clean_interface_b(self):
|
||||||
pass
|
|
||||||
|
interface_name = self.cleaned_data.get('interface_b')
|
||||||
|
if not interface_name:
|
||||||
|
return None
|
||||||
|
|
||||||
# Validate interface B
|
|
||||||
if self.cleaned_data.get('device_b'):
|
|
||||||
try:
|
try:
|
||||||
interface_b = Interface.objects.get(device=self.cleaned_data['device_b'],
|
# Retrieve interface by name
|
||||||
name=self.cleaned_data['interface_b'])
|
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:
|
except Interface.DoesNotExist:
|
||||||
raise forms.ValidationError("Invalid interface ({} {})"
|
raise forms.ValidationError("Invalid interface ({} {})".format(
|
||||||
.format(self.cleaned_data['device_b'], self.cleaned_data['interface_b']))
|
self.cleaned_data['device_b'], interface_name
|
||||||
try:
|
))
|
||||||
InterfaceConnection.objects.get(Q(interface_a=interface_b) | Q(interface_b=interface_b))
|
|
||||||
raise forms.ValidationError("{} {} is already connected"
|
return interface
|
||||||
.format(self.cleaned_data['device_b'], self.cleaned_data['interface_b']))
|
|
||||||
except InterfaceConnection.DoesNotExist:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class InterfaceConnectionImportForm(BootstrapMixin, BulkImportForm):
|
class InterfaceConnectionDeletionForm(ConfirmationForm):
|
||||||
csv = CSVDataField(csv_form=InterfaceConnectionCSVForm)
|
|
||||||
|
|
||||||
def clean(self):
|
|
||||||
records = self.cleaned_data.get('csv')
|
|
||||||
if not records:
|
|
||||||
return
|
|
||||||
|
|
||||||
connection_list = []
|
|
||||||
occupied_interfaces = []
|
|
||||||
|
|
||||||
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))
|
|
||||||
|
|
||||||
self.cleaned_data['csv'] = connection_list
|
|
||||||
|
|
||||||
|
|
||||||
class InterfaceConnectionDeletionForm(BootstrapMixin, forms.Form):
|
|
||||||
confirm = forms.BooleanField(required=True)
|
|
||||||
# Used for HTTP redirect upon successful deletion
|
# Used for HTTP redirect upon successful deletion
|
||||||
device = forms.ModelChoiceField(queryset=Device.objects.all(), widget=forms.HiddenInput(), required=False)
|
device = forms.ModelChoiceField(queryset=Device.objects.all(), widget=forms.HiddenInput(), required=False)
|
||||||
|
|
||||||
@@ -1697,4 +1766,4 @@ class InventoryItemForm(BootstrapMixin, forms.ModelForm):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = InventoryItem
|
model = InventoryItem
|
||||||
fields = ['name', 'manufacturer', 'part_id', 'serial']
|
fields = ['name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description']
|
||||||
|
|||||||
25
netbox/dcim/migrations/0038_wireless_interfaces.py
Normal file
25
netbox/dcim/migrations/0038_wireless_interfaces.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# -*- 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),
|
||||||
|
),
|
||||||
|
]
|
||||||
25
netbox/dcim/migrations/0039_interface_add_enabled_mtu.py
Normal file
25
netbox/dcim/migrations/0039_interface_add_enabled_mtu.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# -*- 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'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# -*- 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),
|
||||||
|
),
|
||||||
|
]
|
||||||
40
netbox/dcim/migrations/0041_napalm_integration.py
Normal file
40
netbox/dcim/migrations/0041_napalm_integration.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.3 on 2017-07-14 17:26
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
def rpc_client_to_napalm_driver(apps, schema_editor):
|
||||||
|
"""
|
||||||
|
Migrate legacy RPC clients to their respective NAPALM drivers
|
||||||
|
"""
|
||||||
|
Platform = apps.get_model('dcim', 'Platform')
|
||||||
|
|
||||||
|
Platform.objects.filter(rpc_client='juniper-junos').update(napalm_driver='junos')
|
||||||
|
Platform.objects.filter(rpc_client='cisco-ios').update(napalm_driver='ios')
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('dcim', '0040_inventoryitem_add_asset_tag_description'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='device',
|
||||||
|
options={'ordering': ['name'], 'permissions': (('napalm_read', 'Read-only access to devices via NAPALM'), ('napalm_write', 'Read/write access to devices via NAPALM'))},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='platform',
|
||||||
|
name='napalm_driver',
|
||||||
|
field=models.CharField(blank=True, help_text='The name of the NAPALM driver to use when interacting with devices.', max_length=50, verbose_name='NAPALM driver'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='platform',
|
||||||
|
name='rpc_client',
|
||||||
|
field=models.CharField(blank=True, choices=[['juniper-junos', 'Juniper Junos (NETCONF)'], ['cisco-ios', 'Cisco IOS (SSH)'], ['opengear', 'Opengear (SSH)']], max_length=30, verbose_name='Legacy RPC client'),
|
||||||
|
),
|
||||||
|
migrations.RunPython(rpc_client_to_napalm_driver),
|
||||||
|
]
|
||||||
@@ -24,204 +24,10 @@ from utilities.fields import ColorField, NullableCharField
|
|||||||
from utilities.managers import NaturalOrderByManager
|
from utilities.managers import NaturalOrderByManager
|
||||||
from utilities.models import CreatedUpdatedModel
|
from utilities.models import CreatedUpdatedModel
|
||||||
from utilities.utils import csv_format
|
from utilities.utils import csv_format
|
||||||
|
from .constants import *
|
||||||
from .fields import ASNField, MACAddressField
|
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
|
# Regions
|
||||||
#
|
#
|
||||||
@@ -280,6 +86,10 @@ class Site(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
|
|
||||||
objects = SiteManager()
|
objects = SiteManager()
|
||||||
|
|
||||||
|
csv_headers = [
|
||||||
|
'name', 'slug', 'region', 'tenant', 'facility', 'asn', 'contact_name', 'contact_phone', 'contact_email',
|
||||||
|
]
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['name']
|
ordering = ['name']
|
||||||
|
|
||||||
@@ -346,7 +156,7 @@ class RackGroup(models.Model):
|
|||||||
]
|
]
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return '{} - {}'.format(self.site.name, self.name)
|
return self.name
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return "{}?group_id={}".format(reverse('dcim:rack_list'), self.pk)
|
return "{}?group_id={}".format(reverse('dcim:rack_list'), self.pk)
|
||||||
@@ -402,6 +212,10 @@ class Rack(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
|
|
||||||
objects = RackManager()
|
objects = RackManager()
|
||||||
|
|
||||||
|
csv_headers = [
|
||||||
|
'site', 'group_name', 'name', 'facility_id', 'tenant', 'role', 'type', 'width', 'u_height', 'desc_units',
|
||||||
|
]
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['site', 'name']
|
ordering = ['site', 'name']
|
||||||
unique_together = [
|
unique_together = [
|
||||||
@@ -808,7 +622,7 @@ class PowerOutletTemplate(models.Model):
|
|||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
class InterfaceManager(models.Manager):
|
class InterfaceQuerySet(models.QuerySet):
|
||||||
|
|
||||||
def order_naturally(self, method=IFACE_ORDERING_POSITION):
|
def order_naturally(self, method=IFACE_ORDERING_POSITION):
|
||||||
"""
|
"""
|
||||||
@@ -833,13 +647,12 @@ class InterfaceManager(models.Manager):
|
|||||||
The original `name` field is taken as a whole to serve as a fallback in the event interfaces do not match any of
|
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.
|
the prescribed fields.
|
||||||
"""
|
"""
|
||||||
queryset = self.get_queryset()
|
sql_col = '{}.name'.format(self.model._meta.db_table)
|
||||||
sql_col = '{}.name'.format(queryset.model._meta.db_table)
|
|
||||||
ordering = {
|
ordering = {
|
||||||
IFACE_ORDERING_POSITION: ('_slot', '_subslot', '_position', '_channel', '_vc', '_type', 'name'),
|
IFACE_ORDERING_POSITION: ('_slot', '_subslot', '_position', '_channel', '_vc', '_type', 'name'),
|
||||||
IFACE_ORDERING_NAME: ('_type', '_slot', '_subslot', '_position', '_channel', '_vc', 'name'),
|
IFACE_ORDERING_NAME: ('_type', '_slot', '_subslot', '_position', '_channel', '_vc', 'name'),
|
||||||
}[method]
|
}[method]
|
||||||
return queryset.extra(select={
|
return self.extra(select={
|
||||||
'_type': "SUBSTRING({} FROM '^([^0-9]+)')".format(sql_col),
|
'_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),
|
'_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),
|
'_subslot': "CAST(SUBSTRING({} FROM '([0-9]+)\/[0-9]+(:[0-9]+)?(\.[0-9]+)?$') AS integer)".format(sql_col),
|
||||||
@@ -848,6 +661,13 @@ class InterfaceManager(models.Manager):
|
|||||||
'_vc': "COALESCE(CAST(SUBSTRING({} FROM '\.([0-9]+)$') AS integer), 0)".format(sql_col),
|
'_vc': "COALESCE(CAST(SUBSTRING({} FROM '\.([0-9]+)$') AS integer), 0)".format(sql_col),
|
||||||
}).order_by(*ordering)
|
}).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
|
@python_2_unicode_compatible
|
||||||
class InterfaceTemplate(models.Model):
|
class InterfaceTemplate(models.Model):
|
||||||
@@ -859,7 +679,7 @@ class InterfaceTemplate(models.Model):
|
|||||||
form_factor = models.PositiveSmallIntegerField(choices=IFACE_FF_CHOICES, default=IFACE_FF_10GE_SFP_PLUS)
|
form_factor = models.PositiveSmallIntegerField(choices=IFACE_FF_CHOICES, default=IFACE_FF_10GE_SFP_PLUS)
|
||||||
mgmt_only = models.BooleanField(default=False, verbose_name='Management only')
|
mgmt_only = models.BooleanField(default=False, verbose_name='Management only')
|
||||||
|
|
||||||
objects = InterfaceManager()
|
objects = InterfaceQuerySet.as_manager()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['device_type', 'name']
|
ordering = ['device_type', 'name']
|
||||||
@@ -918,7 +738,10 @@ class Platform(models.Model):
|
|||||||
"""
|
"""
|
||||||
name = models.CharField(max_length=50, unique=True)
|
name = models.CharField(max_length=50, unique=True)
|
||||||
slug = models.SlugField(unique=True)
|
slug = models.SlugField(unique=True)
|
||||||
rpc_client = models.CharField(max_length=30, choices=RPC_CLIENT_CHOICES, blank=True, verbose_name='RPC client')
|
napalm_driver = models.CharField(max_length=50, blank=True, verbose_name='NAPALM driver',
|
||||||
|
help_text="The name of the NAPALM driver to use when interacting with devices.")
|
||||||
|
rpc_client = models.CharField(max_length=30, choices=RPC_CLIENT_CHOICES, blank=True,
|
||||||
|
verbose_name='Legacy RPC client')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['name']
|
ordering = ['name']
|
||||||
@@ -981,9 +804,18 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
|
|
||||||
objects = DeviceManager()
|
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:
|
class Meta:
|
||||||
ordering = ['name']
|
ordering = ['name']
|
||||||
unique_together = ['rack', 'position', 'face']
|
unique_together = ['rack', 'position', 'face']
|
||||||
|
permissions = (
|
||||||
|
('napalm_read', 'Read-only access to devices via NAPALM'),
|
||||||
|
('napalm_write', 'Read/write access to devices via NAPALM'),
|
||||||
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.display_name or super(Device, self).__str__()
|
return self.display_name or super(Device, self).__str__()
|
||||||
@@ -1096,6 +928,7 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
self.asset_tag,
|
self.asset_tag,
|
||||||
self.get_status_display(),
|
self.get_status_display(),
|
||||||
self.site.name,
|
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.rack.name if self.rack else None,
|
||||||
self.position,
|
self.position,
|
||||||
self.get_face_display(),
|
self.get_face_display(),
|
||||||
@@ -1162,6 +995,8 @@ class ConsolePort(models.Model):
|
|||||||
verbose_name='Console server port', blank=True, null=True)
|
verbose_name='Console server port', blank=True, null=True)
|
||||||
connection_status = models.NullBooleanField(choices=CONNECTION_STATUS_CHOICES, default=CONNECTION_STATUS_CONNECTED)
|
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:
|
class Meta:
|
||||||
ordering = ['device', 'name']
|
ordering = ['device', 'name']
|
||||||
unique_together = ['device', 'name']
|
unique_together = ['device', 'name']
|
||||||
@@ -1231,6 +1066,8 @@ class PowerPort(models.Model):
|
|||||||
blank=True, null=True)
|
blank=True, null=True)
|
||||||
connection_status = models.NullBooleanField(choices=CONNECTION_STATUS_CHOICES, default=CONNECTION_STATUS_CONNECTED)
|
connection_status = models.NullBooleanField(choices=CONNECTION_STATUS_CHOICES, default=CONNECTION_STATUS_CONNECTED)
|
||||||
|
|
||||||
|
csv_headers = ['pdu', 'power_outlet', 'device', 'power_port', 'connection_status']
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['device', 'name']
|
ordering = ['device', 'name']
|
||||||
unique_together = ['device', 'name']
|
unique_together = ['device', 'name']
|
||||||
@@ -1290,16 +1127,27 @@ class Interface(models.Model):
|
|||||||
of an InterfaceConnection.
|
of an InterfaceConnection.
|
||||||
"""
|
"""
|
||||||
device = models.ForeignKey('Device', related_name='interfaces', on_delete=models.CASCADE)
|
device = models.ForeignKey('Device', related_name='interfaces', on_delete=models.CASCADE)
|
||||||
lag = models.ForeignKey('self', related_name='member_interfaces', null=True, blank=True, on_delete=models.SET_NULL,
|
lag = models.ForeignKey(
|
||||||
verbose_name='Parent LAG')
|
'self',
|
||||||
|
models.SET_NULL,
|
||||||
|
related_name='member_interfaces',
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name='Parent LAG'
|
||||||
|
)
|
||||||
name = models.CharField(max_length=30)
|
name = models.CharField(max_length=30)
|
||||||
form_factor = models.PositiveSmallIntegerField(choices=IFACE_FF_CHOICES, default=IFACE_FF_10GE_SFP_PLUS)
|
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')
|
mac_address = MACAddressField(null=True, blank=True, verbose_name='MAC Address')
|
||||||
mgmt_only = models.BooleanField(default=False, verbose_name='OOB Management',
|
mtu = models.PositiveSmallIntegerField(blank=True, null=True, verbose_name='MTU')
|
||||||
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)
|
description = models.CharField(max_length=100, blank=True)
|
||||||
|
|
||||||
objects = InterfaceManager()
|
objects = InterfaceQuerySet.as_manager()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['device', 'name']
|
ordering = ['device', 'name']
|
||||||
@@ -1311,10 +1159,10 @@ class Interface(models.Model):
|
|||||||
def clean(self):
|
def clean(self):
|
||||||
|
|
||||||
# Virtual interfaces cannot be connected
|
# Virtual interfaces cannot be connected
|
||||||
if self.form_factor in VIRTUAL_IFACE_TYPES and self.is_connected:
|
if self.form_factor in NONCONNECTABLE_IFACE_TYPES and self.is_connected:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'form_factor': "Virtual interfaces cannot be connected to another interface or circuit. Disconnect the "
|
'form_factor': "Virtual and wireless interfaces cannot be connected to another interface or circuit. "
|
||||||
"interface or choose a physical form factor."
|
"Disconnect the interface or choose a suitable form factor."
|
||||||
})
|
})
|
||||||
|
|
||||||
# An interface's LAG must belong to the same device
|
# An interface's LAG must belong to the same device
|
||||||
@@ -1326,7 +1174,7 @@ class Interface(models.Model):
|
|||||||
})
|
})
|
||||||
|
|
||||||
# A virtual interface cannot have a parent LAG
|
# A virtual interface cannot have a parent LAG
|
||||||
if self.form_factor in VIRTUAL_IFACE_TYPES and self.lag is not None:
|
if self.form_factor in NONCONNECTABLE_IFACE_TYPES and self.lag is not None:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'lag': "{} interfaces cannot have a parent LAG interface.".format(self.get_form_factor_display())
|
'lag': "{} interfaces cannot have a parent LAG interface.".format(self.get_form_factor_display())
|
||||||
})
|
})
|
||||||
@@ -1343,6 +1191,10 @@ class Interface(models.Model):
|
|||||||
def is_virtual(self):
|
def is_virtual(self):
|
||||||
return self.form_factor in VIRTUAL_IFACE_TYPES
|
return self.form_factor in VIRTUAL_IFACE_TYPES
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_wireless(self):
|
||||||
|
return self.form_factor in WIRELESS_IFACE_TYPES
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_lag(self):
|
def is_lag(self):
|
||||||
return self.form_factor == IFACE_FF_LAG
|
return self.form_factor == IFACE_FF_LAG
|
||||||
@@ -1392,11 +1244,16 @@ class InterfaceConnection(models.Model):
|
|||||||
connection_status = models.BooleanField(choices=CONNECTION_STATUS_CHOICES, default=CONNECTION_STATUS_CONNECTED,
|
connection_status = models.BooleanField(choices=CONNECTION_STATUS_CHOICES, default=CONNECTION_STATUS_CONNECTED,
|
||||||
verbose_name='Status')
|
verbose_name='Status')
|
||||||
|
|
||||||
|
csv_headers = ['device_a', 'interface_a', 'device_b', 'interface_b', 'connection_status']
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
try:
|
||||||
if self.interface_a == self.interface_b:
|
if self.interface_a == self.interface_b:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'interface_b': "Cannot connect an interface to itself."
|
'interface_b': "Cannot connect an interface to itself."
|
||||||
})
|
})
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
pass
|
||||||
|
|
||||||
# Used for connections export
|
# Used for connections export
|
||||||
def to_csv(self):
|
def to_csv(self):
|
||||||
@@ -1456,11 +1313,17 @@ class InventoryItem(models.Model):
|
|||||||
device = models.ForeignKey('Device', related_name='inventory_items', on_delete=models.CASCADE)
|
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)
|
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')
|
name = models.CharField(max_length=50, verbose_name='Name')
|
||||||
manufacturer = models.ForeignKey('Manufacturer', related_name='inventory_items', blank=True, null=True,
|
manufacturer = models.ForeignKey(
|
||||||
on_delete=models.PROTECT)
|
'Manufacturer', models.PROTECT, related_name='inventory_items', blank=True, null=True
|
||||||
|
)
|
||||||
part_id = models.CharField(max_length=50, verbose_name='Part ID', blank=True)
|
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)
|
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')
|
discovered = models.BooleanField(default=False, verbose_name='Discovered')
|
||||||
|
description = models.CharField(max_length=100, blank=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['device__id', 'parent__id', 'name']
|
ordering = ['device__id', 'parent__id', 'name']
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ from __future__ import unicode_literals
|
|||||||
import django_tables2 as tables
|
import django_tables2 as tables
|
||||||
from django_tables2.utils import Accessor
|
from django_tables2.utils import Accessor
|
||||||
|
|
||||||
from utilities.tables import BaseTable, SearchTable, ToggleColumn
|
from utilities.tables import BaseTable, ToggleColumn
|
||||||
from .models import (
|
from .models import (
|
||||||
ConsolePort, ConsolePortTemplate, ConsoleServerPortTemplate, Device, DeviceBayTemplate, DeviceRole, DeviceType,
|
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||||
Interface, InterfaceTemplate, Manufacturer, Platform, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack,
|
DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceTemplate, Manufacturer, Platform, PowerOutlet,
|
||||||
RackGroup, RackReservation, Region, Site,
|
PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, Region, Site,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -142,30 +142,26 @@ class SiteTable(BaseTable):
|
|||||||
name = tables.LinkColumn()
|
name = tables.LinkColumn()
|
||||||
region = tables.TemplateColumn(template_code=SITE_REGION_LINK)
|
region = tables.TemplateColumn(template_code=SITE_REGION_LINK)
|
||||||
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')])
|
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')])
|
||||||
|
|
||||||
|
class Meta(BaseTable.Meta):
|
||||||
|
model = Site
|
||||||
|
fields = ('pk', 'name', 'facility', 'region', 'tenant', 'asn')
|
||||||
|
|
||||||
|
|
||||||
|
class SiteDetailTable(SiteTable):
|
||||||
rack_count = tables.Column(accessor=Accessor('count_racks'), orderable=False, verbose_name='Racks')
|
rack_count = tables.Column(accessor=Accessor('count_racks'), orderable=False, verbose_name='Racks')
|
||||||
device_count = tables.Column(accessor=Accessor('count_devices'), orderable=False, verbose_name='Devices')
|
device_count = tables.Column(accessor=Accessor('count_devices'), orderable=False, verbose_name='Devices')
|
||||||
prefix_count = tables.Column(accessor=Accessor('count_prefixes'), orderable=False, verbose_name='Prefixes')
|
prefix_count = tables.Column(accessor=Accessor('count_prefixes'), orderable=False, verbose_name='Prefixes')
|
||||||
vlan_count = tables.Column(accessor=Accessor('count_vlans'), orderable=False, verbose_name='VLANs')
|
vlan_count = tables.Column(accessor=Accessor('count_vlans'), orderable=False, verbose_name='VLANs')
|
||||||
circuit_count = tables.Column(accessor=Accessor('count_circuits'), orderable=False, verbose_name='Circuits')
|
circuit_count = tables.Column(accessor=Accessor('count_circuits'), orderable=False, verbose_name='Circuits')
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(SiteTable.Meta):
|
||||||
model = Site
|
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'name', 'facility', 'region', 'tenant', 'asn', 'rack_count', 'device_count', 'prefix_count',
|
'pk', 'name', 'facility', 'region', 'tenant', 'asn', 'rack_count', 'device_count', 'prefix_count',
|
||||||
'vlan_count', 'circuit_count',
|
'vlan_count', 'circuit_count',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class SiteSearchTable(SearchTable):
|
|
||||||
name = tables.LinkColumn()
|
|
||||||
region = tables.TemplateColumn(template_code=SITE_REGION_LINK)
|
|
||||||
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')])
|
|
||||||
|
|
||||||
class Meta(SearchTable.Meta):
|
|
||||||
model = Site
|
|
||||||
fields = ('name', 'facility', 'region', 'tenant', 'asn')
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Rack groups
|
# Rack groups
|
||||||
#
|
#
|
||||||
@@ -214,29 +210,22 @@ class RackTable(BaseTable):
|
|||||||
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')])
|
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')])
|
||||||
role = tables.TemplateColumn(RACK_ROLE)
|
role = tables.TemplateColumn(RACK_ROLE)
|
||||||
u_height = tables.TemplateColumn("{{ record.u_height }}U", verbose_name='Height')
|
u_height = tables.TemplateColumn("{{ record.u_height }}U", verbose_name='Height')
|
||||||
devices = tables.Column(accessor=Accessor('device_count'))
|
|
||||||
get_utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False, verbose_name='Utilization')
|
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = Rack
|
model = Rack
|
||||||
|
fields = ('pk', 'name', 'site', 'group', 'facility_id', 'tenant', 'role', 'u_height')
|
||||||
|
|
||||||
|
|
||||||
|
class RackDetailTable(RackTable):
|
||||||
|
devices = tables.Column(accessor=Accessor('device_count'))
|
||||||
|
get_utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False, verbose_name='Utilization')
|
||||||
|
|
||||||
|
class Meta(RackTable.Meta):
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'name', 'site', 'group', 'facility_id', 'tenant', 'role', 'u_height', 'devices', 'get_utilization'
|
'pk', 'name', 'site', 'group', 'facility_id', 'tenant', 'role', 'u_height', 'devices', 'get_utilization'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class RackSearchTable(SearchTable):
|
|
||||||
name = tables.LinkColumn()
|
|
||||||
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')])
|
|
||||||
group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group')
|
|
||||||
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')])
|
|
||||||
role = tables.TemplateColumn(RACK_ROLE)
|
|
||||||
u_height = tables.TemplateColumn("{{ record.u_height }}U", verbose_name='Height')
|
|
||||||
|
|
||||||
class Meta(SearchTable.Meta):
|
|
||||||
model = Rack
|
|
||||||
fields = ('name', 'site', 'group', 'facility_id', 'tenant', 'role', 'u_height')
|
|
||||||
|
|
||||||
|
|
||||||
class RackImportTable(BaseTable):
|
class RackImportTable(BaseTable):
|
||||||
name = tables.LinkColumn('dcim:rack', args=[Accessor('pk')], verbose_name='Name')
|
name = tables.LinkColumn('dcim:rack', args=[Accessor('pk')], verbose_name='Name')
|
||||||
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
|
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
|
||||||
@@ -247,7 +236,7 @@ class RackImportTable(BaseTable):
|
|||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = Rack
|
model = Rack
|
||||||
fields = ('site', 'group', 'name', 'facility_id', 'tenant', 'u_height')
|
fields = ('name', 'site', 'group', 'facility_id', 'tenant', 'u_height')
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -302,23 +291,7 @@ class DeviceTypeTable(BaseTable):
|
|||||||
model = DeviceType
|
model = DeviceType
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'model', 'manufacturer', 'part_number', 'u_height', 'is_full_depth', 'is_console_server', 'is_pdu',
|
'pk', 'model', 'manufacturer', 'part_number', 'u_height', 'is_full_depth', 'is_console_server', 'is_pdu',
|
||||||
'is_network_device', 'subdevice_role', 'instance_count'
|
'is_network_device', 'subdevice_role', 'instance_count',
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class DeviceTypeSearchTable(SearchTable):
|
|
||||||
model = tables.LinkColumn('dcim:devicetype', args=[Accessor('pk')], verbose_name='Device Type')
|
|
||||||
is_full_depth = tables.BooleanColumn(verbose_name='Full Depth')
|
|
||||||
is_console_server = tables.BooleanColumn(verbose_name='CS')
|
|
||||||
is_pdu = tables.BooleanColumn(verbose_name='PDU')
|
|
||||||
is_network_device = tables.BooleanColumn(verbose_name='Net')
|
|
||||||
subdevice_role = tables.TemplateColumn(SUBDEVICE_ROLE_TEMPLATE, verbose_name='Subdevice Role')
|
|
||||||
|
|
||||||
class Meta(SearchTable.Meta):
|
|
||||||
model = DeviceType
|
|
||||||
fields = (
|
|
||||||
'model', 'manufacturer', 'part_number', 'u_height', 'is_full_depth', 'is_console_server', 'is_pdu',
|
|
||||||
'is_network_device', 'subdevice_role',
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -333,7 +306,6 @@ class ConsolePortTemplateTable(BaseTable):
|
|||||||
model = ConsolePortTemplate
|
model = ConsolePortTemplate
|
||||||
fields = ('pk', 'name')
|
fields = ('pk', 'name')
|
||||||
empty_text = "None"
|
empty_text = "None"
|
||||||
show_header = False
|
|
||||||
|
|
||||||
|
|
||||||
class ConsoleServerPortTemplateTable(BaseTable):
|
class ConsoleServerPortTemplateTable(BaseTable):
|
||||||
@@ -343,7 +315,6 @@ class ConsoleServerPortTemplateTable(BaseTable):
|
|||||||
model = ConsoleServerPortTemplate
|
model = ConsoleServerPortTemplate
|
||||||
fields = ('pk', 'name')
|
fields = ('pk', 'name')
|
||||||
empty_text = "None"
|
empty_text = "None"
|
||||||
show_header = False
|
|
||||||
|
|
||||||
|
|
||||||
class PowerPortTemplateTable(BaseTable):
|
class PowerPortTemplateTable(BaseTable):
|
||||||
@@ -353,7 +324,6 @@ class PowerPortTemplateTable(BaseTable):
|
|||||||
model = PowerPortTemplate
|
model = PowerPortTemplate
|
||||||
fields = ('pk', 'name')
|
fields = ('pk', 'name')
|
||||||
empty_text = "None"
|
empty_text = "None"
|
||||||
show_header = False
|
|
||||||
|
|
||||||
|
|
||||||
class PowerOutletTemplateTable(BaseTable):
|
class PowerOutletTemplateTable(BaseTable):
|
||||||
@@ -363,17 +333,16 @@ class PowerOutletTemplateTable(BaseTable):
|
|||||||
model = PowerOutletTemplate
|
model = PowerOutletTemplate
|
||||||
fields = ('pk', 'name')
|
fields = ('pk', 'name')
|
||||||
empty_text = "None"
|
empty_text = "None"
|
||||||
show_header = False
|
|
||||||
|
|
||||||
|
|
||||||
class InterfaceTemplateTable(BaseTable):
|
class InterfaceTemplateTable(BaseTable):
|
||||||
pk = ToggleColumn()
|
pk = ToggleColumn()
|
||||||
|
mgmt_only = tables.TemplateColumn("{% if value %}OOB Management{% endif %}")
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = InterfaceTemplate
|
model = InterfaceTemplate
|
||||||
fields = ('pk', 'name', 'form_factor')
|
fields = ('pk', 'name', 'mgmt_only', 'form_factor')
|
||||||
empty_text = "None"
|
empty_text = "None"
|
||||||
show_header = False
|
|
||||||
|
|
||||||
|
|
||||||
class DeviceBayTemplateTable(BaseTable):
|
class DeviceBayTemplateTable(BaseTable):
|
||||||
@@ -383,7 +352,6 @@ class DeviceBayTemplateTable(BaseTable):
|
|||||||
model = DeviceBayTemplate
|
model = DeviceBayTemplate
|
||||||
fields = ('pk', 'name')
|
fields = ('pk', 'name')
|
||||||
empty_text = "None"
|
empty_text = "None"
|
||||||
show_header = False
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -438,32 +406,22 @@ class DeviceTable(BaseTable):
|
|||||||
'dcim:devicetype', args=[Accessor('device_type.pk')], verbose_name='Type',
|
'dcim:devicetype', args=[Accessor('device_type.pk')], verbose_name='Type',
|
||||||
text=lambda record: record.device_type.full_name
|
text=lambda record: record.device_type.full_name
|
||||||
)
|
)
|
||||||
|
|
||||||
|
class Meta(BaseTable.Meta):
|
||||||
|
model = Device
|
||||||
|
fields = ('pk', 'name', 'status', 'tenant', 'site', 'rack', 'device_role', 'device_type')
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceDetailTable(DeviceTable):
|
||||||
primary_ip = tables.TemplateColumn(
|
primary_ip = tables.TemplateColumn(
|
||||||
orderable=False, verbose_name='IP Address', template_code=DEVICE_PRIMARY_IP
|
orderable=False, verbose_name='IP Address', template_code=DEVICE_PRIMARY_IP
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(DeviceTable.Meta):
|
||||||
model = Device
|
model = Device
|
||||||
fields = ('pk', 'name', 'status', 'tenant', 'site', 'rack', 'device_role', 'device_type', 'primary_ip')
|
fields = ('pk', 'name', 'status', 'tenant', 'site', 'rack', 'device_role', 'device_type', 'primary_ip')
|
||||||
|
|
||||||
|
|
||||||
class DeviceSearchTable(SearchTable):
|
|
||||||
name = tables.TemplateColumn(template_code=DEVICE_LINK)
|
|
||||||
status = tables.TemplateColumn(template_code=DEVICE_STATUS, verbose_name='Status')
|
|
||||||
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')])
|
|
||||||
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')])
|
|
||||||
rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')])
|
|
||||||
device_role = tables.TemplateColumn(DEVICE_ROLE, verbose_name='Role')
|
|
||||||
device_type = tables.LinkColumn(
|
|
||||||
'dcim:devicetype', args=[Accessor('device_type.pk')], verbose_name='Type',
|
|
||||||
text=lambda record: record.device_type.full_name
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta(SearchTable.Meta):
|
|
||||||
model = Device
|
|
||||||
fields = ('name', 'status', 'tenant', 'site', 'rack', 'device_role', 'device_type')
|
|
||||||
|
|
||||||
|
|
||||||
class DeviceImportTable(BaseTable):
|
class DeviceImportTable(BaseTable):
|
||||||
name = tables.TemplateColumn(template_code=DEVICE_LINK, verbose_name='Name')
|
name = tables.TemplateColumn(template_code=DEVICE_LINK, verbose_name='Name')
|
||||||
status = tables.TemplateColumn(template_code=DEVICE_STATUS, verbose_name='Status')
|
status = tables.TemplateColumn(template_code=DEVICE_STATUS, verbose_name='Status')
|
||||||
@@ -480,6 +438,52 @@ class DeviceImportTable(BaseTable):
|
|||||||
empty_text = False
|
empty_text = False
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Device components
|
||||||
|
#
|
||||||
|
|
||||||
|
class ConsolePortTable(BaseTable):
|
||||||
|
|
||||||
|
class Meta(BaseTable.Meta):
|
||||||
|
model = ConsolePort
|
||||||
|
fields = ('name',)
|
||||||
|
|
||||||
|
|
||||||
|
class ConsoleServerPortTable(BaseTable):
|
||||||
|
|
||||||
|
class Meta(BaseTable.Meta):
|
||||||
|
model = ConsoleServerPort
|
||||||
|
fields = ('name',)
|
||||||
|
|
||||||
|
|
||||||
|
class PowerPortTable(BaseTable):
|
||||||
|
|
||||||
|
class Meta(BaseTable.Meta):
|
||||||
|
model = PowerPort
|
||||||
|
fields = ('name',)
|
||||||
|
|
||||||
|
|
||||||
|
class PowerOutletTable(BaseTable):
|
||||||
|
|
||||||
|
class Meta(BaseTable.Meta):
|
||||||
|
model = PowerOutlet
|
||||||
|
fields = ('name',)
|
||||||
|
|
||||||
|
|
||||||
|
class InterfaceTable(BaseTable):
|
||||||
|
|
||||||
|
class Meta(BaseTable.Meta):
|
||||||
|
model = Interface
|
||||||
|
fields = ('name', 'form_factor', 'lag', 'enabled', 'mgmt_only', 'description')
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceBayTable(BaseTable):
|
||||||
|
|
||||||
|
class Meta(BaseTable.Meta):
|
||||||
|
model = DeviceBay
|
||||||
|
fields = ('name',)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Device connections
|
# Device connections
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from __future__ import unicode_literals
|
|||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
|
|
||||||
from extras.views import ImageAttachmentEditView
|
from extras.views import ImageAttachmentEditView
|
||||||
from ipam.views import ServiceEditView
|
from ipam.views import ServiceCreateView
|
||||||
from secrets.views import secret_add
|
from secrets.views import secret_add
|
||||||
from .models import Device, Rack, Site
|
from .models import Device, Rack, Site
|
||||||
from . import views
|
from . import views
|
||||||
@@ -14,13 +14,13 @@ urlpatterns = [
|
|||||||
|
|
||||||
# Regions
|
# Regions
|
||||||
url(r'^regions/$', views.RegionListView.as_view(), name='region_list'),
|
url(r'^regions/$', views.RegionListView.as_view(), name='region_list'),
|
||||||
url(r'^regions/add/$', views.RegionEditView.as_view(), name='region_add'),
|
url(r'^regions/add/$', views.RegionCreateView.as_view(), name='region_add'),
|
||||||
url(r'^regions/delete/$', views.RegionBulkDeleteView.as_view(), name='region_bulk_delete'),
|
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'),
|
url(r'^regions/(?P<pk>\d+)/edit/$', views.RegionEditView.as_view(), name='region_edit'),
|
||||||
|
|
||||||
# Sites
|
# Sites
|
||||||
url(r'^sites/$', views.SiteListView.as_view(), name='site_list'),
|
url(r'^sites/$', views.SiteListView.as_view(), name='site_list'),
|
||||||
url(r'^sites/add/$', views.SiteEditView.as_view(), name='site_add'),
|
url(r'^sites/add/$', views.SiteCreateView.as_view(), name='site_add'),
|
||||||
url(r'^sites/import/$', views.SiteBulkImportView.as_view(), name='site_import'),
|
url(r'^sites/import/$', views.SiteBulkImportView.as_view(), name='site_import'),
|
||||||
url(r'^sites/edit/$', views.SiteBulkEditView.as_view(), name='site_bulk_edit'),
|
url(r'^sites/edit/$', views.SiteBulkEditView.as_view(), name='site_bulk_edit'),
|
||||||
url(r'^sites/(?P<slug>[\w-]+)/$', views.SiteView.as_view(), name='site'),
|
url(r'^sites/(?P<slug>[\w-]+)/$', views.SiteView.as_view(), name='site'),
|
||||||
@@ -30,13 +30,13 @@ urlpatterns = [
|
|||||||
|
|
||||||
# Rack groups
|
# Rack groups
|
||||||
url(r'^rack-groups/$', views.RackGroupListView.as_view(), name='rackgroup_list'),
|
url(r'^rack-groups/$', views.RackGroupListView.as_view(), name='rackgroup_list'),
|
||||||
url(r'^rack-groups/add/$', views.RackGroupEditView.as_view(), name='rackgroup_add'),
|
url(r'^rack-groups/add/$', views.RackGroupCreateView.as_view(), name='rackgroup_add'),
|
||||||
url(r'^rack-groups/delete/$', views.RackGroupBulkDeleteView.as_view(), name='rackgroup_bulk_delete'),
|
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'),
|
url(r'^rack-groups/(?P<pk>\d+)/edit/$', views.RackGroupEditView.as_view(), name='rackgroup_edit'),
|
||||||
|
|
||||||
# Rack roles
|
# Rack roles
|
||||||
url(r'^rack-roles/$', views.RackRoleListView.as_view(), name='rackrole_list'),
|
url(r'^rack-roles/$', views.RackRoleListView.as_view(), name='rackrole_list'),
|
||||||
url(r'^rack-roles/add/$', views.RackRoleEditView.as_view(), name='rackrole_add'),
|
url(r'^rack-roles/add/$', views.RackRoleCreateView.as_view(), name='rackrole_add'),
|
||||||
url(r'^rack-roles/delete/$', views.RackRoleBulkDeleteView.as_view(), name='rackrole_bulk_delete'),
|
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'),
|
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+)/$', views.RackView.as_view(), name='rack'),
|
||||||
url(r'^racks/(?P<pk>\d+)/edit/$', views.RackEditView.as_view(), name='rack_edit'),
|
url(r'^racks/(?P<pk>\d+)/edit/$', views.RackEditView.as_view(), name='rack_edit'),
|
||||||
url(r'^racks/(?P<pk>\d+)/delete/$', views.RackDeleteView.as_view(), name='rack_delete'),
|
url(r'^racks/(?P<pk>\d+)/delete/$', views.RackDeleteView.as_view(), name='rack_delete'),
|
||||||
url(r'^racks/(?P<rack>\d+)/reservations/add/$', views.RackReservationEditView.as_view(), name='rack_add_reservation'),
|
url(r'^racks/(?P<rack>\d+)/reservations/add/$', views.RackReservationCreateView.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}),
|
url(r'^racks/(?P<object_id>\d+)/images/add/$', ImageAttachmentEditView.as_view(), name='rack_add_image', kwargs={'model': Rack}),
|
||||||
|
|
||||||
# Manufacturers
|
# Manufacturers
|
||||||
url(r'^manufacturers/$', views.ManufacturerListView.as_view(), name='manufacturer_list'),
|
url(r'^manufacturers/$', views.ManufacturerListView.as_view(), name='manufacturer_list'),
|
||||||
url(r'^manufacturers/add/$', views.ManufacturerEditView.as_view(), name='manufacturer_add'),
|
url(r'^manufacturers/add/$', views.ManufacturerCreateView.as_view(), name='manufacturer_add'),
|
||||||
url(r'^manufacturers/delete/$', views.ManufacturerBulkDeleteView.as_view(), name='manufacturer_bulk_delete'),
|
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'),
|
url(r'^manufacturers/(?P<slug>[\w-]+)/edit/$', views.ManufacturerEditView.as_view(), name='manufacturer_edit'),
|
||||||
|
|
||||||
# Device types
|
# Device types
|
||||||
url(r'^device-types/$', views.DeviceTypeListView.as_view(), name='devicetype_list'),
|
url(r'^device-types/$', views.DeviceTypeListView.as_view(), name='devicetype_list'),
|
||||||
url(r'^device-types/add/$', views.DeviceTypeEditView.as_view(), name='devicetype_add'),
|
url(r'^device-types/add/$', views.DeviceTypeCreateView.as_view(), name='devicetype_add'),
|
||||||
url(r'^device-types/edit/$', views.DeviceTypeBulkEditView.as_view(), name='devicetype_bulk_edit'),
|
url(r'^device-types/edit/$', views.DeviceTypeBulkEditView.as_view(), name='devicetype_bulk_edit'),
|
||||||
url(r'^device-types/delete/$', views.DeviceTypeBulkDeleteView.as_view(), name='devicetype_bulk_delete'),
|
url(r'^device-types/delete/$', views.DeviceTypeBulkDeleteView.as_view(), name='devicetype_bulk_delete'),
|
||||||
url(r'^device-types/(?P<pk>\d+)/$', views.DeviceTypeView.as_view(), name='devicetype'),
|
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'),
|
url(r'^device-types/(?P<pk>\d+)/delete/$', views.DeviceTypeDeleteView.as_view(), name='devicetype_delete'),
|
||||||
|
|
||||||
# Console port templates
|
# Console port templates
|
||||||
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/add/$', views.ConsolePortTemplateCreateView.as_view(), name='devicetype_add_consoleport'),
|
||||||
url(r'^device-types/(?P<pk>\d+)/console-ports/delete/$', views.ConsolePortTemplateBulkDeleteView.as_view(), name='devicetype_delete_consoleport'),
|
url(r'^device-types/(?P<pk>\d+)/console-ports/delete/$', views.ConsolePortTemplateBulkDeleteView.as_view(), name='devicetype_delete_consoleport'),
|
||||||
|
|
||||||
# Console server port templates
|
# Console server port templates
|
||||||
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/add/$', views.ConsoleServerPortTemplateCreateView.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'),
|
url(r'^device-types/(?P<pk>\d+)/console-server-ports/delete/$', views.ConsoleServerPortTemplateBulkDeleteView.as_view(), name='devicetype_delete_consoleserverport'),
|
||||||
|
|
||||||
# Power port templates
|
# Power port templates
|
||||||
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/add/$', views.PowerPortTemplateCreateView.as_view(), name='devicetype_add_powerport'),
|
||||||
url(r'^device-types/(?P<pk>\d+)/power-ports/delete/$', views.PowerPortTemplateBulkDeleteView.as_view(), name='devicetype_delete_powerport'),
|
url(r'^device-types/(?P<pk>\d+)/power-ports/delete/$', views.PowerPortTemplateBulkDeleteView.as_view(), name='devicetype_delete_powerport'),
|
||||||
|
|
||||||
# Power outlet templates
|
# Power outlet templates
|
||||||
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/add/$', views.PowerOutletTemplateCreateView.as_view(), name='devicetype_add_poweroutlet'),
|
||||||
url(r'^device-types/(?P<pk>\d+)/power-outlets/delete/$', views.PowerOutletTemplateBulkDeleteView.as_view(), name='devicetype_delete_poweroutlet'),
|
url(r'^device-types/(?P<pk>\d+)/power-outlets/delete/$', views.PowerOutletTemplateBulkDeleteView.as_view(), name='devicetype_delete_poweroutlet'),
|
||||||
|
|
||||||
# Interface templates
|
# Interface templates
|
||||||
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/add/$', views.InterfaceTemplateCreateView.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/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'),
|
url(r'^device-types/(?P<pk>\d+)/interfaces/delete/$', views.InterfaceTemplateBulkDeleteView.as_view(), name='devicetype_delete_interface'),
|
||||||
|
|
||||||
# Device bay templates
|
# Device bay templates
|
||||||
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/add/$', views.DeviceBayTemplateCreateView.as_view(), name='devicetype_add_devicebay'),
|
||||||
url(r'^device-types/(?P<pk>\d+)/device-bays/delete/$', views.DeviceBayTemplateBulkDeleteView.as_view(), name='devicetype_delete_devicebay'),
|
url(r'^device-types/(?P<pk>\d+)/device-bays/delete/$', views.DeviceBayTemplateBulkDeleteView.as_view(), name='devicetype_delete_devicebay'),
|
||||||
|
|
||||||
# Device roles
|
# Device roles
|
||||||
url(r'^device-roles/$', views.DeviceRoleListView.as_view(), name='devicerole_list'),
|
url(r'^device-roles/$', views.DeviceRoleListView.as_view(), name='devicerole_list'),
|
||||||
url(r'^device-roles/add/$', views.DeviceRoleEditView.as_view(), name='devicerole_add'),
|
url(r'^device-roles/add/$', views.DeviceRoleCreateView.as_view(), name='devicerole_add'),
|
||||||
url(r'^device-roles/delete/$', views.DeviceRoleBulkDeleteView.as_view(), name='devicerole_bulk_delete'),
|
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'),
|
url(r'^device-roles/(?P<slug>[\w-]+)/edit/$', views.DeviceRoleEditView.as_view(), name='devicerole_edit'),
|
||||||
|
|
||||||
# Platforms
|
# Platforms
|
||||||
url(r'^platforms/$', views.PlatformListView.as_view(), name='platform_list'),
|
url(r'^platforms/$', views.PlatformListView.as_view(), name='platform_list'),
|
||||||
url(r'^platforms/add/$', views.PlatformEditView.as_view(), name='platform_add'),
|
url(r'^platforms/add/$', views.PlatformCreateView.as_view(), name='platform_add'),
|
||||||
url(r'^platforms/delete/$', views.PlatformBulkDeleteView.as_view(), name='platform_bulk_delete'),
|
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'),
|
url(r'^platforms/(?P<slug>[\w-]+)/edit/$', views.PlatformEditView.as_view(), name='platform_edit'),
|
||||||
|
|
||||||
# Devices
|
# Devices
|
||||||
url(r'^devices/$', views.DeviceListView.as_view(), name='device_list'),
|
url(r'^devices/$', views.DeviceListView.as_view(), name='device_list'),
|
||||||
url(r'^devices/add/$', views.DeviceEditView.as_view(), name='device_add'),
|
url(r'^devices/add/$', views.DeviceCreateView.as_view(), name='device_add'),
|
||||||
url(r'^devices/import/$', views.DeviceBulkImportView.as_view(), name='device_import'),
|
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/import/child-devices/$', views.ChildDeviceBulkImportView.as_view(), name='device_import_child'),
|
||||||
url(r'^devices/edit/$', views.DeviceBulkEditView.as_view(), name='device_bulk_edit'),
|
url(r'^devices/edit/$', views.DeviceBulkEditView.as_view(), name='device_bulk_edit'),
|
||||||
@@ -122,14 +122,16 @@ urlpatterns = [
|
|||||||
url(r'^devices/(?P<pk>\d+)/edit/$', views.DeviceEditView.as_view(), name='device_edit'),
|
url(r'^devices/(?P<pk>\d+)/edit/$', views.DeviceEditView.as_view(), name='device_edit'),
|
||||||
url(r'^devices/(?P<pk>\d+)/delete/$', views.DeviceDeleteView.as_view(), name='device_delete'),
|
url(r'^devices/(?P<pk>\d+)/delete/$', views.DeviceDeleteView.as_view(), name='device_delete'),
|
||||||
url(r'^devices/(?P<pk>\d+)/inventory/$', views.DeviceInventoryView.as_view(), name='device_inventory'),
|
url(r'^devices/(?P<pk>\d+)/inventory/$', views.DeviceInventoryView.as_view(), name='device_inventory'),
|
||||||
|
url(r'^devices/(?P<pk>\d+)/status/$', views.DeviceStatusView.as_view(), name='device_status'),
|
||||||
url(r'^devices/(?P<pk>\d+)/lldp-neighbors/$', views.DeviceLLDPNeighborsView.as_view(), name='device_lldp_neighbors'),
|
url(r'^devices/(?P<pk>\d+)/lldp-neighbors/$', views.DeviceLLDPNeighborsView.as_view(), name='device_lldp_neighbors'),
|
||||||
|
url(r'^devices/(?P<pk>\d+)/config/$', views.DeviceConfigView.as_view(), name='device_config'),
|
||||||
url(r'^devices/(?P<pk>\d+)/add-secret/$', secret_add, name='device_addsecret'),
|
url(r'^devices/(?P<pk>\d+)/add-secret/$', secret_add, name='device_addsecret'),
|
||||||
url(r'^devices/(?P<device>\d+)/services/assign/$', ServiceEditView.as_view(), name='service_assign'),
|
url(r'^devices/(?P<device>\d+)/services/assign/$', ServiceCreateView.as_view(), name='service_assign'),
|
||||||
url(r'^devices/(?P<object_id>\d+)/images/add/$', ImageAttachmentEditView.as_view(), name='device_add_image', kwargs={'model': Device}),
|
url(r'^devices/(?P<object_id>\d+)/images/add/$', ImageAttachmentEditView.as_view(), name='device_add_image', kwargs={'model': Device}),
|
||||||
|
|
||||||
# Console ports
|
# Console ports
|
||||||
url(r'^devices/console-ports/add/$', views.DeviceBulkAddConsolePortView.as_view(), name='device_bulk_add_consoleport'),
|
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.ConsolePortAddView.as_view(), name='consoleport_add'),
|
url(r'^devices/(?P<pk>\d+)/console-ports/add/$', views.ConsolePortCreateView.as_view(), name='consoleport_add'),
|
||||||
url(r'^devices/(?P<pk>\d+)/console-ports/delete/$', views.ConsolePortBulkDeleteView.as_view(), name='consoleport_bulk_delete'),
|
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+)/connect/$', views.consoleport_connect, name='consoleport_connect'),
|
||||||
url(r'^console-ports/(?P<pk>\d+)/disconnect/$', views.consoleport_disconnect, name='consoleport_disconnect'),
|
url(r'^console-ports/(?P<pk>\d+)/disconnect/$', views.consoleport_disconnect, name='consoleport_disconnect'),
|
||||||
@@ -138,7 +140,8 @@ urlpatterns = [
|
|||||||
|
|
||||||
# Console server ports
|
# Console server ports
|
||||||
url(r'^devices/console-server-ports/add/$', views.DeviceBulkAddConsoleServerPortView.as_view(), name='device_bulk_add_consoleserverport'),
|
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.ConsoleServerPortAddView.as_view(), name='consoleserverport_add'),
|
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/delete/$', views.ConsoleServerPortBulkDeleteView.as_view(), name='consoleserverport_bulk_delete'),
|
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+)/connect/$', views.consoleserverport_connect, name='consoleserverport_connect'),
|
||||||
url(r'^console-server-ports/(?P<pk>\d+)/disconnect/$', views.consoleserverport_disconnect, name='consoleserverport_disconnect'),
|
url(r'^console-server-ports/(?P<pk>\d+)/disconnect/$', views.consoleserverport_disconnect, name='consoleserverport_disconnect'),
|
||||||
@@ -147,7 +150,7 @@ urlpatterns = [
|
|||||||
|
|
||||||
# Power ports
|
# Power ports
|
||||||
url(r'^devices/power-ports/add/$', views.DeviceBulkAddPowerPortView.as_view(), name='device_bulk_add_powerport'),
|
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.PowerPortAddView.as_view(), name='powerport_add'),
|
url(r'^devices/(?P<pk>\d+)/power-ports/add/$', views.PowerPortCreateView.as_view(), name='powerport_add'),
|
||||||
url(r'^devices/(?P<pk>\d+)/power-ports/delete/$', views.PowerPortBulkDeleteView.as_view(), name='powerport_bulk_delete'),
|
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+)/connect/$', views.powerport_connect, name='powerport_connect'),
|
||||||
url(r'^power-ports/(?P<pk>\d+)/disconnect/$', views.powerport_disconnect, name='powerport_disconnect'),
|
url(r'^power-ports/(?P<pk>\d+)/disconnect/$', views.powerport_disconnect, name='powerport_disconnect'),
|
||||||
@@ -156,7 +159,8 @@ urlpatterns = [
|
|||||||
|
|
||||||
# Power outlets
|
# Power outlets
|
||||||
url(r'^devices/power-outlets/add/$', views.DeviceBulkAddPowerOutletView.as_view(), name='device_bulk_add_poweroutlet'),
|
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.PowerOutletAddView.as_view(), name='poweroutlet_add'),
|
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/delete/$', views.PowerOutletBulkDeleteView.as_view(), name='poweroutlet_bulk_delete'),
|
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+)/connect/$', views.poweroutlet_connect, name='poweroutlet_connect'),
|
||||||
url(r'^power-outlets/(?P<pk>\d+)/disconnect/$', views.poweroutlet_disconnect, name='poweroutlet_disconnect'),
|
url(r'^power-outlets/(?P<pk>\d+)/disconnect/$', views.poweroutlet_disconnect, name='poweroutlet_disconnect'),
|
||||||
@@ -165,8 +169,9 @@ urlpatterns = [
|
|||||||
|
|
||||||
# Interfaces
|
# Interfaces
|
||||||
url(r'^devices/interfaces/add/$', views.DeviceBulkAddInterfaceView.as_view(), name='device_bulk_add_interface'),
|
url(r'^devices/interfaces/add/$', views.DeviceBulkAddInterfaceView.as_view(), name='device_bulk_add_interface'),
|
||||||
url(r'^devices/(?P<pk>\d+)/interfaces/add/$', views.InterfaceAddView.as_view(), name='interface_add'),
|
url(r'^devices/(?P<pk>\d+)/interfaces/add/$', views.InterfaceCreateView.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/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+)/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'^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'),
|
url(r'^interface-connections/(?P<pk>\d+)/delete/$', views.interfaceconnection_delete, name='interfaceconnection_delete'),
|
||||||
@@ -175,7 +180,7 @@ urlpatterns = [
|
|||||||
|
|
||||||
# Device bays
|
# Device bays
|
||||||
url(r'^devices/device-bays/add/$', views.DeviceBulkAddDeviceBayView.as_view(), name='device_bulk_add_devicebay'),
|
url(r'^devices/device-bays/add/$', views.DeviceBulkAddDeviceBayView.as_view(), name='device_bulk_add_devicebay'),
|
||||||
url(r'^devices/(?P<pk>\d+)/bays/add/$', views.DeviceBayAddView.as_view(), name='devicebay_add'),
|
url(r'^devices/(?P<pk>\d+)/bays/add/$', views.DeviceBayCreateView.as_view(), name='devicebay_add'),
|
||||||
url(r'^devices/(?P<pk>\d+)/bays/delete/$', views.DeviceBayBulkDeleteView.as_view(), name='devicebay_bulk_delete'),
|
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+)/edit/$', views.DeviceBayEditView.as_view(), name='devicebay_edit'),
|
||||||
url(r'^device-bays/(?P<pk>\d+)/delete/$', views.DeviceBayDeleteView.as_view(), name='devicebay_delete'),
|
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.decorators import permission_required
|
||||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||||
from django.core.paginator import EmptyPage, PageNotAnInteger
|
from django.core.paginator import EmptyPage, PageNotAnInteger
|
||||||
from django.db.models import Count
|
from django.db.models import Count, Q
|
||||||
from django.http import HttpResponseRedirect
|
from django.http import HttpResponseRedirect
|
||||||
from django.shortcuts import get_object_or_404, redirect, render
|
from django.shortcuts import get_object_or_404, redirect, render
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
@@ -29,8 +29,8 @@ from . import filters, forms, tables
|
|||||||
from .models import (
|
from .models import (
|
||||||
CONNECTION_STATUS_CONNECTED, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device,
|
CONNECTION_STATUS_CONNECTED, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device,
|
||||||
DeviceBay, DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate,
|
DeviceBay, DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate,
|
||||||
Manufacturer, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
|
Manufacturer, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack,
|
||||||
RackReservation, RackRole, Region, Site,
|
RackGroup, RackReservation, RackRole, Region, Site,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -141,6 +141,44 @@ class ComponentDeleteView(ObjectDeleteView):
|
|||||||
return obj.device.get_absolute_url()
|
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
|
# Regions
|
||||||
#
|
#
|
||||||
@@ -151,8 +189,8 @@ class RegionListView(ObjectListView):
|
|||||||
template_name = 'dcim/region_list.html'
|
template_name = 'dcim/region_list.html'
|
||||||
|
|
||||||
|
|
||||||
class RegionEditView(PermissionRequiredMixin, ObjectEditView):
|
class RegionCreateView(PermissionRequiredMixin, ObjectEditView):
|
||||||
permission_required = 'dcim.change_region'
|
permission_required = 'dcim.add_region'
|
||||||
model = Region
|
model = Region
|
||||||
form_class = forms.RegionForm
|
form_class = forms.RegionForm
|
||||||
|
|
||||||
@@ -160,9 +198,15 @@ class RegionEditView(PermissionRequiredMixin, ObjectEditView):
|
|||||||
return reverse('dcim:region_list')
|
return reverse('dcim:region_list')
|
||||||
|
|
||||||
|
|
||||||
|
class RegionEditView(RegionCreateView):
|
||||||
|
permission_required = 'dcim.change_region'
|
||||||
|
|
||||||
|
|
||||||
class RegionBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
class RegionBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
permission_required = 'dcim.delete_region'
|
permission_required = 'dcim.delete_region'
|
||||||
cls = Region
|
cls = Region
|
||||||
|
queryset = Region.objects.annotate(site_count=Count('sites'))
|
||||||
|
table = tables.RegionTable
|
||||||
default_return_url = 'dcim:region_list'
|
default_return_url = 'dcim:region_list'
|
||||||
|
|
||||||
|
|
||||||
@@ -174,7 +218,7 @@ class SiteListView(ObjectListView):
|
|||||||
queryset = Site.objects.select_related('region', 'tenant')
|
queryset = Site.objects.select_related('region', 'tenant')
|
||||||
filter = filters.SiteFilter
|
filter = filters.SiteFilter
|
||||||
filter_form = forms.SiteFilterForm
|
filter_form = forms.SiteFilterForm
|
||||||
table = tables.SiteTable
|
table = tables.SiteDetailTable
|
||||||
template_name = 'dcim/site_list.html'
|
template_name = 'dcim/site_list.html'
|
||||||
|
|
||||||
|
|
||||||
@@ -203,14 +247,18 @@ class SiteView(View):
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
class SiteEditView(PermissionRequiredMixin, ObjectEditView):
|
class SiteCreateView(PermissionRequiredMixin, ObjectEditView):
|
||||||
permission_required = 'dcim.change_site'
|
permission_required = 'dcim.add_site'
|
||||||
model = Site
|
model = Site
|
||||||
form_class = forms.SiteForm
|
form_class = forms.SiteForm
|
||||||
template_name = 'dcim/site_edit.html'
|
template_name = 'dcim/site_edit.html'
|
||||||
default_return_url = 'dcim:site_list'
|
default_return_url = 'dcim:site_list'
|
||||||
|
|
||||||
|
|
||||||
|
class SiteEditView(SiteCreateView):
|
||||||
|
permission_required = 'dcim.change_site'
|
||||||
|
|
||||||
|
|
||||||
class SiteDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
class SiteDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||||
permission_required = 'dcim.delete_site'
|
permission_required = 'dcim.delete_site'
|
||||||
model = Site
|
model = Site
|
||||||
@@ -219,18 +267,18 @@ class SiteDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
|||||||
|
|
||||||
class SiteBulkImportView(PermissionRequiredMixin, BulkImportView):
|
class SiteBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||||
permission_required = 'dcim.add_site'
|
permission_required = 'dcim.add_site'
|
||||||
form = forms.SiteImportForm
|
model_form = forms.SiteCSVForm
|
||||||
table = tables.SiteTable
|
table = tables.SiteTable
|
||||||
template_name = 'dcim/site_import.html'
|
|
||||||
default_return_url = 'dcim:site_list'
|
default_return_url = 'dcim:site_list'
|
||||||
|
|
||||||
|
|
||||||
class SiteBulkEditView(PermissionRequiredMixin, BulkEditView):
|
class SiteBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||||
permission_required = 'dcim.change_site'
|
permission_required = 'dcim.change_site'
|
||||||
cls = Site
|
cls = Site
|
||||||
|
queryset = Site.objects.select_related('region', 'tenant')
|
||||||
filter = filters.SiteFilter
|
filter = filters.SiteFilter
|
||||||
|
table = tables.SiteTable
|
||||||
form = forms.SiteBulkEditForm
|
form = forms.SiteBulkEditForm
|
||||||
template_name = 'dcim/site_bulk_edit.html'
|
|
||||||
default_return_url = 'dcim:site_list'
|
default_return_url = 'dcim:site_list'
|
||||||
|
|
||||||
|
|
||||||
@@ -246,8 +294,8 @@ class RackGroupListView(ObjectListView):
|
|||||||
template_name = 'dcim/rackgroup_list.html'
|
template_name = 'dcim/rackgroup_list.html'
|
||||||
|
|
||||||
|
|
||||||
class RackGroupEditView(PermissionRequiredMixin, ObjectEditView):
|
class RackGroupCreateView(PermissionRequiredMixin, ObjectEditView):
|
||||||
permission_required = 'dcim.change_rackgroup'
|
permission_required = 'dcim.add_rackgroup'
|
||||||
model = RackGroup
|
model = RackGroup
|
||||||
form_class = forms.RackGroupForm
|
form_class = forms.RackGroupForm
|
||||||
|
|
||||||
@@ -255,10 +303,16 @@ class RackGroupEditView(PermissionRequiredMixin, ObjectEditView):
|
|||||||
return reverse('dcim:rackgroup_list')
|
return reverse('dcim:rackgroup_list')
|
||||||
|
|
||||||
|
|
||||||
|
class RackGroupEditView(RackGroupCreateView):
|
||||||
|
permission_required = 'dcim.change_rackgroup'
|
||||||
|
|
||||||
|
|
||||||
class RackGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
class RackGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
permission_required = 'dcim.delete_rackgroup'
|
permission_required = 'dcim.delete_rackgroup'
|
||||||
cls = RackGroup
|
cls = RackGroup
|
||||||
|
queryset = RackGroup.objects.select_related('site').annotate(rack_count=Count('racks'))
|
||||||
filter = filters.RackGroupFilter
|
filter = filters.RackGroupFilter
|
||||||
|
table = tables.RackGroupTable
|
||||||
default_return_url = 'dcim:rackgroup_list'
|
default_return_url = 'dcim:rackgroup_list'
|
||||||
|
|
||||||
|
|
||||||
@@ -272,8 +326,8 @@ class RackRoleListView(ObjectListView):
|
|||||||
template_name = 'dcim/rackrole_list.html'
|
template_name = 'dcim/rackrole_list.html'
|
||||||
|
|
||||||
|
|
||||||
class RackRoleEditView(PermissionRequiredMixin, ObjectEditView):
|
class RackRoleCreateView(PermissionRequiredMixin, ObjectEditView):
|
||||||
permission_required = 'dcim.change_rackrole'
|
permission_required = 'dcim.add_rackrole'
|
||||||
model = RackRole
|
model = RackRole
|
||||||
form_class = forms.RackRoleForm
|
form_class = forms.RackRoleForm
|
||||||
|
|
||||||
@@ -281,9 +335,15 @@ class RackRoleEditView(PermissionRequiredMixin, ObjectEditView):
|
|||||||
return reverse('dcim:rackrole_list')
|
return reverse('dcim:rackrole_list')
|
||||||
|
|
||||||
|
|
||||||
|
class RackRoleEditView(RackRoleCreateView):
|
||||||
|
permission_required = 'dcim.change_rackrole'
|
||||||
|
|
||||||
|
|
||||||
class RackRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
class RackRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
permission_required = 'dcim.delete_rackrole'
|
permission_required = 'dcim.delete_rackrole'
|
||||||
cls = RackRole
|
cls = RackRole
|
||||||
|
queryset = RackRole.objects.annotate(rack_count=Count('racks'))
|
||||||
|
table = tables.RackRoleTable
|
||||||
default_return_url = 'dcim:rackrole_list'
|
default_return_url = 'dcim:rackrole_list'
|
||||||
|
|
||||||
|
|
||||||
@@ -301,7 +361,7 @@ class RackListView(ObjectListView):
|
|||||||
)
|
)
|
||||||
filter = filters.RackFilter
|
filter = filters.RackFilter
|
||||||
filter_form = forms.RackFilterForm
|
filter_form = forms.RackFilterForm
|
||||||
table = tables.RackTable
|
table = tables.RackDetailTable
|
||||||
template_name = 'dcim/rack_list.html'
|
template_name = 'dcim/rack_list.html'
|
||||||
|
|
||||||
|
|
||||||
@@ -374,14 +434,18 @@ class RackView(View):
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
class RackEditView(PermissionRequiredMixin, ObjectEditView):
|
class RackCreateView(PermissionRequiredMixin, ObjectEditView):
|
||||||
permission_required = 'dcim.change_rack'
|
permission_required = 'dcim.add_rack'
|
||||||
model = Rack
|
model = Rack
|
||||||
form_class = forms.RackForm
|
form_class = forms.RackForm
|
||||||
template_name = 'dcim/rack_edit.html'
|
template_name = 'dcim/rack_edit.html'
|
||||||
default_return_url = 'dcim:rack_list'
|
default_return_url = 'dcim:rack_list'
|
||||||
|
|
||||||
|
|
||||||
|
class RackEditView(RackCreateView):
|
||||||
|
permission_required = 'dcim.change_rack'
|
||||||
|
|
||||||
|
|
||||||
class RackDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
class RackDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||||
permission_required = 'dcim.delete_rack'
|
permission_required = 'dcim.delete_rack'
|
||||||
model = Rack
|
model = Rack
|
||||||
@@ -390,25 +454,27 @@ class RackDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
|||||||
|
|
||||||
class RackBulkImportView(PermissionRequiredMixin, BulkImportView):
|
class RackBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||||
permission_required = 'dcim.add_rack'
|
permission_required = 'dcim.add_rack'
|
||||||
form = forms.RackImportForm
|
model_form = forms.RackCSVForm
|
||||||
table = tables.RackImportTable
|
table = tables.RackImportTable
|
||||||
template_name = 'dcim/rack_import.html'
|
|
||||||
default_return_url = 'dcim:rack_list'
|
default_return_url = 'dcim:rack_list'
|
||||||
|
|
||||||
|
|
||||||
class RackBulkEditView(PermissionRequiredMixin, BulkEditView):
|
class RackBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||||
permission_required = 'dcim.change_rack'
|
permission_required = 'dcim.change_rack'
|
||||||
cls = Rack
|
cls = Rack
|
||||||
|
queryset = Rack.objects.select_related('site', 'group', 'tenant', 'role')
|
||||||
filter = filters.RackFilter
|
filter = filters.RackFilter
|
||||||
|
table = tables.RackTable
|
||||||
form = forms.RackBulkEditForm
|
form = forms.RackBulkEditForm
|
||||||
template_name = 'dcim/rack_bulk_edit.html'
|
|
||||||
default_return_url = 'dcim:rack_list'
|
default_return_url = 'dcim:rack_list'
|
||||||
|
|
||||||
|
|
||||||
class RackBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
class RackBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
permission_required = 'dcim.delete_rack'
|
permission_required = 'dcim.delete_rack'
|
||||||
cls = Rack
|
cls = Rack
|
||||||
|
queryset = Rack.objects.select_related('site', 'group', 'tenant', 'role')
|
||||||
filter = filters.RackFilter
|
filter = filters.RackFilter
|
||||||
|
table = tables.RackTable
|
||||||
default_return_url = 'dcim:rack_list'
|
default_return_url = 'dcim:rack_list'
|
||||||
|
|
||||||
|
|
||||||
@@ -424,8 +490,8 @@ class RackReservationListView(ObjectListView):
|
|||||||
template_name = 'dcim/rackreservation_list.html'
|
template_name = 'dcim/rackreservation_list.html'
|
||||||
|
|
||||||
|
|
||||||
class RackReservationEditView(PermissionRequiredMixin, ObjectEditView):
|
class RackReservationCreateView(PermissionRequiredMixin, ObjectEditView):
|
||||||
permission_required = 'dcim.change_rackreservation'
|
permission_required = 'dcim.add_rackreservation'
|
||||||
model = RackReservation
|
model = RackReservation
|
||||||
form_class = forms.RackReservationForm
|
form_class = forms.RackReservationForm
|
||||||
|
|
||||||
@@ -439,6 +505,10 @@ class RackReservationEditView(PermissionRequiredMixin, ObjectEditView):
|
|||||||
return obj.rack.get_absolute_url()
|
return obj.rack.get_absolute_url()
|
||||||
|
|
||||||
|
|
||||||
|
class RackReservationEditView(RackReservationCreateView):
|
||||||
|
permission_required = 'dcim.change_rackreservation'
|
||||||
|
|
||||||
|
|
||||||
class RackReservationDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
class RackReservationDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||||
permission_required = 'dcim.delete_rackreservation'
|
permission_required = 'dcim.delete_rackreservation'
|
||||||
model = RackReservation
|
model = RackReservation
|
||||||
@@ -450,6 +520,7 @@ class RackReservationDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
|||||||
class RackReservationBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
class RackReservationBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
permission_required = 'dcim.delete_rackreservation'
|
permission_required = 'dcim.delete_rackreservation'
|
||||||
cls = RackReservation
|
cls = RackReservation
|
||||||
|
table = tables.RackReservationTable
|
||||||
default_return_url = 'dcim:rackreservation_list'
|
default_return_url = 'dcim:rackreservation_list'
|
||||||
|
|
||||||
|
|
||||||
@@ -463,8 +534,8 @@ class ManufacturerListView(ObjectListView):
|
|||||||
template_name = 'dcim/manufacturer_list.html'
|
template_name = 'dcim/manufacturer_list.html'
|
||||||
|
|
||||||
|
|
||||||
class ManufacturerEditView(PermissionRequiredMixin, ObjectEditView):
|
class ManufacturerCreateView(PermissionRequiredMixin, ObjectEditView):
|
||||||
permission_required = 'dcim.change_manufacturer'
|
permission_required = 'dcim.add_manufacturer'
|
||||||
model = Manufacturer
|
model = Manufacturer
|
||||||
form_class = forms.ManufacturerForm
|
form_class = forms.ManufacturerForm
|
||||||
|
|
||||||
@@ -472,9 +543,15 @@ class ManufacturerEditView(PermissionRequiredMixin, ObjectEditView):
|
|||||||
return reverse('dcim:manufacturer_list')
|
return reverse('dcim:manufacturer_list')
|
||||||
|
|
||||||
|
|
||||||
|
class ManufacturerEditView(ManufacturerCreateView):
|
||||||
|
permission_required = 'dcim.change_manufacturer'
|
||||||
|
|
||||||
|
|
||||||
class ManufacturerBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
class ManufacturerBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
permission_required = 'dcim.delete_manufacturer'
|
permission_required = 'dcim.delete_manufacturer'
|
||||||
cls = Manufacturer
|
cls = Manufacturer
|
||||||
|
queryset = Manufacturer.objects.annotate(devicetype_count=Count('device_types'))
|
||||||
|
table = tables.ManufacturerTable
|
||||||
default_return_url = 'dcim:manufacturer_list'
|
default_return_url = 'dcim:manufacturer_list'
|
||||||
|
|
||||||
|
|
||||||
@@ -498,36 +575,36 @@ class DeviceTypeView(View):
|
|||||||
|
|
||||||
# Component tables
|
# Component tables
|
||||||
consoleport_table = tables.ConsolePortTemplateTable(
|
consoleport_table = tables.ConsolePortTemplateTable(
|
||||||
natsorted(ConsolePortTemplate.objects.filter(device_type=devicetype), key=attrgetter('name'))
|
natsorted(ConsolePortTemplate.objects.filter(device_type=devicetype), key=attrgetter('name')),
|
||||||
|
show_header=False
|
||||||
)
|
)
|
||||||
consoleserverport_table = tables.ConsoleServerPortTemplateTable(
|
consoleserverport_table = tables.ConsoleServerPortTemplateTable(
|
||||||
natsorted(ConsoleServerPortTemplate.objects.filter(device_type=devicetype), key=attrgetter('name'))
|
natsorted(ConsoleServerPortTemplate.objects.filter(device_type=devicetype), key=attrgetter('name')),
|
||||||
|
show_header=False
|
||||||
)
|
)
|
||||||
powerport_table = tables.PowerPortTemplateTable(
|
powerport_table = tables.PowerPortTemplateTable(
|
||||||
natsorted(PowerPortTemplate.objects.filter(device_type=devicetype), key=attrgetter('name'))
|
natsorted(PowerPortTemplate.objects.filter(device_type=devicetype), key=attrgetter('name')),
|
||||||
|
show_header=False
|
||||||
)
|
)
|
||||||
poweroutlet_table = tables.PowerOutletTemplateTable(
|
poweroutlet_table = tables.PowerOutletTemplateTable(
|
||||||
natsorted(PowerOutletTemplate.objects.filter(device_type=devicetype), key=attrgetter('name'))
|
natsorted(PowerOutletTemplate.objects.filter(device_type=devicetype), key=attrgetter('name')),
|
||||||
)
|
show_header=False
|
||||||
mgmt_interface_table = tables.InterfaceTemplateTable(
|
|
||||||
list(InterfaceTemplate.objects.order_naturally(devicetype.interface_ordering).filter(
|
|
||||||
device_type=devicetype, mgmt_only=True
|
|
||||||
))
|
|
||||||
)
|
)
|
||||||
interface_table = tables.InterfaceTemplateTable(
|
interface_table = tables.InterfaceTemplateTable(
|
||||||
list(InterfaceTemplate.objects.order_naturally(devicetype.interface_ordering).filter(
|
list(InterfaceTemplate.objects.order_naturally(
|
||||||
device_type=devicetype, mgmt_only=False
|
devicetype.interface_ordering
|
||||||
))
|
).filter(device_type=devicetype)),
|
||||||
|
show_header=False
|
||||||
)
|
)
|
||||||
devicebay_table = tables.DeviceBayTemplateTable(
|
devicebay_table = tables.DeviceBayTemplateTable(
|
||||||
natsorted(DeviceBayTemplate.objects.filter(device_type=devicetype), key=attrgetter('name'))
|
natsorted(DeviceBayTemplate.objects.filter(device_type=devicetype), key=attrgetter('name')),
|
||||||
|
show_header=False
|
||||||
)
|
)
|
||||||
if request.user.has_perm('dcim.change_devicetype'):
|
if request.user.has_perm('dcim.change_devicetype'):
|
||||||
consoleport_table.base_columns['pk'].visible = True
|
consoleport_table.base_columns['pk'].visible = True
|
||||||
consoleserverport_table.base_columns['pk'].visible = True
|
consoleserverport_table.base_columns['pk'].visible = True
|
||||||
powerport_table.base_columns['pk'].visible = True
|
powerport_table.base_columns['pk'].visible = True
|
||||||
poweroutlet_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
|
interface_table.base_columns['pk'].visible = True
|
||||||
devicebay_table.base_columns['pk'].visible = True
|
devicebay_table.base_columns['pk'].visible = True
|
||||||
|
|
||||||
@@ -537,20 +614,23 @@ class DeviceTypeView(View):
|
|||||||
'consoleserverport_table': consoleserverport_table,
|
'consoleserverport_table': consoleserverport_table,
|
||||||
'powerport_table': powerport_table,
|
'powerport_table': powerport_table,
|
||||||
'poweroutlet_table': poweroutlet_table,
|
'poweroutlet_table': poweroutlet_table,
|
||||||
'mgmt_interface_table': mgmt_interface_table,
|
|
||||||
'interface_table': interface_table,
|
'interface_table': interface_table,
|
||||||
'devicebay_table': devicebay_table,
|
'devicebay_table': devicebay_table,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
class DeviceTypeEditView(PermissionRequiredMixin, ObjectEditView):
|
class DeviceTypeCreateView(PermissionRequiredMixin, ObjectEditView):
|
||||||
permission_required = 'dcim.change_devicetype'
|
permission_required = 'dcim.add_devicetype'
|
||||||
model = DeviceType
|
model = DeviceType
|
||||||
form_class = forms.DeviceTypeForm
|
form_class = forms.DeviceTypeForm
|
||||||
template_name = 'dcim/devicetype_edit.html'
|
template_name = 'dcim/devicetype_edit.html'
|
||||||
default_return_url = 'dcim:devicetype_list'
|
default_return_url = 'dcim:devicetype_list'
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceTypeEditView(DeviceTypeCreateView):
|
||||||
|
permission_required = 'dcim.change_devicetype'
|
||||||
|
|
||||||
|
|
||||||
class DeviceTypeDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
class DeviceTypeDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||||
permission_required = 'dcim.delete_devicetype'
|
permission_required = 'dcim.delete_devicetype'
|
||||||
model = DeviceType
|
model = DeviceType
|
||||||
@@ -560,16 +640,19 @@ class DeviceTypeDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
|||||||
class DeviceTypeBulkEditView(PermissionRequiredMixin, BulkEditView):
|
class DeviceTypeBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||||
permission_required = 'dcim.change_devicetype'
|
permission_required = 'dcim.change_devicetype'
|
||||||
cls = DeviceType
|
cls = DeviceType
|
||||||
|
queryset = DeviceType.objects.select_related('manufacturer').annotate(instance_count=Count('instances'))
|
||||||
filter = filters.DeviceTypeFilter
|
filter = filters.DeviceTypeFilter
|
||||||
|
table = tables.DeviceTypeTable
|
||||||
form = forms.DeviceTypeBulkEditForm
|
form = forms.DeviceTypeBulkEditForm
|
||||||
template_name = 'dcim/devicetype_bulk_edit.html'
|
|
||||||
default_return_url = 'dcim:devicetype_list'
|
default_return_url = 'dcim:devicetype_list'
|
||||||
|
|
||||||
|
|
||||||
class DeviceTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
class DeviceTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
permission_required = 'dcim.delete_devicetype'
|
permission_required = 'dcim.delete_devicetype'
|
||||||
cls = DeviceType
|
cls = DeviceType
|
||||||
|
queryset = DeviceType.objects.select_related('manufacturer').annotate(instance_count=Count('instances'))
|
||||||
filter = filters.DeviceTypeFilter
|
filter = filters.DeviceTypeFilter
|
||||||
|
table = tables.DeviceTypeTable
|
||||||
default_return_url = 'dcim:devicetype_list'
|
default_return_url = 'dcim:devicetype_list'
|
||||||
|
|
||||||
|
|
||||||
@@ -577,7 +660,7 @@ class DeviceTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
|||||||
# Device type components
|
# Device type components
|
||||||
#
|
#
|
||||||
|
|
||||||
class ConsolePortTemplateAddView(PermissionRequiredMixin, ComponentCreateView):
|
class ConsolePortTemplateCreateView(PermissionRequiredMixin, ComponentCreateView):
|
||||||
permission_required = 'dcim.add_consoleporttemplate'
|
permission_required = 'dcim.add_consoleporttemplate'
|
||||||
parent_model = DeviceType
|
parent_model = DeviceType
|
||||||
parent_field = 'device_type'
|
parent_field = 'device_type'
|
||||||
@@ -592,9 +675,10 @@ class ConsolePortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView)
|
|||||||
parent_field = 'device_type'
|
parent_field = 'device_type'
|
||||||
cls = ConsolePortTemplate
|
cls = ConsolePortTemplate
|
||||||
parent_cls = DeviceType
|
parent_cls = DeviceType
|
||||||
|
table = tables.ConsolePortTemplateTable
|
||||||
|
|
||||||
|
|
||||||
class ConsoleServerPortTemplateAddView(PermissionRequiredMixin, ComponentCreateView):
|
class ConsoleServerPortTemplateCreateView(PermissionRequiredMixin, ComponentCreateView):
|
||||||
permission_required = 'dcim.add_consoleserverporttemplate'
|
permission_required = 'dcim.add_consoleserverporttemplate'
|
||||||
parent_model = DeviceType
|
parent_model = DeviceType
|
||||||
parent_field = 'device_type'
|
parent_field = 'device_type'
|
||||||
@@ -607,9 +691,10 @@ class ConsoleServerPortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDelet
|
|||||||
permission_required = 'dcim.delete_consoleserverporttemplate'
|
permission_required = 'dcim.delete_consoleserverporttemplate'
|
||||||
cls = ConsoleServerPortTemplate
|
cls = ConsoleServerPortTemplate
|
||||||
parent_cls = DeviceType
|
parent_cls = DeviceType
|
||||||
|
table = tables.ConsoleServerPortTemplateTable
|
||||||
|
|
||||||
|
|
||||||
class PowerPortTemplateAddView(PermissionRequiredMixin, ComponentCreateView):
|
class PowerPortTemplateCreateView(PermissionRequiredMixin, ComponentCreateView):
|
||||||
permission_required = 'dcim.add_powerporttemplate'
|
permission_required = 'dcim.add_powerporttemplate'
|
||||||
parent_model = DeviceType
|
parent_model = DeviceType
|
||||||
parent_field = 'device_type'
|
parent_field = 'device_type'
|
||||||
@@ -622,9 +707,10 @@ class PowerPortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
|||||||
permission_required = 'dcim.delete_powerporttemplate'
|
permission_required = 'dcim.delete_powerporttemplate'
|
||||||
cls = PowerPortTemplate
|
cls = PowerPortTemplate
|
||||||
parent_cls = DeviceType
|
parent_cls = DeviceType
|
||||||
|
table = tables.PowerPortTemplateTable
|
||||||
|
|
||||||
|
|
||||||
class PowerOutletTemplateAddView(PermissionRequiredMixin, ComponentCreateView):
|
class PowerOutletTemplateCreateView(PermissionRequiredMixin, ComponentCreateView):
|
||||||
permission_required = 'dcim.add_poweroutlettemplate'
|
permission_required = 'dcim.add_poweroutlettemplate'
|
||||||
parent_model = DeviceType
|
parent_model = DeviceType
|
||||||
parent_field = 'device_type'
|
parent_field = 'device_type'
|
||||||
@@ -637,9 +723,10 @@ class PowerOutletTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView)
|
|||||||
permission_required = 'dcim.delete_poweroutlettemplate'
|
permission_required = 'dcim.delete_poweroutlettemplate'
|
||||||
cls = PowerOutletTemplate
|
cls = PowerOutletTemplate
|
||||||
parent_cls = DeviceType
|
parent_cls = DeviceType
|
||||||
|
table = tables.PowerOutletTemplateTable
|
||||||
|
|
||||||
|
|
||||||
class InterfaceTemplateAddView(PermissionRequiredMixin, ComponentCreateView):
|
class InterfaceTemplateCreateView(PermissionRequiredMixin, ComponentCreateView):
|
||||||
permission_required = 'dcim.add_interfacetemplate'
|
permission_required = 'dcim.add_interfacetemplate'
|
||||||
parent_model = DeviceType
|
parent_model = DeviceType
|
||||||
parent_field = 'device_type'
|
parent_field = 'device_type'
|
||||||
@@ -652,17 +739,18 @@ class InterfaceTemplateBulkEditView(PermissionRequiredMixin, BulkEditView):
|
|||||||
permission_required = 'dcim.change_interfacetemplate'
|
permission_required = 'dcim.change_interfacetemplate'
|
||||||
cls = InterfaceTemplate
|
cls = InterfaceTemplate
|
||||||
parent_cls = DeviceType
|
parent_cls = DeviceType
|
||||||
|
table = tables.InterfaceTemplateTable
|
||||||
form = forms.InterfaceTemplateBulkEditForm
|
form = forms.InterfaceTemplateBulkEditForm
|
||||||
template_name = 'dcim/interfacetemplate_bulk_edit.html'
|
|
||||||
|
|
||||||
|
|
||||||
class InterfaceTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
class InterfaceTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
permission_required = 'dcim.delete_interfacetemplate'
|
permission_required = 'dcim.delete_interfacetemplate'
|
||||||
cls = InterfaceTemplate
|
cls = InterfaceTemplate
|
||||||
parent_cls = DeviceType
|
parent_cls = DeviceType
|
||||||
|
table = tables.InterfaceTemplateTable
|
||||||
|
|
||||||
|
|
||||||
class DeviceBayTemplateAddView(PermissionRequiredMixin, ComponentCreateView):
|
class DeviceBayTemplateCreateView(PermissionRequiredMixin, ComponentCreateView):
|
||||||
permission_required = 'dcim.add_devicebaytemplate'
|
permission_required = 'dcim.add_devicebaytemplate'
|
||||||
parent_model = DeviceType
|
parent_model = DeviceType
|
||||||
parent_field = 'device_type'
|
parent_field = 'device_type'
|
||||||
@@ -675,6 +763,7 @@ class DeviceBayTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
|||||||
permission_required = 'dcim.delete_devicebaytemplate'
|
permission_required = 'dcim.delete_devicebaytemplate'
|
||||||
cls = DeviceBayTemplate
|
cls = DeviceBayTemplate
|
||||||
parent_cls = DeviceType
|
parent_cls = DeviceType
|
||||||
|
table = tables.DeviceBayTemplateTable
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -687,8 +776,8 @@ class DeviceRoleListView(ObjectListView):
|
|||||||
template_name = 'dcim/devicerole_list.html'
|
template_name = 'dcim/devicerole_list.html'
|
||||||
|
|
||||||
|
|
||||||
class DeviceRoleEditView(PermissionRequiredMixin, ObjectEditView):
|
class DeviceRoleCreateView(PermissionRequiredMixin, ObjectEditView):
|
||||||
permission_required = 'dcim.change_devicerole'
|
permission_required = 'dcim.add_devicerole'
|
||||||
model = DeviceRole
|
model = DeviceRole
|
||||||
form_class = forms.DeviceRoleForm
|
form_class = forms.DeviceRoleForm
|
||||||
|
|
||||||
@@ -696,9 +785,15 @@ class DeviceRoleEditView(PermissionRequiredMixin, ObjectEditView):
|
|||||||
return reverse('dcim:devicerole_list')
|
return reverse('dcim:devicerole_list')
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceRoleEditView(DeviceRoleCreateView):
|
||||||
|
permission_required = 'dcim.change_devicerole'
|
||||||
|
|
||||||
|
|
||||||
class DeviceRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
class DeviceRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
permission_required = 'dcim.delete_devicerole'
|
permission_required = 'dcim.delete_devicerole'
|
||||||
cls = DeviceRole
|
cls = DeviceRole
|
||||||
|
queryset = DeviceRole.objects.annotate(device_count=Count('devices'))
|
||||||
|
table = tables.DeviceRoleTable
|
||||||
default_return_url = 'dcim:devicerole_list'
|
default_return_url = 'dcim:devicerole_list'
|
||||||
|
|
||||||
|
|
||||||
@@ -712,8 +807,8 @@ class PlatformListView(ObjectListView):
|
|||||||
template_name = 'dcim/platform_list.html'
|
template_name = 'dcim/platform_list.html'
|
||||||
|
|
||||||
|
|
||||||
class PlatformEditView(PermissionRequiredMixin, ObjectEditView):
|
class PlatformCreateView(PermissionRequiredMixin, ObjectEditView):
|
||||||
permission_required = 'dcim.change_platform'
|
permission_required = 'dcim.add_platform'
|
||||||
model = Platform
|
model = Platform
|
||||||
form_class = forms.PlatformForm
|
form_class = forms.PlatformForm
|
||||||
|
|
||||||
@@ -721,9 +816,15 @@ class PlatformEditView(PermissionRequiredMixin, ObjectEditView):
|
|||||||
return reverse('dcim:platform_list')
|
return reverse('dcim:platform_list')
|
||||||
|
|
||||||
|
|
||||||
|
class PlatformEditView(PlatformCreateView):
|
||||||
|
permission_required = 'dcim.change_platform'
|
||||||
|
|
||||||
|
|
||||||
class PlatformBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
class PlatformBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
permission_required = 'dcim.delete_platform'
|
permission_required = 'dcim.delete_platform'
|
||||||
cls = Platform
|
cls = Platform
|
||||||
|
queryset = Platform.objects.annotate(device_count=Count('devices'))
|
||||||
|
table = tables.PlatformTable
|
||||||
default_return_url = 'dcim:platform_list'
|
default_return_url = 'dcim:platform_list'
|
||||||
|
|
||||||
|
|
||||||
@@ -736,7 +837,7 @@ class DeviceListView(ObjectListView):
|
|||||||
'primary_ip4', 'primary_ip6')
|
'primary_ip4', 'primary_ip6')
|
||||||
filter = filters.DeviceFilter
|
filter = filters.DeviceFilter
|
||||||
filter_form = forms.DeviceFilterForm
|
filter_form = forms.DeviceFilterForm
|
||||||
table = tables.DeviceTable
|
table = tables.DeviceDetailTable
|
||||||
template_name = 'dcim/device_list.html'
|
template_name = 'dcim/device_list.html'
|
||||||
|
|
||||||
|
|
||||||
@@ -759,14 +860,10 @@ class DeviceView(View):
|
|||||||
power_outlets = natsorted(
|
power_outlets = natsorted(
|
||||||
PowerOutlet.objects.filter(device=device).select_related('connected_port'), key=attrgetter('name')
|
PowerOutlet.objects.filter(device=device).select_related('connected_port'), key=attrgetter('name')
|
||||||
)
|
)
|
||||||
interfaces = Interface.objects.order_naturally(device.device_type.interface_ordering).filter(
|
interfaces = Interface.objects.order_naturally(
|
||||||
device=device, mgmt_only=False
|
device.device_type.interface_ordering
|
||||||
).select_related(
|
).filter(
|
||||||
'connected_as_a__interface_b__device', 'connected_as_b__interface_a__device',
|
device=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(
|
).select_related(
|
||||||
'connected_as_a__interface_b__device', 'connected_as_b__interface_a__device',
|
'connected_as_a__interface_b__device', 'connected_as_b__interface_a__device',
|
||||||
'circuit_termination__circuit'
|
'circuit_termination__circuit'
|
||||||
@@ -778,20 +875,14 @@ class DeviceView(View):
|
|||||||
services = Service.objects.filter(device=device)
|
services = Service.objects.filter(device=device)
|
||||||
secrets = device.secrets.all()
|
secrets = device.secrets.all()
|
||||||
|
|
||||||
# Find any related devices for convenient linking in the UI
|
# Find up to ten devices in the same site with the same functional role for quick reference.
|
||||||
related_devices = []
|
related_devices = Device.objects.filter(
|
||||||
if device.name:
|
site=device.site, device_role=device.device_role
|
||||||
if re.match('.+[0-9]+$', device.name):
|
).exclude(
|
||||||
# Strip 1 or more trailing digits (e.g. core-switch1)
|
pk=device.pk
|
||||||
base_name = re.match('(.*?)[0-9]+$', device.name).group(1)
|
).select_related(
|
||||||
elif re.match('.+\d[a-z]$', device.name.lower()):
|
'rack', 'device_type__manufacturer'
|
||||||
# Strip a trailing letter if preceded by a digit (e.g. dist-switch3a -> dist-switch3)
|
)[:10]
|
||||||
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 graph button on interfaces only if at least one graph has been created.
|
||||||
show_graphs = Graph.objects.filter(type=GRAPH_TYPE_INTERFACE).exists()
|
show_graphs = Graph.objects.filter(type=GRAPH_TYPE_INTERFACE).exists()
|
||||||
@@ -803,7 +894,6 @@ class DeviceView(View):
|
|||||||
'power_ports': power_ports,
|
'power_ports': power_ports,
|
||||||
'power_outlets': power_outlets,
|
'power_outlets': power_outlets,
|
||||||
'interfaces': interfaces,
|
'interfaces': interfaces,
|
||||||
'mgmt_interfaces': mgmt_interfaces,
|
|
||||||
'device_bays': device_bays,
|
'device_bays': device_bays,
|
||||||
'services': services,
|
'services': services,
|
||||||
'secrets': secrets,
|
'secrets': secrets,
|
||||||
@@ -831,7 +921,20 @@ class DeviceInventoryView(View):
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
class DeviceLLDPNeighborsView(View):
|
class DeviceStatusView(PermissionRequiredMixin, View):
|
||||||
|
permission_required = 'dcim.napalm_read'
|
||||||
|
|
||||||
|
def get(self, request, pk):
|
||||||
|
|
||||||
|
device = get_object_or_404(Device, pk=pk)
|
||||||
|
|
||||||
|
return render(request, 'dcim/device_status.html', {
|
||||||
|
'device': device,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceLLDPNeighborsView(PermissionRequiredMixin, View):
|
||||||
|
permission_required = 'dcim.napalm_read'
|
||||||
|
|
||||||
def get(self, request, pk):
|
def get(self, request, pk):
|
||||||
|
|
||||||
@@ -850,14 +953,30 @@ class DeviceLLDPNeighborsView(View):
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
class DeviceEditView(PermissionRequiredMixin, ObjectEditView):
|
class DeviceConfigView(PermissionRequiredMixin, View):
|
||||||
permission_required = 'dcim.change_device'
|
permission_required = 'dcim.napalm_read'
|
||||||
|
|
||||||
|
def get(self, request, pk):
|
||||||
|
|
||||||
|
device = get_object_or_404(Device, pk=pk)
|
||||||
|
|
||||||
|
return render(request, 'dcim/device_config.html', {
|
||||||
|
'device': device,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceCreateView(PermissionRequiredMixin, ObjectEditView):
|
||||||
|
permission_required = 'dcim.add_device'
|
||||||
model = Device
|
model = Device
|
||||||
form_class = forms.DeviceForm
|
form_class = forms.DeviceForm
|
||||||
template_name = 'dcim/device_edit.html'
|
template_name = 'dcim/device_edit.html'
|
||||||
default_return_url = 'dcim:device_list'
|
default_return_url = 'dcim:device_list'
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceEditView(DeviceCreateView):
|
||||||
|
permission_required = 'dcim.change_device'
|
||||||
|
|
||||||
|
|
||||||
class DeviceDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
class DeviceDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||||
permission_required = 'dcim.delete_device'
|
permission_required = 'dcim.delete_device'
|
||||||
model = Device
|
model = Device
|
||||||
@@ -866,7 +985,7 @@ class DeviceDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
|||||||
|
|
||||||
class DeviceBulkImportView(PermissionRequiredMixin, BulkImportView):
|
class DeviceBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||||
permission_required = 'dcim.add_device'
|
permission_required = 'dcim.add_device'
|
||||||
form = forms.DeviceImportForm
|
model_form = forms.DeviceCSVForm
|
||||||
table = tables.DeviceImportTable
|
table = tables.DeviceImportTable
|
||||||
template_name = 'dcim/device_import.html'
|
template_name = 'dcim/device_import.html'
|
||||||
default_return_url = 'dcim:device_list'
|
default_return_url = 'dcim:device_list'
|
||||||
@@ -874,37 +993,39 @@ class DeviceBulkImportView(PermissionRequiredMixin, BulkImportView):
|
|||||||
|
|
||||||
class ChildDeviceBulkImportView(PermissionRequiredMixin, BulkImportView):
|
class ChildDeviceBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||||
permission_required = 'dcim.add_device'
|
permission_required = 'dcim.add_device'
|
||||||
form = forms.ChildDeviceImportForm
|
model_form = forms.ChildDeviceCSVForm
|
||||||
table = tables.DeviceImportTable
|
table = tables.DeviceImportTable
|
||||||
template_name = 'dcim/device_import_child.html'
|
template_name = 'dcim/device_import_child.html'
|
||||||
default_return_url = 'dcim:device_list'
|
default_return_url = 'dcim:device_list'
|
||||||
|
|
||||||
def save_obj(self, obj):
|
def _save_obj(self, obj_form):
|
||||||
|
|
||||||
# Inherit site and rack from parent device
|
obj = obj_form.save()
|
||||||
obj.site = obj.parent_bay.device.site
|
|
||||||
obj.rack = obj.parent_bay.device.rack
|
|
||||||
obj.save()
|
|
||||||
|
|
||||||
# Save the reverse relation
|
# Save the reverse relation to the parent device bay
|
||||||
device_bay = obj.parent_bay
|
device_bay = obj.parent_bay
|
||||||
device_bay.installed_device = obj
|
device_bay.installed_device = obj
|
||||||
device_bay.save()
|
device_bay.save()
|
||||||
|
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
class DeviceBulkEditView(PermissionRequiredMixin, BulkEditView):
|
class DeviceBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||||
permission_required = 'dcim.change_device'
|
permission_required = 'dcim.change_device'
|
||||||
cls = Device
|
cls = Device
|
||||||
|
queryset = Device.objects.select_related('tenant', 'site', 'rack', 'device_role', 'device_type__manufacturer')
|
||||||
filter = filters.DeviceFilter
|
filter = filters.DeviceFilter
|
||||||
|
table = tables.DeviceTable
|
||||||
form = forms.DeviceBulkEditForm
|
form = forms.DeviceBulkEditForm
|
||||||
template_name = 'dcim/device_bulk_edit.html'
|
|
||||||
default_return_url = 'dcim:device_list'
|
default_return_url = 'dcim:device_list'
|
||||||
|
|
||||||
|
|
||||||
class DeviceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
class DeviceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
permission_required = 'dcim.delete_device'
|
permission_required = 'dcim.delete_device'
|
||||||
cls = Device
|
cls = Device
|
||||||
|
queryset = Device.objects.select_related('tenant', 'site', 'rack', 'device_role', 'device_type__manufacturer')
|
||||||
filter = filters.DeviceFilter
|
filter = filters.DeviceFilter
|
||||||
|
table = tables.DeviceTable
|
||||||
default_return_url = 'dcim:device_list'
|
default_return_url = 'dcim:device_list'
|
||||||
|
|
||||||
|
|
||||||
@@ -912,7 +1033,7 @@ class DeviceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
|||||||
# Console ports
|
# Console ports
|
||||||
#
|
#
|
||||||
|
|
||||||
class ConsolePortAddView(PermissionRequiredMixin, ComponentCreateView):
|
class ConsolePortCreateView(PermissionRequiredMixin, ComponentCreateView):
|
||||||
permission_required = 'dcim.add_consoleport'
|
permission_required = 'dcim.add_consoleport'
|
||||||
parent_model = Device
|
parent_model = Device
|
||||||
parent_field = 'device'
|
parent_field = 'device'
|
||||||
@@ -1012,13 +1133,13 @@ class ConsolePortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
|||||||
permission_required = 'dcim.delete_consoleport'
|
permission_required = 'dcim.delete_consoleport'
|
||||||
cls = ConsolePort
|
cls = ConsolePort
|
||||||
parent_cls = Device
|
parent_cls = Device
|
||||||
|
table = tables.ConsolePortTable
|
||||||
|
|
||||||
|
|
||||||
class ConsoleConnectionsBulkImportView(PermissionRequiredMixin, BulkImportView):
|
class ConsoleConnectionsBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||||
permission_required = 'dcim.change_consoleport'
|
permission_required = 'dcim.change_consoleport'
|
||||||
form = forms.ConsoleConnectionImportForm
|
model_form = forms.ConsoleConnectionCSVForm
|
||||||
table = tables.ConsoleConnectionTable
|
table = tables.ConsoleConnectionTable
|
||||||
template_name = 'dcim/console_connections_import.html'
|
|
||||||
default_return_url = 'dcim:console_connections_list'
|
default_return_url = 'dcim:console_connections_list'
|
||||||
|
|
||||||
|
|
||||||
@@ -1026,7 +1147,7 @@ class ConsoleConnectionsBulkImportView(PermissionRequiredMixin, BulkImportView):
|
|||||||
# Console server ports
|
# Console server ports
|
||||||
#
|
#
|
||||||
|
|
||||||
class ConsoleServerPortAddView(PermissionRequiredMixin, ComponentCreateView):
|
class ConsoleServerPortCreateView(PermissionRequiredMixin, ComponentCreateView):
|
||||||
permission_required = 'dcim.add_consoleserverport'
|
permission_required = 'dcim.add_consoleserverport'
|
||||||
parent_model = Device
|
parent_model = Device
|
||||||
parent_field = 'device'
|
parent_field = 'device'
|
||||||
@@ -1125,17 +1246,27 @@ class ConsoleServerPortDeleteView(PermissionRequiredMixin, ComponentDeleteView):
|
|||||||
model = ConsoleServerPort
|
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):
|
class ConsoleServerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
permission_required = 'dcim.delete_consoleserverport'
|
permission_required = 'dcim.delete_consoleserverport'
|
||||||
cls = ConsoleServerPort
|
cls = ConsoleServerPort
|
||||||
parent_cls = Device
|
parent_cls = Device
|
||||||
|
table = tables.ConsoleServerPortTable
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Power ports
|
# Power ports
|
||||||
#
|
#
|
||||||
|
|
||||||
class PowerPortAddView(PermissionRequiredMixin, ComponentCreateView):
|
class PowerPortCreateView(PermissionRequiredMixin, ComponentCreateView):
|
||||||
permission_required = 'dcim.add_powerport'
|
permission_required = 'dcim.add_powerport'
|
||||||
parent_model = Device
|
parent_model = Device
|
||||||
parent_field = 'device'
|
parent_field = 'device'
|
||||||
@@ -1235,13 +1366,13 @@ class PowerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
|||||||
permission_required = 'dcim.delete_powerport'
|
permission_required = 'dcim.delete_powerport'
|
||||||
cls = PowerPort
|
cls = PowerPort
|
||||||
parent_cls = Device
|
parent_cls = Device
|
||||||
|
table = tables.PowerPortTable
|
||||||
|
|
||||||
|
|
||||||
class PowerConnectionsBulkImportView(PermissionRequiredMixin, BulkImportView):
|
class PowerConnectionsBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||||
permission_required = 'dcim.change_powerport'
|
permission_required = 'dcim.change_powerport'
|
||||||
form = forms.PowerConnectionImportForm
|
model_form = forms.PowerConnectionCSVForm
|
||||||
table = tables.PowerConnectionTable
|
table = tables.PowerConnectionTable
|
||||||
template_name = 'dcim/power_connections_import.html'
|
|
||||||
default_return_url = 'dcim:power_connections_list'
|
default_return_url = 'dcim:power_connections_list'
|
||||||
|
|
||||||
|
|
||||||
@@ -1249,7 +1380,7 @@ class PowerConnectionsBulkImportView(PermissionRequiredMixin, BulkImportView):
|
|||||||
# Power outlets
|
# Power outlets
|
||||||
#
|
#
|
||||||
|
|
||||||
class PowerOutletAddView(PermissionRequiredMixin, ComponentCreateView):
|
class PowerOutletCreateView(PermissionRequiredMixin, ComponentCreateView):
|
||||||
permission_required = 'dcim.add_poweroutlet'
|
permission_required = 'dcim.add_poweroutlet'
|
||||||
parent_model = Device
|
parent_model = Device
|
||||||
parent_field = 'device'
|
parent_field = 'device'
|
||||||
@@ -1348,17 +1479,29 @@ class PowerOutletDeleteView(PermissionRequiredMixin, ComponentDeleteView):
|
|||||||
model = PowerOutlet
|
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):
|
class PowerOutletBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
permission_required = 'dcim.delete_poweroutlet'
|
permission_required = 'dcim.delete_poweroutlet'
|
||||||
cls = PowerOutlet
|
cls = PowerOutlet
|
||||||
parent_cls = Device
|
parent_cls = Device
|
||||||
|
table = tables.PowerOutletTable
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Interfaces
|
# Interfaces
|
||||||
#
|
#
|
||||||
|
|
||||||
class InterfaceAddView(PermissionRequiredMixin, ComponentCreateView):
|
class InterfaceCreateView(PermissionRequiredMixin, ComponentCreateView):
|
||||||
permission_required = 'dcim.add_interface'
|
permission_required = 'dcim.add_interface'
|
||||||
parent_model = Device
|
parent_model = Device
|
||||||
parent_field = 'device'
|
parent_field = 'device'
|
||||||
@@ -1378,25 +1521,38 @@ class InterfaceDeleteView(PermissionRequiredMixin, ComponentDeleteView):
|
|||||||
model = Interface
|
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):
|
class InterfaceBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||||
permission_required = 'dcim.change_interface'
|
permission_required = 'dcim.change_interface'
|
||||||
cls = Interface
|
cls = Interface
|
||||||
parent_cls = Device
|
parent_cls = Device
|
||||||
|
table = tables.InterfaceTable
|
||||||
form = forms.InterfaceBulkEditForm
|
form = forms.InterfaceBulkEditForm
|
||||||
template_name = 'dcim/interface_bulk_edit.html'
|
|
||||||
|
|
||||||
|
|
||||||
class InterfaceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
class InterfaceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
permission_required = 'dcim.delete_interface'
|
permission_required = 'dcim.delete_interface'
|
||||||
cls = Interface
|
cls = Interface
|
||||||
parent_cls = Device
|
parent_cls = Device
|
||||||
|
table = tables.InterfaceTable
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Device bays
|
# Device bays
|
||||||
#
|
#
|
||||||
|
|
||||||
class DeviceBayAddView(PermissionRequiredMixin, ComponentCreateView):
|
class DeviceBayCreateView(PermissionRequiredMixin, ComponentCreateView):
|
||||||
permission_required = 'dcim.add_devicebay'
|
permission_required = 'dcim.add_devicebay'
|
||||||
parent_model = Device
|
parent_model = Device
|
||||||
parent_field = 'device'
|
parent_field = 'device'
|
||||||
@@ -1470,6 +1626,7 @@ class DeviceBayBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
|||||||
permission_required = 'dcim.delete_devicebay'
|
permission_required = 'dcim.delete_devicebay'
|
||||||
cls = DeviceBay
|
cls = DeviceBay
|
||||||
parent_cls = Device
|
parent_cls = Device
|
||||||
|
table = tables.DeviceBayTable
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -1676,9 +1833,8 @@ def interfaceconnection_delete(request, pk):
|
|||||||
|
|
||||||
class InterfaceConnectionsBulkImportView(PermissionRequiredMixin, BulkImportView):
|
class InterfaceConnectionsBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||||
permission_required = 'dcim.change_interface'
|
permission_required = 'dcim.change_interface'
|
||||||
form = forms.InterfaceConnectionImportForm
|
model_form = forms.InterfaceConnectionCSVForm
|
||||||
table = tables.InterfaceConnectionTable
|
table = tables.InterfaceConnectionTable
|
||||||
template_name = 'dcim/interface_connections_import.html'
|
|
||||||
default_return_url = 'dcim:interface_connections_list'
|
default_return_url = 'dcim:interface_connections_list'
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.exceptions import ValidationError
|
from rest_framework.exceptions import ValidationError
|
||||||
@@ -6,7 +7,9 @@ from rest_framework.exceptions import ValidationError
|
|||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
|
|
||||||
from extras.models import CF_TYPE_SELECT, CustomField, CustomFieldChoice, CustomFieldValue
|
from extras.models import (
|
||||||
|
CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_SELECT, CustomField, CustomFieldChoice, CustomFieldValue,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -25,16 +28,34 @@ class CustomFieldsSerializer(serializers.BaseSerializer):
|
|||||||
|
|
||||||
for field_name, value in data.items():
|
for field_name, value in data.items():
|
||||||
|
|
||||||
|
cf = custom_fields[field_name]
|
||||||
|
|
||||||
# Validate custom field name
|
# Validate custom field name
|
||||||
if field_name not in custom_fields:
|
if field_name not in custom_fields:
|
||||||
raise ValidationError("Invalid custom field for {} objects: {}".format(content_type, field_name))
|
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
|
# Validate selected choice
|
||||||
cf = custom_fields[field_name]
|
|
||||||
if cf.type == CF_TYPE_SELECT:
|
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()]
|
valid_choices = [c.pk for c in cf.choices.all()]
|
||||||
if value not in valid_choices:
|
if value not in valid_choices:
|
||||||
raise ValidationError("Invalid choice ({}) for field {}".format(value, field_name))
|
raise ValidationError("Invalid choice for field {}: {}".format(field_name, value))
|
||||||
|
|
||||||
# Check for missing required fields
|
# Check for missing required fields
|
||||||
missing_fields = []
|
missing_fields = []
|
||||||
@@ -87,9 +108,19 @@ class CustomFieldModelSerializer(serializers.ModelSerializer):
|
|||||||
field=custom_field,
|
field=custom_field,
|
||||||
obj_type=content_type,
|
obj_type=content_type,
|
||||||
obj_id=instance.pk,
|
obj_id=instance.pk,
|
||||||
defaults={'serialized_value': value},
|
defaults={'serialized_value': custom_field.serialize_value(value)},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def validate(self, data):
|
||||||
|
"""
|
||||||
|
Enforce model validation (see utilities.api.ModelValidationMixin)
|
||||||
|
"""
|
||||||
|
model_data = data.copy()
|
||||||
|
model_data.pop('custom_fields', None)
|
||||||
|
instance = self.Meta.model(**model_data)
|
||||||
|
instance.clean()
|
||||||
|
return data
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
|
|
||||||
custom_fields = validated_data.pop('custom_fields', None)
|
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,
|
ACTION_CHOICES, ExportTemplate, Graph, GRAPH_TYPE_CHOICES, ImageAttachment, TopologyMap, UserAction,
|
||||||
)
|
)
|
||||||
from users.api.serializers import NestedUserSerializer
|
from users.api.serializers import NestedUserSerializer
|
||||||
from utilities.api import ChoiceFieldSerializer, ContentTypeFieldSerializer
|
from utilities.api import ChoiceFieldSerializer, ContentTypeFieldSerializer, ModelValidationMixin
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -104,7 +104,7 @@ class ImageAttachmentSerializer(serializers.ModelSerializer):
|
|||||||
return serializer(obj.parent, context={'request': self.context['request']}).data
|
return serializer(obj.parent, context={'request': self.context['request']}).data
|
||||||
|
|
||||||
|
|
||||||
class WritableImageAttachmentSerializer(serializers.ModelSerializer):
|
class WritableImageAttachmentSerializer(ModelValidationMixin, serializers.ModelSerializer):
|
||||||
content_type = ContentTypeFieldSerializer()
|
content_type = ContentTypeFieldSerializer()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -121,6 +121,9 @@ class WritableImageAttachmentSerializer(serializers.ModelSerializer):
|
|||||||
"Invalid parent object: {} ID {}".format(data['content_type'], data['object_id'])
|
"Invalid parent object: {} ID {}".format(data['content_type'], data['object_id'])
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Enforce model validation
|
||||||
|
super(WritableImageAttachmentSerializer, self).validate(data)
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
62
netbox/extras/constants.py
Normal file
62
netbox/extras/constants.py
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
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
|
pass
|
||||||
return queryset.filter(
|
return queryset.filter(
|
||||||
custom_field_values__field__name=self.name,
|
custom_field_values__field__name=self.name,
|
||||||
custom_field_values__serialized_value=value,
|
custom_field_values__serialized_value__icontains=value,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
72
netbox/extras/management/commands/nbshell.py
Normal file
72
netbox/extras/management/commands/nbshell.py
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
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 and constants from each app
|
||||||
|
for app in APPS:
|
||||||
|
self.django_models[app] = []
|
||||||
|
|
||||||
|
# Models
|
||||||
|
app_models = sys.modules['{}.models'.format(app)]
|
||||||
|
for name in dir(app_models):
|
||||||
|
model = getattr(app_models, name)
|
||||||
|
try:
|
||||||
|
if issubclass(model, Model) and model._meta.app_label == app:
|
||||||
|
namespace[name] = model
|
||||||
|
self.django_models[app].append(name)
|
||||||
|
except TypeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Constants
|
||||||
|
try:
|
||||||
|
app_constants = sys.modules['{}.constants'.format(app)]
|
||||||
|
for name in dir(app_constants):
|
||||||
|
namespace[name] = getattr(app_constants, name)
|
||||||
|
except KeyError:
|
||||||
|
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,62 +15,7 @@ from django.utils.encoding import python_2_unicode_compatible
|
|||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
|
|
||||||
from utilities.utils import foreground_color
|
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'),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -139,7 +84,11 @@ class CustomField(models.Model):
|
|||||||
if self.type == CF_TYPE_BOOLEAN:
|
if self.type == CF_TYPE_BOOLEAN:
|
||||||
return str(int(bool(value)))
|
return str(int(bool(value)))
|
||||||
if self.type == CF_TYPE_DATE:
|
if self.type == CF_TYPE_DATE:
|
||||||
|
# Could be date/datetime object or string
|
||||||
|
try:
|
||||||
return value.strftime('%Y-%m-%d')
|
return value.strftime('%Y-%m-%d')
|
||||||
|
except AttributeError:
|
||||||
|
return value
|
||||||
if self.type == CF_TYPE_SELECT:
|
if self.type == CF_TYPE_SELECT:
|
||||||
# Could be ModelChoiceField or TypedChoiceField
|
# Could be ModelChoiceField or TypedChoiceField
|
||||||
return str(value.id) if hasattr(value, 'id') else str(value)
|
return str(value.id) if hasattr(value, 'id') else str(value)
|
||||||
@@ -367,7 +316,8 @@ class TopologyMap(models.Model):
|
|||||||
# Add all circuits to the graph
|
# Add all circuits to the graph
|
||||||
for termination in CircuitTermination.objects.filter(term_side='A', interface__device__in=devices):
|
for termination in CircuitTermination.objects.filter(term_side='A', interface__device__in=devices):
|
||||||
peer_termination = termination.get_peer_termination()
|
peer_termination = termination.get_peer_termination()
|
||||||
if peer_termination is not None and peer_termination.interface.device in devices:
|
if (peer_termination is not None and peer_termination.interface is not None and
|
||||||
|
peer_termination.interface.device in devices):
|
||||||
graph.edge(termination.interface.device.name, peer_termination.interface.device.name, color='blue')
|
graph.edge(termination.interface.device.name, peer_termination.interface.device.name, color='blue')
|
||||||
|
|
||||||
return graph.pipe(format=img_format)
|
return graph.pipe(format=img_format)
|
||||||
@@ -382,7 +332,7 @@ def image_upload(instance, filename):
|
|||||||
path = 'image-attachments/'
|
path = 'image-attachments/'
|
||||||
|
|
||||||
# Rename the file to the provided name, if any. Attempt to preserve the file extension.
|
# Rename the file to the provided name, if any. Attempt to preserve the file extension.
|
||||||
extension = filename.rsplit('.')[-1]
|
extension = filename.rsplit('.')[-1].lower()
|
||||||
if instance.name and extension in ['bmp', 'gif', 'jpeg', 'jpg', 'png']:
|
if instance.name and extension in ['bmp', 'gif', 'jpeg', 'jpg', 'png']:
|
||||||
filename = '.'.join([instance.name, extension])
|
filename = '.'.join([instance.name, extension])
|
||||||
elif instance.name:
|
elif instance.name:
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ class ImageAttachmentEditView(PermissionRequiredMixin, ObjectEditView):
|
|||||||
|
|
||||||
|
|
||||||
class ImageAttachmentDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
class ImageAttachmentDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||||
permission_required = 'dcim.delete_imageattachment'
|
permission_required = 'extras.delete_imageattachment'
|
||||||
model = ImageAttachment
|
model = ImageAttachment
|
||||||
|
|
||||||
def get_return_url(self, request, imageattachment):
|
def get_return_url(self, request, imageattachment):
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.validators import UniqueTogetherValidator
|
from rest_framework.validators import UniqueTogetherValidator
|
||||||
@@ -6,11 +7,11 @@ from rest_framework.validators import UniqueTogetherValidator
|
|||||||
from dcim.api.serializers import NestedDeviceSerializer, InterfaceSerializer, NestedSiteSerializer
|
from dcim.api.serializers import NestedDeviceSerializer, InterfaceSerializer, NestedSiteSerializer
|
||||||
from extras.api.customfields import CustomFieldModelSerializer
|
from extras.api.customfields import CustomFieldModelSerializer
|
||||||
from ipam.models import (
|
from ipam.models import (
|
||||||
Aggregate, IPAddress, IPADDRESS_STATUS_CHOICES, IP_PROTOCOL_CHOICES, Prefix, PREFIX_STATUS_CHOICES, RIR, Role,
|
Aggregate, IPAddress, IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, IP_PROTOCOL_CHOICES, Prefix,
|
||||||
Service, VLAN, VLAN_STATUS_CHOICES, VLANGroup, VRF,
|
PREFIX_STATUS_CHOICES, RIR, Role, Service, VLAN, VLAN_STATUS_CHOICES, VLANGroup, VRF,
|
||||||
)
|
)
|
||||||
from tenancy.api.serializers import NestedTenantSerializer
|
from tenancy.api.serializers import NestedTenantSerializer
|
||||||
from utilities.api import ChoiceFieldSerializer
|
from utilities.api import ChoiceFieldSerializer, ModelValidationMixin
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -22,7 +23,7 @@ class VRFSerializer(CustomFieldModelSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = VRF
|
model = VRF
|
||||||
fields = ['id', 'name', 'rd', 'tenant', 'enforce_unique', 'description', 'custom_fields']
|
fields = ['id', 'name', 'rd', 'tenant', 'enforce_unique', 'description', 'display_name', 'custom_fields']
|
||||||
|
|
||||||
|
|
||||||
class NestedVRFSerializer(serializers.ModelSerializer):
|
class NestedVRFSerializer(serializers.ModelSerializer):
|
||||||
@@ -44,7 +45,7 @@ class WritableVRFSerializer(CustomFieldModelSerializer):
|
|||||||
# Roles
|
# Roles
|
||||||
#
|
#
|
||||||
|
|
||||||
class RoleSerializer(serializers.ModelSerializer):
|
class RoleSerializer(ModelValidationMixin, serializers.ModelSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Role
|
model = Role
|
||||||
@@ -63,7 +64,7 @@ class NestedRoleSerializer(serializers.ModelSerializer):
|
|||||||
# RIRs
|
# RIRs
|
||||||
#
|
#
|
||||||
|
|
||||||
class RIRSerializer(serializers.ModelSerializer):
|
class RIRSerializer(ModelValidationMixin, serializers.ModelSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = RIR
|
model = RIR
|
||||||
@@ -137,10 +138,13 @@ class WritableVLANGroupSerializer(serializers.ModelSerializer):
|
|||||||
# Validate uniqueness of name and slug if a site has been assigned.
|
# Validate uniqueness of name and slug if a site has been assigned.
|
||||||
if data.get('site', None):
|
if data.get('site', None):
|
||||||
for field in ['name', 'slug']:
|
for field in ['name', 'slug']:
|
||||||
validator = UniqueTogetherValidator(queryset=VLAN.objects.all(), fields=('site', field))
|
validator = UniqueTogetherValidator(queryset=VLANGroup.objects.all(), fields=('site', field))
|
||||||
validator.set_context(self)
|
validator.set_context(self)
|
||||||
validator(data)
|
validator(data)
|
||||||
|
|
||||||
|
# Enforce model validation
|
||||||
|
super(WritableVLANGroupSerializer, self).validate(data)
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
@@ -187,6 +191,9 @@ class WritableVLANSerializer(CustomFieldModelSerializer):
|
|||||||
validator.set_context(self)
|
validator.set_context(self)
|
||||||
validator(data)
|
validator(data)
|
||||||
|
|
||||||
|
# Enforce model validation
|
||||||
|
super(WritableVLANSerializer, self).validate(data)
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
@@ -236,12 +243,13 @@ class IPAddressSerializer(CustomFieldModelSerializer):
|
|||||||
vrf = NestedVRFSerializer()
|
vrf = NestedVRFSerializer()
|
||||||
tenant = NestedTenantSerializer()
|
tenant = NestedTenantSerializer()
|
||||||
status = ChoiceFieldSerializer(choices=IPADDRESS_STATUS_CHOICES)
|
status = ChoiceFieldSerializer(choices=IPADDRESS_STATUS_CHOICES)
|
||||||
|
role = ChoiceFieldSerializer(choices=IPADDRESS_ROLE_CHOICES)
|
||||||
interface = InterfaceSerializer()
|
interface = InterfaceSerializer()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = IPAddress
|
model = IPAddress
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'family', 'address', 'vrf', 'tenant', 'status', 'interface', 'description', 'nat_inside',
|
'id', 'family', 'address', 'vrf', 'tenant', 'status', 'role', 'interface', 'description', 'nat_inside',
|
||||||
'nat_outside', 'custom_fields',
|
'nat_outside', 'custom_fields',
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -261,7 +269,24 @@ class WritableIPAddressSerializer(CustomFieldModelSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = IPAddress
|
model = IPAddress
|
||||||
fields = ['id', 'address', 'vrf', 'tenant', 'status', 'interface', 'description', 'nat_inside', 'custom_fields']
|
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),
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -278,6 +303,7 @@ class ServiceSerializer(serializers.ModelSerializer):
|
|||||||
fields = ['id', 'device', 'name', 'port', 'protocol', 'ipaddresses', 'description']
|
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 WritableServiceSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
from __future__ import unicode_literals
|
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 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.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
|
||||||
from ipam import filters
|
from ipam import filters
|
||||||
from extras.api.views import CustomFieldModelViewSet
|
from extras.api.views import CustomFieldModelViewSet
|
||||||
@@ -20,15 +27,6 @@ class VRFViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
|
|||||||
filter_class = filters.VRFFilter
|
filter_class = filters.VRFFilter
|
||||||
|
|
||||||
|
|
||||||
#
|
|
||||||
# Roles
|
|
||||||
#
|
|
||||||
|
|
||||||
class RoleViewSet(ModelViewSet):
|
|
||||||
queryset = Role.objects.all()
|
|
||||||
serializer_class = serializers.RoleSerializer
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# RIRs
|
# RIRs
|
||||||
#
|
#
|
||||||
@@ -50,6 +48,16 @@ class AggregateViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
|
|||||||
filter_class = filters.AggregateFilter
|
filter_class = filters.AggregateFilter
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Roles
|
||||||
|
#
|
||||||
|
|
||||||
|
class RoleViewSet(ModelViewSet):
|
||||||
|
queryset = Role.objects.all()
|
||||||
|
serializer_class = serializers.RoleSerializer
|
||||||
|
filter_class = filters.RoleFilter
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Prefixes
|
# Prefixes
|
||||||
#
|
#
|
||||||
@@ -60,6 +68,62 @@ class PrefixViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
|
|||||||
write_serializer_class = serializers.WritablePrefixSerializer
|
write_serializer_class = serializers.WritablePrefixSerializer
|
||||||
filter_class = filters.PrefixFilter
|
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
|
# IP addresses
|
||||||
|
|||||||
78
netbox/ipam/constants.py
Normal file
78
netbox/ipam/constants.py
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
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 tenancy.models import Tenant
|
||||||
from utilities.filters import NullableModelMultipleChoiceFilter, NumericInFilter
|
from utilities.filters import NullableModelMultipleChoiceFilter, NumericInFilter
|
||||||
from .models import (
|
from .models import (
|
||||||
Aggregate, IPAddress, IPADDRESS_STATUS_CHOICES, Prefix, PREFIX_STATUS_CHOICES, RIR, Role, Service, VLAN,
|
Aggregate, IPAddress, IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, Prefix, PREFIX_STATUS_CHOICES, RIR, Role,
|
||||||
VLAN_STATUS_CHOICES, VLANGroup, VRF,
|
Service, VLAN, VLAN_STATUS_CHOICES, VLANGroup, VRF,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -23,7 +23,6 @@ class VRFFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
label='Search',
|
label='Search',
|
||||||
)
|
)
|
||||||
tenant_id = NullableModelMultipleChoiceFilter(
|
tenant_id = NullableModelMultipleChoiceFilter(
|
||||||
name='tenant',
|
|
||||||
queryset=Tenant.objects.all(),
|
queryset=Tenant.objects.all(),
|
||||||
label='Tenant (ID)',
|
label='Tenant (ID)',
|
||||||
)
|
)
|
||||||
@@ -45,7 +44,7 @@ class VRFFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = VRF
|
model = VRF
|
||||||
fields = ['name', 'rd']
|
fields = ['name', 'rd', 'enforce_unique']
|
||||||
|
|
||||||
|
|
||||||
class RIRFilter(django_filters.FilterSet):
|
class RIRFilter(django_filters.FilterSet):
|
||||||
@@ -53,7 +52,7 @@ class RIRFilter(django_filters.FilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = RIR
|
model = RIR
|
||||||
fields = ['is_private']
|
fields = ['name', 'slug', 'is_private']
|
||||||
|
|
||||||
|
|
||||||
class AggregateFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
class AggregateFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||||
@@ -63,7 +62,6 @@ class AggregateFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
label='Search',
|
label='Search',
|
||||||
)
|
)
|
||||||
rir_id = django_filters.ModelMultipleChoiceFilter(
|
rir_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
name='rir',
|
|
||||||
queryset=RIR.objects.all(),
|
queryset=RIR.objects.all(),
|
||||||
label='RIR (ID)',
|
label='RIR (ID)',
|
||||||
)
|
)
|
||||||
@@ -85,11 +83,18 @@ class AggregateFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
try:
|
try:
|
||||||
prefix = str(IPNetwork(value.strip()).cidr)
|
prefix = str(IPNetwork(value.strip()).cidr)
|
||||||
qs_filter |= Q(prefix__net_contains_or_equals=prefix)
|
qs_filter |= Q(prefix__net_contains_or_equals=prefix)
|
||||||
except AddrFormatError:
|
except (AddrFormatError, ValueError):
|
||||||
pass
|
pass
|
||||||
return queryset.filter(qs_filter)
|
return queryset.filter(qs_filter)
|
||||||
|
|
||||||
|
|
||||||
|
class RoleFilter(django_filters.FilterSet):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Role
|
||||||
|
fields = ['name', 'slug']
|
||||||
|
|
||||||
|
|
||||||
class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||||
id__in = NumericInFilter(name='id', lookup_expr='in')
|
id__in = NumericInFilter(name='id', lookup_expr='in')
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
@@ -105,7 +110,6 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
label='Mask length',
|
label='Mask length',
|
||||||
)
|
)
|
||||||
vrf_id = NullableModelMultipleChoiceFilter(
|
vrf_id = NullableModelMultipleChoiceFilter(
|
||||||
name='vrf_id',
|
|
||||||
queryset=VRF.objects.all(),
|
queryset=VRF.objects.all(),
|
||||||
label='VRF',
|
label='VRF',
|
||||||
)
|
)
|
||||||
@@ -116,7 +120,6 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
label='VRF (RD)',
|
label='VRF (RD)',
|
||||||
)
|
)
|
||||||
tenant_id = NullableModelMultipleChoiceFilter(
|
tenant_id = NullableModelMultipleChoiceFilter(
|
||||||
name='tenant',
|
|
||||||
queryset=Tenant.objects.all(),
|
queryset=Tenant.objects.all(),
|
||||||
label='Tenant (ID)',
|
label='Tenant (ID)',
|
||||||
)
|
)
|
||||||
@@ -127,7 +130,6 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
label='Tenant (slug)',
|
label='Tenant (slug)',
|
||||||
)
|
)
|
||||||
site_id = NullableModelMultipleChoiceFilter(
|
site_id = NullableModelMultipleChoiceFilter(
|
||||||
name='site',
|
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
label='Site (ID)',
|
label='Site (ID)',
|
||||||
)
|
)
|
||||||
@@ -138,7 +140,6 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
label='Site (slug)',
|
label='Site (slug)',
|
||||||
)
|
)
|
||||||
vlan_id = NullableModelMultipleChoiceFilter(
|
vlan_id = NullableModelMultipleChoiceFilter(
|
||||||
name='vlan',
|
|
||||||
queryset=VLAN.objects.all(),
|
queryset=VLAN.objects.all(),
|
||||||
label='VLAN (ID)',
|
label='VLAN (ID)',
|
||||||
)
|
)
|
||||||
@@ -147,7 +148,6 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
label='VLAN number (1-4095)',
|
label='VLAN number (1-4095)',
|
||||||
)
|
)
|
||||||
role_id = NullableModelMultipleChoiceFilter(
|
role_id = NullableModelMultipleChoiceFilter(
|
||||||
name='role',
|
|
||||||
queryset=Role.objects.all(),
|
queryset=Role.objects.all(),
|
||||||
label='Role (ID)',
|
label='Role (ID)',
|
||||||
)
|
)
|
||||||
@@ -163,7 +163,7 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Prefix
|
model = Prefix
|
||||||
fields = ['family']
|
fields = ['family', 'is_pool']
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
if not value.strip():
|
if not value.strip():
|
||||||
@@ -172,7 +172,7 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
try:
|
try:
|
||||||
prefix = str(IPNetwork(value.strip()).cidr)
|
prefix = str(IPNetwork(value.strip()).cidr)
|
||||||
qs_filter |= Q(prefix__net_contains_or_equals=prefix)
|
qs_filter |= Q(prefix__net_contains_or_equals=prefix)
|
||||||
except AddrFormatError:
|
except (AddrFormatError, ValueError):
|
||||||
pass
|
pass
|
||||||
return queryset.filter(qs_filter)
|
return queryset.filter(qs_filter)
|
||||||
|
|
||||||
@@ -183,7 +183,7 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
try:
|
try:
|
||||||
query = str(IPNetwork(value).cidr)
|
query = str(IPNetwork(value).cidr)
|
||||||
return queryset.filter(prefix__net_contained_or_equal=query)
|
return queryset.filter(prefix__net_contained_or_equal=query)
|
||||||
except AddrFormatError:
|
except (AddrFormatError, ValueError):
|
||||||
return queryset.none()
|
return queryset.none()
|
||||||
|
|
||||||
def filter_mask_length(self, queryset, name, value):
|
def filter_mask_length(self, queryset, name, value):
|
||||||
@@ -207,7 +207,6 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
label='Mask length',
|
label='Mask length',
|
||||||
)
|
)
|
||||||
vrf_id = NullableModelMultipleChoiceFilter(
|
vrf_id = NullableModelMultipleChoiceFilter(
|
||||||
name='vrf_id',
|
|
||||||
queryset=VRF.objects.all(),
|
queryset=VRF.objects.all(),
|
||||||
label='VRF',
|
label='VRF',
|
||||||
)
|
)
|
||||||
@@ -218,7 +217,6 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
label='VRF (RD)',
|
label='VRF (RD)',
|
||||||
)
|
)
|
||||||
tenant_id = NullableModelMultipleChoiceFilter(
|
tenant_id = NullableModelMultipleChoiceFilter(
|
||||||
name='tenant',
|
|
||||||
queryset=Tenant.objects.all(),
|
queryset=Tenant.objects.all(),
|
||||||
label='Tenant (ID)',
|
label='Tenant (ID)',
|
||||||
)
|
)
|
||||||
@@ -240,13 +238,15 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
label='Device (name)',
|
label='Device (name)',
|
||||||
)
|
)
|
||||||
interface_id = django_filters.ModelMultipleChoiceFilter(
|
interface_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
name='interface',
|
|
||||||
queryset=Interface.objects.all(),
|
queryset=Interface.objects.all(),
|
||||||
label='Interface (ID)',
|
label='Interface (ID)',
|
||||||
)
|
)
|
||||||
status = django_filters.MultipleChoiceFilter(
|
status = django_filters.MultipleChoiceFilter(
|
||||||
choices=IPADDRESS_STATUS_CHOICES
|
choices=IPADDRESS_STATUS_CHOICES
|
||||||
)
|
)
|
||||||
|
role = django_filters.MultipleChoiceFilter(
|
||||||
|
choices=IPADDRESS_ROLE_CHOICES
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = IPAddress
|
model = IPAddress
|
||||||
@@ -259,7 +259,7 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
try:
|
try:
|
||||||
ipaddress = str(IPNetwork(value.strip()))
|
ipaddress = str(IPNetwork(value.strip()))
|
||||||
qs_filter |= Q(address__net_host=ipaddress)
|
qs_filter |= Q(address__net_host=ipaddress)
|
||||||
except AddrFormatError:
|
except (AddrFormatError, ValueError):
|
||||||
pass
|
pass
|
||||||
return queryset.filter(qs_filter)
|
return queryset.filter(qs_filter)
|
||||||
|
|
||||||
@@ -270,7 +270,7 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
try:
|
try:
|
||||||
query = str(IPNetwork(value.strip()).cidr)
|
query = str(IPNetwork(value.strip()).cidr)
|
||||||
return queryset.filter(address__net_host_contained=query)
|
return queryset.filter(address__net_host_contained=query)
|
||||||
except AddrFormatError:
|
except (AddrFormatError, ValueError):
|
||||||
return queryset.none()
|
return queryset.none()
|
||||||
|
|
||||||
def filter_mask_length(self, queryset, name, value):
|
def filter_mask_length(self, queryset, name, value):
|
||||||
@@ -281,7 +281,6 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
|
|
||||||
class VLANGroupFilter(django_filters.FilterSet):
|
class VLANGroupFilter(django_filters.FilterSet):
|
||||||
site_id = NullableModelMultipleChoiceFilter(
|
site_id = NullableModelMultipleChoiceFilter(
|
||||||
name='site',
|
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
label='Site (ID)',
|
label='Site (ID)',
|
||||||
)
|
)
|
||||||
@@ -294,7 +293,7 @@ class VLANGroupFilter(django_filters.FilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = VLANGroup
|
model = VLANGroup
|
||||||
fields = ['name']
|
fields = ['name', 'slug']
|
||||||
|
|
||||||
|
|
||||||
class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||||
@@ -304,7 +303,6 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
label='Search',
|
label='Search',
|
||||||
)
|
)
|
||||||
site_id = NullableModelMultipleChoiceFilter(
|
site_id = NullableModelMultipleChoiceFilter(
|
||||||
name='site',
|
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
label='Site (ID)',
|
label='Site (ID)',
|
||||||
)
|
)
|
||||||
@@ -315,7 +313,6 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
label='Site (slug)',
|
label='Site (slug)',
|
||||||
)
|
)
|
||||||
group_id = NullableModelMultipleChoiceFilter(
|
group_id = NullableModelMultipleChoiceFilter(
|
||||||
name='group',
|
|
||||||
queryset=VLANGroup.objects.all(),
|
queryset=VLANGroup.objects.all(),
|
||||||
label='Group (ID)',
|
label='Group (ID)',
|
||||||
)
|
)
|
||||||
@@ -326,7 +323,6 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
label='Group',
|
label='Group',
|
||||||
)
|
)
|
||||||
tenant_id = NullableModelMultipleChoiceFilter(
|
tenant_id = NullableModelMultipleChoiceFilter(
|
||||||
name='tenant',
|
|
||||||
queryset=Tenant.objects.all(),
|
queryset=Tenant.objects.all(),
|
||||||
label='Tenant (ID)',
|
label='Tenant (ID)',
|
||||||
)
|
)
|
||||||
@@ -337,7 +333,6 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
label='Tenant (slug)',
|
label='Tenant (slug)',
|
||||||
)
|
)
|
||||||
role_id = NullableModelMultipleChoiceFilter(
|
role_id = NullableModelMultipleChoiceFilter(
|
||||||
name='role',
|
|
||||||
queryset=Role.objects.all(),
|
queryset=Role.objects.all(),
|
||||||
label='Role (ID)',
|
label='Role (ID)',
|
||||||
)
|
)
|
||||||
@@ -353,7 +348,7 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = VLAN
|
model = VLAN
|
||||||
fields = ['name', 'vid']
|
fields = ['vid', 'name']
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
if not value.strip():
|
if not value.strip():
|
||||||
@@ -368,7 +363,6 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
|
|
||||||
class ServiceFilter(django_filters.FilterSet):
|
class ServiceFilter(django_filters.FilterSet):
|
||||||
device_id = django_filters.ModelMultipleChoiceFilter(
|
device_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
name='device',
|
|
||||||
queryset=Device.objects.all(),
|
queryset=Device.objects.all(),
|
||||||
label='Device (ID)',
|
label='Device (ID)',
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import MultipleObjectsReturned
|
||||||
from django.db.models import Count
|
from django.db.models import Count
|
||||||
|
|
||||||
from dcim.models import Site, Rack, Device, Interface
|
from dcim.models import Site, Rack, Device, Interface
|
||||||
@@ -9,12 +9,13 @@ from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFi
|
|||||||
from tenancy.forms import TenancyForm
|
from tenancy.forms import TenancyForm
|
||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
from utilities.forms import (
|
from utilities.forms import (
|
||||||
APISelect, BootstrapMixin, BulkEditNullBooleanSelect, BulkImportForm, ChainedModelChoiceField, CSVDataField,
|
APISelect, BootstrapMixin, BulkEditNullBooleanSelect, ChainedModelChoiceField, CSVChoiceField,
|
||||||
ExpandableIPAddressField, FilterChoiceField, Livesearch, ReturnURLForm, SlugField, add_blank_choice,
|
ExpandableIPAddressField, FilterChoiceField, FlexibleModelChoiceField, Livesearch, ReturnURLForm, SlugField,
|
||||||
|
add_blank_choice,
|
||||||
)
|
)
|
||||||
from .models import (
|
from .models import (
|
||||||
Aggregate, IPAddress, IPADDRESS_STATUS_CHOICES, Prefix, PREFIX_STATUS_CHOICES, RIR, Role, Service, VLAN,
|
Aggregate, IPAddress, IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, Prefix, PREFIX_STATUS_CHOICES, RIR, Role,
|
||||||
VLANGroup, VLAN_STATUS_CHOICES, VRF,
|
Service, VLAN, VLANGroup, VLAN_STATUS_CHOICES, VRF,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -48,17 +49,23 @@ class VRFForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class VRFFromCSVForm(forms.ModelForm):
|
class VRFCSVForm(forms.ModelForm):
|
||||||
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
|
tenant = forms.ModelChoiceField(
|
||||||
error_messages={'invalid_choice': 'Tenant not found.'})
|
queryset=Tenant.objects.all(),
|
||||||
|
required=False,
|
||||||
|
to_field_name='name',
|
||||||
|
help_text='Name of assigned tenant',
|
||||||
|
error_messages={
|
||||||
|
'invalid_choice': 'Tenant not found.',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = VRF
|
model = VRF
|
||||||
fields = ['name', 'rd', 'tenant', 'enforce_unique', 'description']
|
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):
|
class VRFBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||||
@@ -116,19 +123,21 @@ class AggregateForm(BootstrapMixin, CustomFieldForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class AggregateFromCSVForm(forms.ModelForm):
|
class AggregateCSVForm(forms.ModelForm):
|
||||||
rir = forms.ModelChoiceField(queryset=RIR.objects.all(), to_field_name='name',
|
rir = forms.ModelChoiceField(
|
||||||
error_messages={'invalid_choice': 'RIR not found.'})
|
queryset=RIR.objects.all(),
|
||||||
|
to_field_name='name',
|
||||||
|
help_text='Name of parent RIR',
|
||||||
|
error_messages={
|
||||||
|
'invalid_choice': 'RIR not found.',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Aggregate
|
model = Aggregate
|
||||||
fields = ['prefix', 'rir', 'date_added', 'description']
|
fields = ['prefix', 'rir', 'date_added', 'description']
|
||||||
|
|
||||||
|
|
||||||
class AggregateImportForm(BootstrapMixin, BulkImportForm):
|
|
||||||
csv = CSVDataField(csv_form=AggregateFromCSVForm)
|
|
||||||
|
|
||||||
|
|
||||||
class AggregateBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
class AggregateBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||||
pk = forms.ModelMultipleChoiceField(queryset=Aggregate.objects.all(), widget=forms.MultipleHiddenInput)
|
pk = forms.ModelMultipleChoiceField(queryset=Aggregate.objects.all(), widget=forms.MultipleHiddenInput)
|
||||||
rir = forms.ModelChoiceField(queryset=RIR.objects.all(), required=False, label='RIR')
|
rir = forms.ModelChoiceField(queryset=RIR.objects.all(), required=False, label='RIR')
|
||||||
@@ -172,6 +181,18 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
|||||||
required=False,
|
required=False,
|
||||||
label='Site',
|
label='Site',
|
||||||
widget=forms.Select(
|
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'}
|
attrs={'filter-for': 'vlan', 'nullable': 'true'}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -179,11 +200,12 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
|||||||
queryset=VLAN.objects.all(),
|
queryset=VLAN.objects.all(),
|
||||||
chains=(
|
chains=(
|
||||||
('site', 'site'),
|
('site', 'site'),
|
||||||
|
('group', 'vlan_group'),
|
||||||
),
|
),
|
||||||
required=False,
|
required=False,
|
||||||
label='VLAN',
|
label='VLAN',
|
||||||
widget=APISelect(
|
widget=APISelect(
|
||||||
api_url='/api/ipam/vlans/?site_id={{site}}', display_field='display_name'
|
api_url='/api/ipam/vlans/?site_id={{site}}&group_id={{vlan_group}}', display_field='display_name'
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -192,74 +214,108 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
|||||||
fields = ['prefix', 'vrf', 'site', 'vlan', 'status', 'role', 'is_pool', 'description', 'tenant_group', 'tenant']
|
fields = ['prefix', 'vrf', 'site', 'vlan', 'status', 'role', 'is_pool', 'description', 'tenant_group', 'tenant']
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
|
||||||
|
# Initialize helper selectors
|
||||||
|
instance = kwargs.get('instance')
|
||||||
|
initial = kwargs.get('initial', {}).copy()
|
||||||
|
if instance and instance.vlan is not None:
|
||||||
|
initial['vlan_group'] = instance.vlan.group
|
||||||
|
kwargs['initial'] = initial
|
||||||
|
|
||||||
super(PrefixForm, self).__init__(*args, **kwargs)
|
super(PrefixForm, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
self.fields['vrf'].empty_label = 'Global'
|
self.fields['vrf'].empty_label = 'Global'
|
||||||
|
|
||||||
|
|
||||||
class PrefixFromCSVForm(forms.ModelForm):
|
class PrefixCSVForm(forms.ModelForm):
|
||||||
vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, to_field_name='rd',
|
vrf = forms.ModelChoiceField(
|
||||||
error_messages={'invalid_choice': 'VRF not found.'})
|
queryset=VRF.objects.all(),
|
||||||
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
|
required=False,
|
||||||
error_messages={'invalid_choice': 'Tenant not found.'})
|
to_field_name='rd',
|
||||||
site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False, to_field_name='name',
|
help_text='Route distinguisher of parent VRF',
|
||||||
error_messages={'invalid_choice': 'Site not found.'})
|
error_messages={
|
||||||
vlan_group_name = forms.CharField(required=False)
|
'invalid_choice': 'VRF not found.',
|
||||||
vlan_vid = forms.IntegerField(required=False)
|
}
|
||||||
status = forms.CharField()
|
)
|
||||||
role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False, to_field_name='name',
|
tenant = forms.ModelChoiceField(
|
||||||
error_messages={'invalid_choice': 'Invalid role.'})
|
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=PREFIX_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 Meta:
|
class Meta:
|
||||||
model = Prefix
|
model = Prefix
|
||||||
fields = [
|
fields = [
|
||||||
'prefix', 'vrf', 'tenant', 'site', 'vlan_group_name', 'vlan_vid', 'status', 'role', 'is_pool',
|
'prefix', 'vrf', 'tenant', 'site', 'vlan_group', 'vlan_vid', 'status', 'role', 'is_pool', 'description',
|
||||||
'description',
|
|
||||||
]
|
]
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
|
||||||
super(PrefixFromCSVForm, self).clean()
|
super(PrefixCSVForm, self).clean()
|
||||||
|
|
||||||
site = self.cleaned_data.get('site')
|
site = self.cleaned_data.get('site')
|
||||||
vlan_group_name = self.cleaned_data.get('vlan_group_name')
|
vlan_group = self.cleaned_data.get('vlan_group')
|
||||||
vlan_vid = self.cleaned_data.get('vlan_vid')
|
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
|
# Validate VLAN
|
||||||
if vlan_vid:
|
if vlan_group and vlan_vid:
|
||||||
try:
|
try:
|
||||||
self.instance.vlan = VLAN.objects.get(site=site, group=vlan_group, vid=vlan_vid)
|
self.instance.vlan = VLAN.objects.get(site=site, group__name=vlan_group, vid=vlan_vid)
|
||||||
except VLAN.DoesNotExist:
|
except VLAN.DoesNotExist:
|
||||||
if site:
|
if site:
|
||||||
self.add_error('vlan_vid', "Invalid VLAN ID ({}) for site {}.".format(vlan_vid, site))
|
raise forms.ValidationError("VLAN {} not found in site {} group {}".format(
|
||||||
elif vlan_group:
|
vlan_vid, site, vlan_group
|
||||||
self.add_error('vlan_vid', "Invalid VLAN ID ({}) for group {}.".format(vlan_vid, vlan_group_name))
|
))
|
||||||
elif not vlan_group_name:
|
else:
|
||||||
self.add_error('vlan_vid', "Invalid global VLAN ID ({}).".format(vlan_vid))
|
raise forms.ValidationError("Global VLAN {} not found in group {}".format(vlan_vid, vlan_group))
|
||||||
except VLAN.MultipleObjectsReturned:
|
except MultipleObjectsReturned:
|
||||||
self.add_error('vlan_vid', "Multiple VLANs found ({} - VID {})".format(site, vlan_vid))
|
raise forms.ValidationError(
|
||||||
|
"Multiple VLANs with VID {} found in group {}".format(vlan_vid, vlan_group)
|
||||||
def clean_status(self):
|
)
|
||||||
status_choices = {s[1].lower(): s[0] for s in PREFIX_STATUS_CHOICES}
|
elif vlan_vid:
|
||||||
try:
|
try:
|
||||||
return status_choices[self.cleaned_data['status'].lower()]
|
self.instance.vlan = VLAN.objects.get(site=site, group__isnull=True, vid=vlan_vid)
|
||||||
except KeyError:
|
except VLAN.DoesNotExist:
|
||||||
raise ValidationError("Invalid status: {}".format(self.cleaned_data['status']))
|
if site:
|
||||||
|
raise forms.ValidationError("VLAN {} not found in site {}".format(vlan_vid, site))
|
||||||
|
else:
|
||||||
class PrefixImportForm(BootstrapMixin, BulkImportForm):
|
raise forms.ValidationError("Global VLAN {} not found".format(vlan_vid))
|
||||||
csv = CSVDataField(csv_form=PrefixFromCSVForm)
|
except MultipleObjectsReturned:
|
||||||
|
raise forms.ValidationError("Multiple VLANs with VID {} found".format(vlan_vid))
|
||||||
|
|
||||||
|
|
||||||
class PrefixBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
class PrefixBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||||
@@ -428,7 +484,7 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm)
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = IPAddress
|
model = IPAddress
|
||||||
fields = [
|
fields = [
|
||||||
'address', 'vrf', 'status', 'description', 'interface', 'primary_for_device', 'nat_site', 'nat_rack',
|
'address', 'vrf', 'status', 'role', 'description', 'interface', 'primary_for_device', 'nat_site', 'nat_rack',
|
||||||
'nat_inside', 'tenant_group', 'tenant',
|
'nat_inside', 'tenant_group', 'tenant',
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -436,12 +492,12 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm)
|
|||||||
|
|
||||||
# Initialize helper selectors
|
# Initialize helper selectors
|
||||||
instance = kwargs.get('instance')
|
instance = kwargs.get('instance')
|
||||||
initial = kwargs.get('initial', {})
|
initial = kwargs.get('initial', {}).copy()
|
||||||
if instance and instance.interface is not None:
|
if instance and instance.interface is not None:
|
||||||
initial['interface_site'] = instance.interface.device.site
|
initial['interface_site'] = instance.interface.device.site
|
||||||
initial['interface_rack'] = instance.interface.device.rack
|
initial['interface_rack'] = instance.interface.device.rack
|
||||||
initial['interface_device'] = instance.interface.device
|
initial['interface_device'] = instance.interface.device
|
||||||
if instance and instance.nat_inside is not None:
|
if instance and instance.nat_inside and instance.nat_inside.device is not None:
|
||||||
initial['nat_site'] = instance.nat_inside.device.site
|
initial['nat_site'] = instance.nat_inside.device.site
|
||||||
initial['nat_rack'] = instance.nat_inside.device.rack
|
initial['nat_rack'] = instance.nat_inside.device.rack
|
||||||
initial['nat_device'] = instance.nat_inside.device
|
initial['nat_device'] = instance.nat_inside.device
|
||||||
@@ -506,30 +562,67 @@ class IPAddressBulkAddForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = IPAddress
|
model = IPAddress
|
||||||
fields = ['address', 'status', 'vrf', 'description', 'tenant_group', 'tenant']
|
fields = ['address', 'vrf', 'status', 'role', 'description', 'tenant_group', 'tenant']
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(IPAddressBulkAddForm, self).__init__(*args, **kwargs)
|
super(IPAddressBulkAddForm, self).__init__(*args, **kwargs)
|
||||||
self.fields['vrf'].empty_label = 'Global'
|
self.fields['vrf'].empty_label = 'Global'
|
||||||
|
|
||||||
|
|
||||||
class IPAddressFromCSVForm(forms.ModelForm):
|
class IPAddressCSVForm(forms.ModelForm):
|
||||||
vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, to_field_name='rd',
|
vrf = forms.ModelChoiceField(
|
||||||
error_messages={'invalid_choice': 'VRF not found.'})
|
queryset=VRF.objects.all(),
|
||||||
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
|
required=False,
|
||||||
error_messages={'invalid_choice': 'Tenant not found.'})
|
to_field_name='rd',
|
||||||
status = forms.CharField()
|
help_text='Route distinguisher of the assigned VRF',
|
||||||
device = forms.ModelChoiceField(queryset=Device.objects.all(), required=False, to_field_name='name',
|
error_messages={
|
||||||
error_messages={'invalid_choice': 'Device not found.'})
|
'invalid_choice': 'VRF not found.',
|
||||||
interface_name = forms.CharField(required=False)
|
}
|
||||||
is_primary = forms.BooleanField(required=False)
|
)
|
||||||
|
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 Meta:
|
class Meta:
|
||||||
model = IPAddress
|
model = IPAddress
|
||||||
fields = ['address', 'vrf', 'tenant', 'status', 'device', 'interface_name', 'is_primary', 'description']
|
fields = ['address', 'vrf', 'tenant', 'status', 'role', 'device', 'interface_name', 'is_primary', 'description']
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
|
||||||
|
super(IPAddressCSVForm, self).clean()
|
||||||
|
|
||||||
device = self.cleaned_data.get('device')
|
device = self.cleaned_data.get('device')
|
||||||
interface_name = self.cleaned_data.get('interface_name')
|
interface_name = self.cleaned_data.get('interface_name')
|
||||||
is_primary = self.cleaned_data.get('is_primary')
|
is_primary = self.cleaned_data.get('is_primary')
|
||||||
@@ -537,43 +630,39 @@ class IPAddressFromCSVForm(forms.ModelForm):
|
|||||||
# Validate interface
|
# Validate interface
|
||||||
if device and interface_name:
|
if device and interface_name:
|
||||||
try:
|
try:
|
||||||
Interface.objects.get(device=device, name=interface_name)
|
self.instance.interface = Interface.objects.get(device=device, name=interface_name)
|
||||||
except Interface.DoesNotExist:
|
except Interface.DoesNotExist:
|
||||||
self.add_error('interface_name', "Invalid interface ({}) for {}".format(interface_name, device))
|
raise forms.ValidationError("Invalid interface {} for device {}".format(interface_name, device))
|
||||||
elif device and not interface_name:
|
elif device and not interface_name:
|
||||||
self.add_error('interface_name', "Device set ({}) but interface missing".format(device))
|
raise forms.ValidationError("Device set ({}) but interface missing".format(device))
|
||||||
elif interface_name and not device:
|
elif interface_name and not device:
|
||||||
self.add_error('device', "Interface set ({}) but device missing or invalid".format(interface_name))
|
raise forms.ValidationError("Interface set ({}) but device missing or invalid".format(interface_name))
|
||||||
|
|
||||||
# Validate is_primary
|
# Validate is_primary
|
||||||
if is_primary and not device:
|
if is_primary and not device:
|
||||||
self.add_error('is_primary', "No device specified; cannot set as primary IP")
|
raise forms.ValidationError("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):
|
def save(self, *args, **kwargs):
|
||||||
|
|
||||||
# Set interface
|
# Set interface
|
||||||
if self.cleaned_data['device'] and self.cleaned_data['interface_name']:
|
if self.cleaned_data['device'] and self.cleaned_data['interface_name']:
|
||||||
self.instance.interface = Interface.objects.get(device=self.cleaned_data['device'],
|
self.instance.interface = Interface.objects.get(
|
||||||
name=self.cleaned_data['interface_name'])
|
device=self.cleaned_data['device'],
|
||||||
|
name=self.cleaned_data['interface_name']
|
||||||
|
)
|
||||||
|
|
||||||
|
ipaddress = super(IPAddressCSVForm, self).save(*args, **kwargs)
|
||||||
|
|
||||||
# Set as primary for device
|
# Set as primary for device
|
||||||
if self.cleaned_data['is_primary']:
|
if self.cleaned_data['is_primary']:
|
||||||
|
device = self.cleaned_data['device']
|
||||||
if self.instance.address.version == 4:
|
if self.instance.address.version == 4:
|
||||||
self.instance.primary_ip4_for = self.cleaned_data['device']
|
device.primary_ip4 = ipaddress
|
||||||
elif self.instance.address.version == 6:
|
elif self.instance.address.version == 6:
|
||||||
self.instance.primary_ip6_for = self.cleaned_data['device']
|
device.primary_ip6 = ipaddress
|
||||||
|
device.save()
|
||||||
|
|
||||||
return super(IPAddressFromCSVForm, self).save(*args, **kwargs)
|
return ipaddress
|
||||||
|
|
||||||
|
|
||||||
class IPAddressImportForm(BootstrapMixin, BulkImportForm):
|
|
||||||
csv = CSVDataField(csv_form=IPAddressFromCSVForm)
|
|
||||||
|
|
||||||
|
|
||||||
class IPAddressBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
class IPAddressBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||||
@@ -581,10 +670,11 @@ class IPAddressBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
|||||||
vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, label='VRF')
|
vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, label='VRF')
|
||||||
tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
|
tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
|
||||||
status = forms.ChoiceField(choices=add_blank_choice(IPADDRESS_STATUS_CHOICES), 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)
|
description = forms.CharField(max_length=100, required=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
nullable_fields = ['vrf', 'tenant', 'description']
|
nullable_fields = ['vrf', 'role', 'tenant', 'description']
|
||||||
|
|
||||||
|
|
||||||
def ipaddress_status_choices():
|
def ipaddress_status_choices():
|
||||||
@@ -594,6 +684,13 @@ def ipaddress_status_choices():
|
|||||||
return [(s[0], '{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in IPADDRESS_STATUS_CHOICES]
|
return [(s[0], '{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in IPADDRESS_STATUS_CHOICES]
|
||||||
|
|
||||||
|
|
||||||
|
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):
|
class IPAddressFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||||
model = IPAddress
|
model = IPAddress
|
||||||
q = forms.CharField(required=False, label='Search')
|
q = forms.CharField(required=False, label='Search')
|
||||||
@@ -614,6 +711,7 @@ class IPAddressFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
|||||||
null_option=(0, 'None')
|
null_option=(0, 'None')
|
||||||
)
|
)
|
||||||
status = forms.MultipleChoiceField(choices=ipaddress_status_choices, required=False)
|
status = forms.MultipleChoiceField(choices=ipaddress_status_choices, required=False)
|
||||||
|
role = forms.MultipleChoiceField(choices=ipaddress_role_choices, required=False)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -673,60 +771,67 @@ class VLANForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class VLANFromCSVForm(forms.ModelForm):
|
class VLANCSVForm(forms.ModelForm):
|
||||||
site = forms.ModelChoiceField(
|
site = forms.ModelChoiceField(
|
||||||
queryset=Site.objects.all(), required=False, to_field_name='name',
|
queryset=Site.objects.all(),
|
||||||
error_messages={'invalid_choice': 'Site not found.'}
|
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
|
||||||
)
|
)
|
||||||
group_name = forms.CharField(required=False)
|
|
||||||
tenant = forms.ModelChoiceField(
|
tenant = forms.ModelChoiceField(
|
||||||
Tenant.objects.all(), to_field_name='name', required=False,
|
queryset=Tenant.objects.all(),
|
||||||
error_messages={'invalid_choice': 'Tenant not found.'}
|
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'
|
||||||
)
|
)
|
||||||
status = forms.CharField()
|
|
||||||
role = forms.ModelChoiceField(
|
role = forms.ModelChoiceField(
|
||||||
queryset=Role.objects.all(), required=False, to_field_name='name',
|
queryset=Role.objects.all(),
|
||||||
error_messages={'invalid_choice': 'Invalid role.'}
|
required=False,
|
||||||
|
to_field_name='name',
|
||||||
|
help_text='Functional role',
|
||||||
|
error_messages={
|
||||||
|
'invalid_choice': 'Invalid role.',
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = VLAN
|
model = VLAN
|
||||||
fields = ['site', 'group_name', 'vid', 'name', 'tenant', 'status', 'role', 'description']
|
fields = ['site', 'group_name', 'vid', 'name', 'tenant', 'status', 'role', 'description']
|
||||||
|
help_texts = {
|
||||||
|
'vid': 'Numeric VLAN ID (1-4095)',
|
||||||
|
'name': 'VLAN name',
|
||||||
|
}
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
|
||||||
super(VLANFromCSVForm, self).clean()
|
super(VLANCSVForm, self).clean()
|
||||||
|
|
||||||
# Validate VLANGroup
|
site = self.cleaned_data.get('site')
|
||||||
group_name = self.cleaned_data.get('group_name')
|
group_name = self.cleaned_data.get('group_name')
|
||||||
|
|
||||||
|
# Validate VLAN group
|
||||||
if group_name:
|
if group_name:
|
||||||
try:
|
try:
|
||||||
VLANGroup.objects.get(site=self.cleaned_data.get('site'), name=group_name)
|
self.instance.group = VLANGroup.objects.get(site=site, name=group_name)
|
||||||
except VLANGroup.DoesNotExist:
|
except VLANGroup.DoesNotExist:
|
||||||
self.add_error('group_name', "Invalid VLAN group {}.".format(group_name))
|
if site:
|
||||||
|
raise forms.ValidationError("VLAN group {} not found for site {}".format(group_name, site))
|
||||||
def clean_status(self):
|
else:
|
||||||
status_choices = {s[1].lower(): s[0] for s in VLAN_STATUS_CHOICES}
|
raise forms.ValidationError("Global VLAN group {} not found".format(group_name))
|
||||||
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):
|
class VLANBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||||
|
|||||||
25
netbox/ipam/migrations/0017_ipaddress_roles.py
Normal file
25
netbox/ipam/migrations/0017_ipaddress_roles.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# -*- 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,6 +1,5 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
import netaddr
|
||||||
from netaddr import IPNetwork, cidr_merge
|
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.contenttypes.fields import GenericRelation
|
from django.contrib.contenttypes.fields import GenericRelation
|
||||||
@@ -17,63 +16,10 @@ from tenancy.models import Tenant
|
|||||||
from utilities.models import CreatedUpdatedModel
|
from utilities.models import CreatedUpdatedModel
|
||||||
from utilities.sql import NullsFirstQuerySet
|
from utilities.sql import NullsFirstQuerySet
|
||||||
from utilities.utils import csv_format
|
from utilities.utils import csv_format
|
||||||
|
from .constants import *
|
||||||
from .fields import IPNetworkField, IPAddressField
|
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
|
@python_2_unicode_compatible
|
||||||
class VRF(CreatedUpdatedModel, CustomFieldModel):
|
class VRF(CreatedUpdatedModel, CustomFieldModel):
|
||||||
"""
|
"""
|
||||||
@@ -89,13 +35,15 @@ class VRF(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
description = models.CharField(max_length=100, blank=True)
|
description = models.CharField(max_length=100, blank=True)
|
||||||
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
|
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:
|
class Meta:
|
||||||
ordering = ['name']
|
ordering = ['name']
|
||||||
verbose_name = 'VRF'
|
verbose_name = 'VRF'
|
||||||
verbose_name_plural = 'VRFs'
|
verbose_name_plural = 'VRFs'
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.display_name or super(VRF, self).__str__()
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('ipam:vrf', args=[self.pk])
|
return reverse('ipam:vrf', args=[self.pk])
|
||||||
@@ -109,6 +57,12 @@ class VRF(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
self.description,
|
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
|
@python_2_unicode_compatible
|
||||||
class RIR(models.Model):
|
class RIR(models.Model):
|
||||||
@@ -146,6 +100,8 @@ class Aggregate(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
description = models.CharField(max_length=100, blank=True)
|
description = models.CharField(max_length=100, blank=True)
|
||||||
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
|
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
|
||||||
|
|
||||||
|
csv_headers = ['prefix', 'rir', 'date_added', 'description']
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['family', 'prefix']
|
ordering = ['family', 'prefix']
|
||||||
|
|
||||||
@@ -200,15 +156,11 @@ class Aggregate(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
|
|
||||||
def get_utilization(self):
|
def get_utilization(self):
|
||||||
"""
|
"""
|
||||||
Determine the utilization rate of the aggregate prefix and return it as a percentage.
|
Determine the prefix utilization of the aggregate and return it as a percentage.
|
||||||
"""
|
"""
|
||||||
child_prefixes = Prefix.objects.filter(prefix__net_contained_or_equal=str(self.prefix))
|
queryset = Prefix.objects.filter(prefix__net_contained_or_equal=str(self.prefix))
|
||||||
# Remove overlapping prefixes from list of children
|
child_prefixes = netaddr.IPSet([p.prefix for p in queryset])
|
||||||
networks = cidr_merge([c.prefix for c in child_prefixes])
|
return int(float(child_prefixes.size) / self.prefix.size * 100)
|
||||||
children_size = float(0)
|
|
||||||
for p in networks:
|
|
||||||
children_size += p.size
|
|
||||||
return int(children_size / self.prefix.size * 100)
|
|
||||||
|
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
@@ -297,6 +249,10 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
|
|
||||||
objects = PrefixQuerySet.as_manager()
|
objects = PrefixQuerySet.as_manager()
|
||||||
|
|
||||||
|
csv_headers = [
|
||||||
|
'prefix', 'vrf', 'tenant', 'site', 'vlan_group', 'vlan_vid', 'status', 'role', 'is_pool', 'description',
|
||||||
|
]
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['vrf', 'family', 'prefix']
|
ordering = ['vrf', 'family', 'prefix']
|
||||||
verbose_name_plural = 'prefixes'
|
verbose_name_plural = 'prefixes'
|
||||||
@@ -307,9 +263,6 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('ipam:prefix', args=[self.pk])
|
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):
|
def clean(self):
|
||||||
|
|
||||||
if self.prefix:
|
if self.prefix:
|
||||||
@@ -357,20 +310,64 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
self.description,
|
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
|
@property
|
||||||
def new_subnet(self):
|
def new_subnet(self):
|
||||||
if self.family == 4:
|
if self.family == 4:
|
||||||
if self.prefix.prefixlen <= 30:
|
if self.prefix.prefixlen <= 30:
|
||||||
return IPNetwork('{}/{}'.format(self.prefix.network, self.prefix.prefixlen + 1))
|
return netaddr.IPNetwork('{}/{}'.format(self.prefix.network, self.prefix.prefixlen + 1))
|
||||||
return None
|
return None
|
||||||
if self.family == 6:
|
if self.family == 6:
|
||||||
if self.prefix.prefixlen <= 126:
|
if self.prefix.prefixlen <= 126:
|
||||||
return IPNetwork('{}/{}'.format(self.prefix.network, self.prefix.prefixlen + 1))
|
return netaddr.IPNetwork('{}/{}'.format(self.prefix.network, self.prefix.prefixlen + 1))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_status_class(self):
|
|
||||||
return STATUS_CHOICE_CLASSES[self.status]
|
|
||||||
|
|
||||||
|
|
||||||
class IPAddressManager(models.Manager):
|
class IPAddressManager(models.Manager):
|
||||||
|
|
||||||
@@ -403,7 +400,13 @@ class IPAddress(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
vrf = models.ForeignKey('VRF', related_name='ip_addresses', on_delete=models.PROTECT, blank=True, null=True,
|
vrf = models.ForeignKey('VRF', related_name='ip_addresses', on_delete=models.PROTECT, blank=True, null=True,
|
||||||
verbose_name='VRF')
|
verbose_name='VRF')
|
||||||
tenant = models.ForeignKey(Tenant, related_name='ip_addresses', blank=True, null=True, on_delete=models.PROTECT)
|
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=1)
|
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'
|
||||||
|
)
|
||||||
interface = models.ForeignKey(Interface, related_name='ip_addresses', on_delete=models.CASCADE, blank=True,
|
interface = models.ForeignKey(Interface, related_name='ip_addresses', on_delete=models.CASCADE, blank=True,
|
||||||
null=True)
|
null=True)
|
||||||
nat_inside = models.OneToOneField('self', related_name='nat_outside', on_delete=models.SET_NULL, blank=True,
|
nat_inside = models.OneToOneField('self', related_name='nat_outside', on_delete=models.SET_NULL, blank=True,
|
||||||
@@ -414,6 +417,10 @@ class IPAddress(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
|
|
||||||
objects = IPAddressManager()
|
objects = IPAddressManager()
|
||||||
|
|
||||||
|
csv_headers = [
|
||||||
|
'address', 'vrf', 'tenant', 'status', 'role', 'device', 'interface_name', 'is_primary', 'description',
|
||||||
|
]
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['family', 'address']
|
ordering = ['family', 'address']
|
||||||
verbose_name = 'IP address'
|
verbose_name = 'IP address'
|
||||||
@@ -452,17 +459,19 @@ class IPAddress(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
def to_csv(self):
|
def to_csv(self):
|
||||||
|
|
||||||
# Determine if this IP is primary for a Device
|
# Determine if this IP is primary for a Device
|
||||||
is_primary = False
|
|
||||||
if self.family == 4 and getattr(self, 'primary_ip4_for', False):
|
if self.family == 4 and getattr(self, 'primary_ip4_for', False):
|
||||||
is_primary = True
|
is_primary = True
|
||||||
elif self.family == 6 and getattr(self, 'primary_ip6_for', False):
|
elif self.family == 6 and getattr(self, 'primary_ip6_for', False):
|
||||||
is_primary = True
|
is_primary = True
|
||||||
|
else:
|
||||||
|
is_primary = False
|
||||||
|
|
||||||
return csv_format([
|
return csv_format([
|
||||||
self.address,
|
self.address,
|
||||||
self.vrf.rd if self.vrf else None,
|
self.vrf.rd if self.vrf else None,
|
||||||
self.tenant.name if self.tenant else None,
|
self.tenant.name if self.tenant else None,
|
||||||
self.get_status_display(),
|
self.get_status_display(),
|
||||||
|
self.get_role_display(),
|
||||||
self.device.identifier if self.device else None,
|
self.device.identifier if self.device else None,
|
||||||
self.interface.name if self.interface else None,
|
self.interface.name if self.interface else None,
|
||||||
is_primary,
|
is_primary,
|
||||||
@@ -498,9 +507,7 @@ class VLANGroup(models.Model):
|
|||||||
verbose_name_plural = 'VLAN groups'
|
verbose_name_plural = 'VLAN groups'
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
if self.site is None:
|
|
||||||
return self.name
|
return self.name
|
||||||
return '{} - {}'.format(self.site.name, self.name)
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return "{}?group_id={}".format(reverse('ipam:vlan_list'), self.pk)
|
return "{}?group_id={}".format(reverse('ipam:vlan_list'), self.pk)
|
||||||
@@ -529,6 +536,8 @@ class VLAN(CreatedUpdatedModel, CustomFieldModel):
|
|||||||
description = models.CharField(max_length=100, blank=True)
|
description = models.CharField(max_length=100, blank=True)
|
||||||
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
|
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:
|
class Meta:
|
||||||
ordering = ['site', 'group', 'vid']
|
ordering = ['site', 'group', 'vid']
|
||||||
unique_together = [
|
unique_together = [
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from __future__ import unicode_literals
|
|||||||
import django_tables2 as tables
|
import django_tables2 as tables
|
||||||
from django_tables2.utils import Accessor
|
from django_tables2.utils import Accessor
|
||||||
|
|
||||||
from utilities.tables import BaseTable, SearchTable, ToggleColumn
|
from utilities.tables import BaseTable, ToggleColumn
|
||||||
from .models import Aggregate, IPAddress, Prefix, RIR, Role, VLAN, VLANGroup, VRF
|
from .models import Aggregate, IPAddress, Prefix, RIR, Role, VLAN, VLANGroup, VRF
|
||||||
|
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ RIR_ACTIONS = """
|
|||||||
|
|
||||||
UTILIZATION_GRAPH = """
|
UTILIZATION_GRAPH = """
|
||||||
{% load helpers %}
|
{% load helpers %}
|
||||||
{% utilization_graph value %}
|
{% if record.pk %}{% utilization_graph value %}{% else %}—{% endif %}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
ROLE_ACTIONS = """
|
ROLE_ACTIONS = """
|
||||||
@@ -152,16 +152,6 @@ class VRFTable(BaseTable):
|
|||||||
fields = ('pk', 'name', 'rd', 'tenant', 'description')
|
fields = ('pk', 'name', 'rd', 'tenant', 'description')
|
||||||
|
|
||||||
|
|
||||||
class VRFSearchTable(SearchTable):
|
|
||||||
name = tables.LinkColumn()
|
|
||||||
rd = tables.Column(verbose_name='RD')
|
|
||||||
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')])
|
|
||||||
|
|
||||||
class Meta(SearchTable.Meta):
|
|
||||||
model = VRF
|
|
||||||
fields = ('name', 'rd', 'tenant', 'description')
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# RIRs
|
# RIRs
|
||||||
#
|
#
|
||||||
@@ -171,6 +161,14 @@ class RIRTable(BaseTable):
|
|||||||
name = tables.LinkColumn(verbose_name='Name')
|
name = tables.LinkColumn(verbose_name='Name')
|
||||||
is_private = tables.BooleanColumn(verbose_name='Private')
|
is_private = tables.BooleanColumn(verbose_name='Private')
|
||||||
aggregate_count = tables.Column(verbose_name='Aggregates')
|
aggregate_count = tables.Column(verbose_name='Aggregates')
|
||||||
|
actions = tables.TemplateColumn(template_code=RIR_ACTIONS, attrs={'td': {'class': 'text-right'}}, verbose_name='')
|
||||||
|
|
||||||
|
class Meta(BaseTable.Meta):
|
||||||
|
model = RIR
|
||||||
|
fields = ('pk', 'name', 'is_private', 'aggregate_count', 'actions')
|
||||||
|
|
||||||
|
|
||||||
|
class RIRDetailTable(RIRTable):
|
||||||
stats_total = tables.Column(accessor='stats.total', verbose_name='Total',
|
stats_total = tables.Column(accessor='stats.total', verbose_name='Total',
|
||||||
footer=lambda table: sum(r.stats['total'] for r in table.data))
|
footer=lambda table: sum(r.stats['total'] for r in table.data))
|
||||||
stats_active = tables.Column(accessor='stats.active', verbose_name='Active',
|
stats_active = tables.Column(accessor='stats.active', verbose_name='Active',
|
||||||
@@ -182,12 +180,12 @@ class RIRTable(BaseTable):
|
|||||||
stats_available = tables.Column(accessor='stats.available', verbose_name='Available',
|
stats_available = tables.Column(accessor='stats.available', verbose_name='Available',
|
||||||
footer=lambda table: sum(r.stats['available'] for r in table.data))
|
footer=lambda table: sum(r.stats['available'] for r in table.data))
|
||||||
utilization = tables.TemplateColumn(template_code=RIR_UTILIZATION, verbose_name='Utilization')
|
utilization = tables.TemplateColumn(template_code=RIR_UTILIZATION, verbose_name='Utilization')
|
||||||
actions = tables.TemplateColumn(template_code=RIR_ACTIONS, attrs={'td': {'class': 'text-right'}}, verbose_name='')
|
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(RIRTable.Meta):
|
||||||
model = RIR
|
fields = (
|
||||||
fields = ('pk', 'name', 'is_private', 'aggregate_count', 'stats_total', 'stats_active', 'stats_reserved',
|
'pk', 'name', 'is_private', 'aggregate_count', 'stats_total', 'stats_active', 'stats_reserved',
|
||||||
'stats_deprecated', 'stats_available', 'utilization', 'actions')
|
'stats_deprecated', 'stats_available', 'utilization', 'actions',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -197,24 +195,21 @@ class RIRTable(BaseTable):
|
|||||||
class AggregateTable(BaseTable):
|
class AggregateTable(BaseTable):
|
||||||
pk = ToggleColumn()
|
pk = ToggleColumn()
|
||||||
prefix = tables.LinkColumn(verbose_name='Aggregate')
|
prefix = tables.LinkColumn(verbose_name='Aggregate')
|
||||||
child_count = tables.Column(verbose_name='Prefixes')
|
|
||||||
get_utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False, verbose_name='Utilization')
|
|
||||||
date_added = tables.DateColumn(format="Y-m-d", verbose_name='Added')
|
date_added = tables.DateColumn(format="Y-m-d", verbose_name='Added')
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = Aggregate
|
model = Aggregate
|
||||||
|
fields = ('pk', 'prefix', 'rir', 'date_added', 'description')
|
||||||
|
|
||||||
|
|
||||||
|
class AggregateDetailTable(AggregateTable):
|
||||||
|
child_count = tables.Column(verbose_name='Prefixes')
|
||||||
|
get_utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False, verbose_name='Utilization')
|
||||||
|
|
||||||
|
class Meta(AggregateTable.Meta):
|
||||||
fields = ('pk', 'prefix', 'rir', 'child_count', 'get_utilization', 'date_added', 'description')
|
fields = ('pk', 'prefix', 'rir', 'child_count', 'get_utilization', 'date_added', 'description')
|
||||||
|
|
||||||
|
|
||||||
class AggregateSearchTable(SearchTable):
|
|
||||||
prefix = tables.LinkColumn(verbose_name='Aggregate')
|
|
||||||
date_added = tables.DateColumn(format="Y-m-d", verbose_name='Added')
|
|
||||||
|
|
||||||
class Meta(SearchTable.Meta):
|
|
||||||
model = Aggregate
|
|
||||||
fields = ('prefix', 'rir', 'date_added', 'description')
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Roles
|
# Roles
|
||||||
#
|
#
|
||||||
@@ -254,31 +249,11 @@ class PrefixTable(BaseTable):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class PrefixBriefTable(BaseTable):
|
class PrefixDetailTable(PrefixTable):
|
||||||
prefix = tables.TemplateColumn(PREFIX_LINK_BRIEF)
|
get_utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False, verbose_name='Utilization')
|
||||||
vrf = tables.LinkColumn('ipam:vrf', args=[Accessor('vrf.pk')], default='Global')
|
|
||||||
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')])
|
|
||||||
status = tables.TemplateColumn(STATUS_LABEL)
|
|
||||||
vlan = tables.LinkColumn('ipam:vlan', args=[Accessor('vlan.pk')])
|
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(PrefixTable.Meta):
|
||||||
model = Prefix
|
fields = ('pk', 'prefix', 'status', 'vrf', 'get_utilization', 'tenant', 'site', 'vlan', 'role', 'description')
|
||||||
fields = ('prefix', 'vrf', 'status', 'site', 'vlan', 'role')
|
|
||||||
orderable = False
|
|
||||||
|
|
||||||
|
|
||||||
class PrefixSearchTable(SearchTable):
|
|
||||||
prefix = tables.TemplateColumn(PREFIX_LINK, attrs={'th': {'style': 'padding-left: 17px'}})
|
|
||||||
status = tables.TemplateColumn(STATUS_LABEL)
|
|
||||||
vrf = tables.TemplateColumn(VRF_LINK, verbose_name='VRF')
|
|
||||||
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')
|
|
||||||
role = tables.TemplateColumn(PREFIX_ROLE_LINK)
|
|
||||||
|
|
||||||
class Meta(SearchTable.Meta):
|
|
||||||
model = Prefix
|
|
||||||
fields = ('prefix', 'status', 'vrf', 'tenant', 'site', 'vlan', 'role', 'description')
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -291,43 +266,26 @@ class IPAddressTable(BaseTable):
|
|||||||
status = tables.TemplateColumn(STATUS_LABEL)
|
status = tables.TemplateColumn(STATUS_LABEL)
|
||||||
vrf = tables.TemplateColumn(VRF_LINK, verbose_name='VRF')
|
vrf = tables.TemplateColumn(VRF_LINK, verbose_name='VRF')
|
||||||
tenant = tables.TemplateColumn(TENANT_LINK)
|
tenant = tables.TemplateColumn(TENANT_LINK)
|
||||||
nat_inside = tables.LinkColumn(
|
|
||||||
'ipam:ipaddress', args=[Accessor('nat_inside.pk')], orderable=False, verbose_name='NAT (Inside)'
|
|
||||||
)
|
|
||||||
device = tables.TemplateColumn(IPADDRESS_DEVICE, orderable=False)
|
device = tables.TemplateColumn(IPADDRESS_DEVICE, orderable=False)
|
||||||
|
interface = tables.Column(orderable=False)
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = IPAddress
|
model = IPAddress
|
||||||
fields = ('pk', 'address', 'status', 'vrf', 'tenant', 'nat_inside', 'device', 'description')
|
fields = ('pk', 'address', 'vrf', 'status', 'role', 'tenant', 'device', 'interface', 'description')
|
||||||
row_attrs = {
|
row_attrs = {
|
||||||
'class': lambda record: 'success' if not isinstance(record, IPAddress) else '',
|
'class': lambda record: 'success' if not isinstance(record, IPAddress) else '',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class IPAddressBriefTable(BaseTable):
|
class IPAddressDetailTable(IPAddressTable):
|
||||||
address = tables.LinkColumn('ipam:ipaddress', args=[Accessor('pk')], verbose_name='IP Address')
|
|
||||||
device = tables.LinkColumn('dcim:device', args=[Accessor('interface.device.pk')], orderable=False)
|
|
||||||
interface = tables.Column(orderable=False)
|
|
||||||
nat_inside = tables.LinkColumn(
|
nat_inside = tables.LinkColumn(
|
||||||
'ipam:ipaddress', args=[Accessor('nat_inside.pk')], orderable=False, verbose_name='NAT (Inside)'
|
'ipam:ipaddress', args=[Accessor('nat_inside.pk')], orderable=False, verbose_name='NAT (Inside)'
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(IPAddressTable.Meta):
|
||||||
model = IPAddress
|
fields = (
|
||||||
fields = ('address', 'device', 'interface', 'nat_inside')
|
'pk', 'address', 'vrf', 'status', 'role', 'tenant', 'nat_inside', 'device', 'interface', 'description',
|
||||||
|
)
|
||||||
|
|
||||||
class IPAddressSearchTable(SearchTable):
|
|
||||||
address = tables.TemplateColumn(IPADDRESS_LINK, verbose_name='IP Address')
|
|
||||||
status = tables.TemplateColumn(STATUS_LABEL)
|
|
||||||
vrf = tables.TemplateColumn(VRF_LINK, verbose_name='VRF')
|
|
||||||
tenant = tables.TemplateColumn(TENANT_LINK)
|
|
||||||
device = tables.LinkColumn('dcim:device', args=[Accessor('interface.device.pk')], orderable=False)
|
|
||||||
interface = tables.Column(orderable=False)
|
|
||||||
|
|
||||||
class Meta(SearchTable.Meta):
|
|
||||||
model = IPAddress
|
|
||||||
fields = ('address', 'status', 'vrf', 'tenant', 'device', 'interface', 'description')
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -357,24 +315,17 @@ class VLANTable(BaseTable):
|
|||||||
vid = tables.LinkColumn('ipam:vlan', args=[Accessor('pk')], verbose_name='ID')
|
vid = tables.LinkColumn('ipam:vlan', args=[Accessor('pk')], verbose_name='ID')
|
||||||
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')])
|
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')])
|
||||||
group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group')
|
group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group')
|
||||||
prefixes = tables.TemplateColumn(VLAN_PREFIXES, orderable=False, verbose_name='Prefixes')
|
|
||||||
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')])
|
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')])
|
||||||
status = tables.TemplateColumn(STATUS_LABEL)
|
status = tables.TemplateColumn(STATUS_LABEL)
|
||||||
role = tables.TemplateColumn(VLAN_ROLE_LINK)
|
role = tables.TemplateColumn(VLAN_ROLE_LINK)
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = VLAN
|
model = VLAN
|
||||||
|
fields = ('pk', 'vid', 'site', 'group', 'name', 'tenant', 'status', 'role', 'description')
|
||||||
|
|
||||||
|
|
||||||
|
class VLANDetailTable(VLANTable):
|
||||||
|
prefixes = tables.TemplateColumn(VLAN_PREFIXES, orderable=False, verbose_name='Prefixes')
|
||||||
|
|
||||||
|
class Meta(VLANTable.Meta):
|
||||||
fields = ('pk', 'vid', 'site', 'group', 'name', 'prefixes', 'tenant', 'status', 'role', 'description')
|
fields = ('pk', 'vid', 'site', 'group', 'name', 'prefixes', 'tenant', 'status', 'role', 'description')
|
||||||
|
|
||||||
|
|
||||||
class VLANSearchTable(SearchTable):
|
|
||||||
vid = tables.LinkColumn('ipam:vlan', args=[Accessor('pk')], verbose_name='ID')
|
|
||||||
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')])
|
|
||||||
group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group')
|
|
||||||
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')])
|
|
||||||
status = tables.TemplateColumn(STATUS_LABEL)
|
|
||||||
role = tables.TemplateColumn(VLAN_ROLE_LINK)
|
|
||||||
|
|
||||||
class Meta(SearchTable.Meta):
|
|
||||||
model = VLAN
|
|
||||||
fields = ('vid', 'site', 'group', 'name', 'tenant', 'status', 'role', 'description')
|
|
||||||
|
|||||||
@@ -367,6 +367,35 @@ class PrefixTest(HttpStatusMixin, APITestCase):
|
|||||||
self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
|
self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
|
||||||
self.assertEqual(Prefix.objects.count(), 2)
|
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):
|
class IPAddressTest(HttpStatusMixin, APITestCase):
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ urlpatterns = [
|
|||||||
|
|
||||||
# VRFs
|
# VRFs
|
||||||
url(r'^vrfs/$', views.VRFListView.as_view(), name='vrf_list'),
|
url(r'^vrfs/$', views.VRFListView.as_view(), name='vrf_list'),
|
||||||
url(r'^vrfs/add/$', views.VRFEditView.as_view(), name='vrf_add'),
|
url(r'^vrfs/add/$', views.VRFCreateView.as_view(), name='vrf_add'),
|
||||||
url(r'^vrfs/import/$', views.VRFBulkImportView.as_view(), name='vrf_import'),
|
url(r'^vrfs/import/$', views.VRFBulkImportView.as_view(), name='vrf_import'),
|
||||||
url(r'^vrfs/edit/$', views.VRFBulkEditView.as_view(), name='vrf_bulk_edit'),
|
url(r'^vrfs/edit/$', views.VRFBulkEditView.as_view(), name='vrf_bulk_edit'),
|
||||||
url(r'^vrfs/delete/$', views.VRFBulkDeleteView.as_view(), name='vrf_bulk_delete'),
|
url(r'^vrfs/delete/$', views.VRFBulkDeleteView.as_view(), name='vrf_bulk_delete'),
|
||||||
@@ -20,13 +20,13 @@ urlpatterns = [
|
|||||||
|
|
||||||
# RIRs
|
# RIRs
|
||||||
url(r'^rirs/$', views.RIRListView.as_view(), name='rir_list'),
|
url(r'^rirs/$', views.RIRListView.as_view(), name='rir_list'),
|
||||||
url(r'^rirs/add/$', views.RIREditView.as_view(), name='rir_add'),
|
url(r'^rirs/add/$', views.RIRCreateView.as_view(), name='rir_add'),
|
||||||
url(r'^rirs/delete/$', views.RIRBulkDeleteView.as_view(), name='rir_bulk_delete'),
|
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'),
|
url(r'^rirs/(?P<slug>[\w-]+)/edit/$', views.RIREditView.as_view(), name='rir_edit'),
|
||||||
|
|
||||||
# Aggregates
|
# Aggregates
|
||||||
url(r'^aggregates/$', views.AggregateListView.as_view(), name='aggregate_list'),
|
url(r'^aggregates/$', views.AggregateListView.as_view(), name='aggregate_list'),
|
||||||
url(r'^aggregates/add/$', views.AggregateEditView.as_view(), name='aggregate_add'),
|
url(r'^aggregates/add/$', views.AggregateCreateView.as_view(), name='aggregate_add'),
|
||||||
url(r'^aggregates/import/$', views.AggregateBulkImportView.as_view(), name='aggregate_import'),
|
url(r'^aggregates/import/$', views.AggregateBulkImportView.as_view(), name='aggregate_import'),
|
||||||
url(r'^aggregates/edit/$', views.AggregateBulkEditView.as_view(), name='aggregate_bulk_edit'),
|
url(r'^aggregates/edit/$', views.AggregateBulkEditView.as_view(), name='aggregate_bulk_edit'),
|
||||||
url(r'^aggregates/delete/$', views.AggregateBulkDeleteView.as_view(), name='aggregate_bulk_delete'),
|
url(r'^aggregates/delete/$', views.AggregateBulkDeleteView.as_view(), name='aggregate_bulk_delete'),
|
||||||
@@ -36,13 +36,13 @@ urlpatterns = [
|
|||||||
|
|
||||||
# Roles
|
# Roles
|
||||||
url(r'^roles/$', views.RoleListView.as_view(), name='role_list'),
|
url(r'^roles/$', views.RoleListView.as_view(), name='role_list'),
|
||||||
url(r'^roles/add/$', views.RoleEditView.as_view(), name='role_add'),
|
url(r'^roles/add/$', views.RoleCreateView.as_view(), name='role_add'),
|
||||||
url(r'^roles/delete/$', views.RoleBulkDeleteView.as_view(), name='role_bulk_delete'),
|
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'),
|
url(r'^roles/(?P<slug>[\w-]+)/edit/$', views.RoleEditView.as_view(), name='role_edit'),
|
||||||
|
|
||||||
# Prefixes
|
# Prefixes
|
||||||
url(r'^prefixes/$', views.PrefixListView.as_view(), name='prefix_list'),
|
url(r'^prefixes/$', views.PrefixListView.as_view(), name='prefix_list'),
|
||||||
url(r'^prefixes/add/$', views.PrefixEditView.as_view(), name='prefix_add'),
|
url(r'^prefixes/add/$', views.PrefixCreateView.as_view(), name='prefix_add'),
|
||||||
url(r'^prefixes/import/$', views.PrefixBulkImportView.as_view(), name='prefix_import'),
|
url(r'^prefixes/import/$', views.PrefixBulkImportView.as_view(), name='prefix_import'),
|
||||||
url(r'^prefixes/edit/$', views.PrefixBulkEditView.as_view(), name='prefix_bulk_edit'),
|
url(r'^prefixes/edit/$', views.PrefixBulkEditView.as_view(), name='prefix_bulk_edit'),
|
||||||
url(r'^prefixes/delete/$', views.PrefixBulkDeleteView.as_view(), name='prefix_bulk_delete'),
|
url(r'^prefixes/delete/$', views.PrefixBulkDeleteView.as_view(), name='prefix_bulk_delete'),
|
||||||
@@ -53,8 +53,8 @@ urlpatterns = [
|
|||||||
|
|
||||||
# IP addresses
|
# IP addresses
|
||||||
url(r'^ip-addresses/$', views.IPAddressListView.as_view(), name='ipaddress_list'),
|
url(r'^ip-addresses/$', views.IPAddressListView.as_view(), name='ipaddress_list'),
|
||||||
url(r'^ip-addresses/add/$', views.IPAddressEditView.as_view(), name='ipaddress_add'),
|
url(r'^ip-addresses/add/$', views.IPAddressCreateView.as_view(), name='ipaddress_add'),
|
||||||
url(r'^ip-addresses/bulk-add/$', views.IPAddressBulkAddView.as_view(), name='ipaddress_bulk_add'),
|
url(r'^ip-addresses/bulk-add/$', views.IPAddressBulkCreateView.as_view(), name='ipaddress_bulk_add'),
|
||||||
url(r'^ip-addresses/import/$', views.IPAddressBulkImportView.as_view(), name='ipaddress_import'),
|
url(r'^ip-addresses/import/$', views.IPAddressBulkImportView.as_view(), name='ipaddress_import'),
|
||||||
url(r'^ip-addresses/edit/$', views.IPAddressBulkEditView.as_view(), name='ipaddress_bulk_edit'),
|
url(r'^ip-addresses/edit/$', views.IPAddressBulkEditView.as_view(), name='ipaddress_bulk_edit'),
|
||||||
url(r'^ip-addresses/delete/$', views.IPAddressBulkDeleteView.as_view(), name='ipaddress_bulk_delete'),
|
url(r'^ip-addresses/delete/$', views.IPAddressBulkDeleteView.as_view(), name='ipaddress_bulk_delete'),
|
||||||
@@ -64,13 +64,13 @@ urlpatterns = [
|
|||||||
|
|
||||||
# VLAN groups
|
# VLAN groups
|
||||||
url(r'^vlan-groups/$', views.VLANGroupListView.as_view(), name='vlangroup_list'),
|
url(r'^vlan-groups/$', views.VLANGroupListView.as_view(), name='vlangroup_list'),
|
||||||
url(r'^vlan-groups/add/$', views.VLANGroupEditView.as_view(), name='vlangroup_add'),
|
url(r'^vlan-groups/add/$', views.VLANGroupCreateView.as_view(), name='vlangroup_add'),
|
||||||
url(r'^vlan-groups/delete/$', views.VLANGroupBulkDeleteView.as_view(), name='vlangroup_bulk_delete'),
|
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'),
|
url(r'^vlan-groups/(?P<pk>\d+)/edit/$', views.VLANGroupEditView.as_view(), name='vlangroup_edit'),
|
||||||
|
|
||||||
# VLANs
|
# VLANs
|
||||||
url(r'^vlans/$', views.VLANListView.as_view(), name='vlan_list'),
|
url(r'^vlans/$', views.VLANListView.as_view(), name='vlan_list'),
|
||||||
url(r'^vlans/add/$', views.VLANEditView.as_view(), name='vlan_add'),
|
url(r'^vlans/add/$', views.VLANCreateView.as_view(), name='vlan_add'),
|
||||||
url(r'^vlans/import/$', views.VLANBulkImportView.as_view(), name='vlan_import'),
|
url(r'^vlans/import/$', views.VLANBulkImportView.as_view(), name='vlan_import'),
|
||||||
url(r'^vlans/edit/$', views.VLANBulkEditView.as_view(), name='vlan_bulk_edit'),
|
url(r'^vlans/edit/$', views.VLANBulkEditView.as_view(), name='vlan_bulk_edit'),
|
||||||
url(r'^vlans/delete/$', views.VLANBulkDeleteView.as_view(), name='vlan_bulk_delete'),
|
url(r'^vlans/delete/$', views.VLANBulkDeleteView.as_view(), name='vlan_bulk_delete'),
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ from django.views.generic import View
|
|||||||
from dcim.models import Device
|
from dcim.models import Device
|
||||||
from utilities.paginator import EnhancedPaginator
|
from utilities.paginator import EnhancedPaginator
|
||||||
from utilities.views import (
|
from utilities.views import (
|
||||||
BulkAddView, BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
|
BulkCreateView, BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
|
||||||
)
|
)
|
||||||
from . import filters, forms, tables
|
from . import filters, forms, tables
|
||||||
from .models import (
|
from .models import (
|
||||||
@@ -103,8 +103,8 @@ class VRFView(View):
|
|||||||
def get(self, request, pk):
|
def get(self, request, pk):
|
||||||
|
|
||||||
vrf = get_object_or_404(VRF.objects.all(), pk=pk)
|
vrf = get_object_or_404(VRF.objects.all(), pk=pk)
|
||||||
prefix_table = tables.PrefixBriefTable(
|
prefix_table = tables.PrefixTable(
|
||||||
list(Prefix.objects.filter(vrf=vrf).select_related('site', 'role'))
|
list(Prefix.objects.filter(vrf=vrf).select_related('site', 'role')), orderable=False
|
||||||
)
|
)
|
||||||
prefix_table.exclude = ('vrf',)
|
prefix_table.exclude = ('vrf',)
|
||||||
|
|
||||||
@@ -114,14 +114,18 @@ class VRFView(View):
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
class VRFEditView(PermissionRequiredMixin, ObjectEditView):
|
class VRFCreateView(PermissionRequiredMixin, ObjectEditView):
|
||||||
permission_required = 'ipam.change_vrf'
|
permission_required = 'ipam.add_vrf'
|
||||||
model = VRF
|
model = VRF
|
||||||
form_class = forms.VRFForm
|
form_class = forms.VRFForm
|
||||||
template_name = 'ipam/vrf_edit.html'
|
template_name = 'ipam/vrf_edit.html'
|
||||||
default_return_url = 'ipam:vrf_list'
|
default_return_url = 'ipam:vrf_list'
|
||||||
|
|
||||||
|
|
||||||
|
class VRFEditView(VRFCreateView):
|
||||||
|
permission_required = 'ipam.change_vrf'
|
||||||
|
|
||||||
|
|
||||||
class VRFDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
class VRFDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||||
permission_required = 'ipam.delete_vrf'
|
permission_required = 'ipam.delete_vrf'
|
||||||
model = VRF
|
model = VRF
|
||||||
@@ -130,25 +134,27 @@ class VRFDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
|||||||
|
|
||||||
class VRFBulkImportView(PermissionRequiredMixin, BulkImportView):
|
class VRFBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||||
permission_required = 'ipam.add_vrf'
|
permission_required = 'ipam.add_vrf'
|
||||||
form = forms.VRFImportForm
|
model_form = forms.VRFCSVForm
|
||||||
table = tables.VRFTable
|
table = tables.VRFTable
|
||||||
template_name = 'ipam/vrf_import.html'
|
|
||||||
default_return_url = 'ipam:vrf_list'
|
default_return_url = 'ipam:vrf_list'
|
||||||
|
|
||||||
|
|
||||||
class VRFBulkEditView(PermissionRequiredMixin, BulkEditView):
|
class VRFBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||||
permission_required = 'ipam.change_vrf'
|
permission_required = 'ipam.change_vrf'
|
||||||
cls = VRF
|
cls = VRF
|
||||||
|
queryset = VRF.objects.select_related('tenant')
|
||||||
filter = filters.VRFFilter
|
filter = filters.VRFFilter
|
||||||
|
table = tables.VRFTable
|
||||||
form = forms.VRFBulkEditForm
|
form = forms.VRFBulkEditForm
|
||||||
template_name = 'ipam/vrf_bulk_edit.html'
|
|
||||||
default_return_url = 'ipam:vrf_list'
|
default_return_url = 'ipam:vrf_list'
|
||||||
|
|
||||||
|
|
||||||
class VRFBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
class VRFBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
permission_required = 'ipam.delete_vrf'
|
permission_required = 'ipam.delete_vrf'
|
||||||
cls = VRF
|
cls = VRF
|
||||||
|
queryset = VRF.objects.select_related('tenant')
|
||||||
filter = filters.VRFFilter
|
filter = filters.VRFFilter
|
||||||
|
table = tables.VRFTable
|
||||||
default_return_url = 'ipam:vrf_list'
|
default_return_url = 'ipam:vrf_list'
|
||||||
|
|
||||||
|
|
||||||
@@ -160,7 +166,7 @@ class RIRListView(ObjectListView):
|
|||||||
queryset = RIR.objects.annotate(aggregate_count=Count('aggregates'))
|
queryset = RIR.objects.annotate(aggregate_count=Count('aggregates'))
|
||||||
filter = filters.RIRFilter
|
filter = filters.RIRFilter
|
||||||
filter_form = forms.RIRFilterForm
|
filter_form = forms.RIRFilterForm
|
||||||
table = tables.RIRTable
|
table = tables.RIRDetailTable
|
||||||
template_name = 'ipam/rir_list.html'
|
template_name = 'ipam/rir_list.html'
|
||||||
|
|
||||||
def alter_queryset(self, request):
|
def alter_queryset(self, request):
|
||||||
@@ -240,8 +246,8 @@ class RIRListView(ObjectListView):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class RIREditView(PermissionRequiredMixin, ObjectEditView):
|
class RIRCreateView(PermissionRequiredMixin, ObjectEditView):
|
||||||
permission_required = 'ipam.change_rir'
|
permission_required = 'ipam.add_rir'
|
||||||
model = RIR
|
model = RIR
|
||||||
form_class = forms.RIRForm
|
form_class = forms.RIRForm
|
||||||
|
|
||||||
@@ -249,10 +255,16 @@ class RIREditView(PermissionRequiredMixin, ObjectEditView):
|
|||||||
return reverse('ipam:rir_list')
|
return reverse('ipam:rir_list')
|
||||||
|
|
||||||
|
|
||||||
|
class RIREditView(RIRCreateView):
|
||||||
|
permission_required = 'ipam.change_rir'
|
||||||
|
|
||||||
|
|
||||||
class RIRBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
class RIRBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
permission_required = 'ipam.delete_rir'
|
permission_required = 'ipam.delete_rir'
|
||||||
cls = RIR
|
cls = RIR
|
||||||
|
queryset = RIR.objects.annotate(aggregate_count=Count('aggregates'))
|
||||||
filter = filters.RIRFilter
|
filter = filters.RIRFilter
|
||||||
|
table = tables.RIRTable
|
||||||
default_return_url = 'ipam:rir_list'
|
default_return_url = 'ipam:rir_list'
|
||||||
|
|
||||||
|
|
||||||
@@ -266,7 +278,7 @@ class AggregateListView(ObjectListView):
|
|||||||
})
|
})
|
||||||
filter = filters.AggregateFilter
|
filter = filters.AggregateFilter
|
||||||
filter_form = forms.AggregateFilterForm
|
filter_form = forms.AggregateFilterForm
|
||||||
table = tables.AggregateTable
|
table = tables.AggregateDetailTable
|
||||||
template_name = 'ipam/aggregate_list.html'
|
template_name = 'ipam/aggregate_list.html'
|
||||||
|
|
||||||
def extra_context(self):
|
def extra_context(self):
|
||||||
@@ -325,14 +337,18 @@ class AggregateView(View):
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
class AggregateEditView(PermissionRequiredMixin, ObjectEditView):
|
class AggregateCreateView(PermissionRequiredMixin, ObjectEditView):
|
||||||
permission_required = 'ipam.change_aggregate'
|
permission_required = 'ipam.add_aggregate'
|
||||||
model = Aggregate
|
model = Aggregate
|
||||||
form_class = forms.AggregateForm
|
form_class = forms.AggregateForm
|
||||||
template_name = 'ipam/aggregate_edit.html'
|
template_name = 'ipam/aggregate_edit.html'
|
||||||
default_return_url = 'ipam:aggregate_list'
|
default_return_url = 'ipam:aggregate_list'
|
||||||
|
|
||||||
|
|
||||||
|
class AggregateEditView(AggregateCreateView):
|
||||||
|
permission_required = 'ipam.change_aggregate'
|
||||||
|
|
||||||
|
|
||||||
class AggregateDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
class AggregateDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||||
permission_required = 'ipam.delete_aggregate'
|
permission_required = 'ipam.delete_aggregate'
|
||||||
model = Aggregate
|
model = Aggregate
|
||||||
@@ -341,25 +357,27 @@ class AggregateDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
|||||||
|
|
||||||
class AggregateBulkImportView(PermissionRequiredMixin, BulkImportView):
|
class AggregateBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||||
permission_required = 'ipam.add_aggregate'
|
permission_required = 'ipam.add_aggregate'
|
||||||
form = forms.AggregateImportForm
|
model_form = forms.AggregateCSVForm
|
||||||
table = tables.AggregateTable
|
table = tables.AggregateTable
|
||||||
template_name = 'ipam/aggregate_import.html'
|
|
||||||
default_return_url = 'ipam:aggregate_list'
|
default_return_url = 'ipam:aggregate_list'
|
||||||
|
|
||||||
|
|
||||||
class AggregateBulkEditView(PermissionRequiredMixin, BulkEditView):
|
class AggregateBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||||
permission_required = 'ipam.change_aggregate'
|
permission_required = 'ipam.change_aggregate'
|
||||||
cls = Aggregate
|
cls = Aggregate
|
||||||
|
queryset = Aggregate.objects.select_related('rir')
|
||||||
filter = filters.AggregateFilter
|
filter = filters.AggregateFilter
|
||||||
|
table = tables.AggregateTable
|
||||||
form = forms.AggregateBulkEditForm
|
form = forms.AggregateBulkEditForm
|
||||||
template_name = 'ipam/aggregate_bulk_edit.html'
|
|
||||||
default_return_url = 'ipam:aggregate_list'
|
default_return_url = 'ipam:aggregate_list'
|
||||||
|
|
||||||
|
|
||||||
class AggregateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
class AggregateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
permission_required = 'ipam.delete_aggregate'
|
permission_required = 'ipam.delete_aggregate'
|
||||||
cls = Aggregate
|
cls = Aggregate
|
||||||
|
queryset = Aggregate.objects.select_related('rir')
|
||||||
filter = filters.AggregateFilter
|
filter = filters.AggregateFilter
|
||||||
|
table = tables.AggregateTable
|
||||||
default_return_url = 'ipam:aggregate_list'
|
default_return_url = 'ipam:aggregate_list'
|
||||||
|
|
||||||
|
|
||||||
@@ -373,8 +391,8 @@ class RoleListView(ObjectListView):
|
|||||||
template_name = 'ipam/role_list.html'
|
template_name = 'ipam/role_list.html'
|
||||||
|
|
||||||
|
|
||||||
class RoleEditView(PermissionRequiredMixin, ObjectEditView):
|
class RoleCreateView(PermissionRequiredMixin, ObjectEditView):
|
||||||
permission_required = 'ipam.change_role'
|
permission_required = 'ipam.add_role'
|
||||||
model = Role
|
model = Role
|
||||||
form_class = forms.RoleForm
|
form_class = forms.RoleForm
|
||||||
|
|
||||||
@@ -382,9 +400,14 @@ class RoleEditView(PermissionRequiredMixin, ObjectEditView):
|
|||||||
return reverse('ipam:role_list')
|
return reverse('ipam:role_list')
|
||||||
|
|
||||||
|
|
||||||
|
class RoleEditView(RoleCreateView):
|
||||||
|
permission_required = 'ipam.change_role'
|
||||||
|
|
||||||
|
|
||||||
class RoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
class RoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
permission_required = 'ipam.delete_role'
|
permission_required = 'ipam.delete_role'
|
||||||
cls = Role
|
cls = Role
|
||||||
|
table = tables.RoleTable
|
||||||
default_return_url = 'ipam:role_list'
|
default_return_url = 'ipam:role_list'
|
||||||
|
|
||||||
|
|
||||||
@@ -396,7 +419,7 @@ class PrefixListView(ObjectListView):
|
|||||||
queryset = Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role')
|
queryset = Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role')
|
||||||
filter = filters.PrefixFilter
|
filter = filters.PrefixFilter
|
||||||
filter_form = forms.PrefixFilterForm
|
filter_form = forms.PrefixFilterForm
|
||||||
table = tables.PrefixTable
|
table = tables.PrefixDetailTable
|
||||||
template_name = 'ipam/prefix_list.html'
|
template_name = 'ipam/prefix_list.html'
|
||||||
|
|
||||||
def alter_queryset(self, request):
|
def alter_queryset(self, request):
|
||||||
@@ -431,7 +454,7 @@ class PrefixView(View):
|
|||||||
).select_related(
|
).select_related(
|
||||||
'site', 'role'
|
'site', 'role'
|
||||||
).annotate_depth()
|
).annotate_depth()
|
||||||
parent_prefix_table = tables.PrefixBriefTable(parent_prefixes)
|
parent_prefix_table = tables.PrefixTable(list(parent_prefixes), orderable=False)
|
||||||
parent_prefix_table.exclude = ('vrf',)
|
parent_prefix_table.exclude = ('vrf',)
|
||||||
|
|
||||||
# Duplicate prefixes table
|
# Duplicate prefixes table
|
||||||
@@ -442,7 +465,7 @@ class PrefixView(View):
|
|||||||
).select_related(
|
).select_related(
|
||||||
'site', 'role'
|
'site', 'role'
|
||||||
)
|
)
|
||||||
duplicate_prefix_table = tables.PrefixBriefTable(list(duplicate_prefixes))
|
duplicate_prefix_table = tables.PrefixTable(list(duplicate_prefixes), orderable=False)
|
||||||
duplicate_prefix_table.exclude = ('vrf',)
|
duplicate_prefix_table.exclude = ('vrf',)
|
||||||
|
|
||||||
# Child prefixes table
|
# Child prefixes table
|
||||||
@@ -521,14 +544,18 @@ class PrefixIPAddressesView(View):
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
class PrefixEditView(PermissionRequiredMixin, ObjectEditView):
|
class PrefixCreateView(PermissionRequiredMixin, ObjectEditView):
|
||||||
permission_required = 'ipam.change_prefix'
|
permission_required = 'ipam.add_prefix'
|
||||||
model = Prefix
|
model = Prefix
|
||||||
form_class = forms.PrefixForm
|
form_class = forms.PrefixForm
|
||||||
template_name = 'ipam/prefix_edit.html'
|
template_name = 'ipam/prefix_edit.html'
|
||||||
default_return_url = 'ipam:prefix_list'
|
default_return_url = 'ipam:prefix_list'
|
||||||
|
|
||||||
|
|
||||||
|
class PrefixEditView(PrefixCreateView):
|
||||||
|
permission_required = 'ipam.change_prefix'
|
||||||
|
|
||||||
|
|
||||||
class PrefixDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
class PrefixDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||||
permission_required = 'ipam.delete_prefix'
|
permission_required = 'ipam.delete_prefix'
|
||||||
model = Prefix
|
model = Prefix
|
||||||
@@ -538,25 +565,27 @@ class PrefixDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
|||||||
|
|
||||||
class PrefixBulkImportView(PermissionRequiredMixin, BulkImportView):
|
class PrefixBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||||
permission_required = 'ipam.add_prefix'
|
permission_required = 'ipam.add_prefix'
|
||||||
form = forms.PrefixImportForm
|
model_form = forms.PrefixCSVForm
|
||||||
table = tables.PrefixTable
|
table = tables.PrefixTable
|
||||||
template_name = 'ipam/prefix_import.html'
|
|
||||||
default_return_url = 'ipam:prefix_list'
|
default_return_url = 'ipam:prefix_list'
|
||||||
|
|
||||||
|
|
||||||
class PrefixBulkEditView(PermissionRequiredMixin, BulkEditView):
|
class PrefixBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||||
permission_required = 'ipam.change_prefix'
|
permission_required = 'ipam.change_prefix'
|
||||||
cls = Prefix
|
cls = Prefix
|
||||||
|
queryset = Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role')
|
||||||
filter = filters.PrefixFilter
|
filter = filters.PrefixFilter
|
||||||
|
table = tables.PrefixTable
|
||||||
form = forms.PrefixBulkEditForm
|
form = forms.PrefixBulkEditForm
|
||||||
template_name = 'ipam/prefix_bulk_edit.html'
|
|
||||||
default_return_url = 'ipam:prefix_list'
|
default_return_url = 'ipam:prefix_list'
|
||||||
|
|
||||||
|
|
||||||
class PrefixBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
class PrefixBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
permission_required = 'ipam.delete_prefix'
|
permission_required = 'ipam.delete_prefix'
|
||||||
cls = Prefix
|
cls = Prefix
|
||||||
|
queryset = Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role')
|
||||||
filter = filters.PrefixFilter
|
filter = filters.PrefixFilter
|
||||||
|
table = tables.PrefixTable
|
||||||
default_return_url = 'ipam:prefix_list'
|
default_return_url = 'ipam:prefix_list'
|
||||||
|
|
||||||
|
|
||||||
@@ -568,7 +597,7 @@ class IPAddressListView(ObjectListView):
|
|||||||
queryset = IPAddress.objects.select_related('vrf__tenant', 'tenant', 'interface__device', 'nat_inside')
|
queryset = IPAddress.objects.select_related('vrf__tenant', 'tenant', 'interface__device', 'nat_inside')
|
||||||
filter = filters.IPAddressFilter
|
filter = filters.IPAddressFilter
|
||||||
filter_form = forms.IPAddressFilterForm
|
filter_form = forms.IPAddressFilterForm
|
||||||
table = tables.IPAddressTable
|
table = tables.IPAddressDetailTable
|
||||||
template_name = 'ipam/ipaddress_list.html'
|
template_name = 'ipam/ipaddress_list.html'
|
||||||
|
|
||||||
|
|
||||||
@@ -584,7 +613,7 @@ class IPAddressView(View):
|
|||||||
).select_related(
|
).select_related(
|
||||||
'site', 'role'
|
'site', 'role'
|
||||||
)
|
)
|
||||||
parent_prefixes_table = tables.PrefixBriefTable(list(parent_prefixes))
|
parent_prefixes_table = tables.PrefixTable(list(parent_prefixes), orderable=False)
|
||||||
parent_prefixes_table.exclude = ('vrf',)
|
parent_prefixes_table.exclude = ('vrf',)
|
||||||
|
|
||||||
# Duplicate IPs table
|
# Duplicate IPs table
|
||||||
@@ -595,7 +624,7 @@ class IPAddressView(View):
|
|||||||
).select_related(
|
).select_related(
|
||||||
'interface__device', 'nat_inside'
|
'interface__device', 'nat_inside'
|
||||||
)
|
)
|
||||||
duplicate_ips_table = tables.IPAddressBriefTable(list(duplicate_ips))
|
duplicate_ips_table = tables.IPAddressTable(list(duplicate_ips), orderable=False)
|
||||||
|
|
||||||
# Related IP table
|
# Related IP table
|
||||||
related_ips = IPAddress.objects.select_related(
|
related_ips = IPAddress.objects.select_related(
|
||||||
@@ -605,7 +634,7 @@ class IPAddressView(View):
|
|||||||
).filter(
|
).filter(
|
||||||
vrf=ipaddress.vrf, address__net_contained_or_equal=str(ipaddress.address)
|
vrf=ipaddress.vrf, address__net_contained_or_equal=str(ipaddress.address)
|
||||||
)
|
)
|
||||||
related_ips_table = tables.IPAddressBriefTable(list(related_ips))
|
related_ips_table = tables.IPAddressTable(list(related_ips), orderable=False)
|
||||||
|
|
||||||
return render(request, 'ipam/ipaddress.html', {
|
return render(request, 'ipam/ipaddress.html', {
|
||||||
'ipaddress': ipaddress,
|
'ipaddress': ipaddress,
|
||||||
@@ -615,21 +644,25 @@ class IPAddressView(View):
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
class IPAddressEditView(PermissionRequiredMixin, ObjectEditView):
|
class IPAddressCreateView(PermissionRequiredMixin, ObjectEditView):
|
||||||
permission_required = 'ipam.change_ipaddress'
|
permission_required = 'ipam.add_ipaddress'
|
||||||
model = IPAddress
|
model = IPAddress
|
||||||
form_class = forms.IPAddressForm
|
form_class = forms.IPAddressForm
|
||||||
template_name = 'ipam/ipaddress_edit.html'
|
template_name = 'ipam/ipaddress_edit.html'
|
||||||
default_return_url = 'ipam:ipaddress_list'
|
default_return_url = 'ipam:ipaddress_list'
|
||||||
|
|
||||||
|
|
||||||
|
class IPAddressEditView(IPAddressCreateView):
|
||||||
|
permission_required = 'ipam.change_ipaddress'
|
||||||
|
|
||||||
|
|
||||||
class IPAddressDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
class IPAddressDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||||
permission_required = 'ipam.delete_ipaddress'
|
permission_required = 'ipam.delete_ipaddress'
|
||||||
model = IPAddress
|
model = IPAddress
|
||||||
default_return_url = 'ipam:ipaddress_list'
|
default_return_url = 'ipam:ipaddress_list'
|
||||||
|
|
||||||
|
|
||||||
class IPAddressBulkAddView(PermissionRequiredMixin, BulkAddView):
|
class IPAddressBulkCreateView(PermissionRequiredMixin, BulkCreateView):
|
||||||
permission_required = 'ipam.add_ipaddress'
|
permission_required = 'ipam.add_ipaddress'
|
||||||
pattern_form = forms.IPAddressPatternForm
|
pattern_form = forms.IPAddressPatternForm
|
||||||
model_form = forms.IPAddressBulkAddForm
|
model_form = forms.IPAddressBulkAddForm
|
||||||
@@ -640,38 +673,27 @@ class IPAddressBulkAddView(PermissionRequiredMixin, BulkAddView):
|
|||||||
|
|
||||||
class IPAddressBulkImportView(PermissionRequiredMixin, BulkImportView):
|
class IPAddressBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||||
permission_required = 'ipam.add_ipaddress'
|
permission_required = 'ipam.add_ipaddress'
|
||||||
form = forms.IPAddressImportForm
|
model_form = forms.IPAddressCSVForm
|
||||||
table = tables.IPAddressTable
|
table = tables.IPAddressTable
|
||||||
template_name = 'ipam/ipaddress_import.html'
|
|
||||||
default_return_url = 'ipam:ipaddress_list'
|
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):
|
class IPAddressBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||||
permission_required = 'ipam.change_ipaddress'
|
permission_required = 'ipam.change_ipaddress'
|
||||||
cls = IPAddress
|
cls = IPAddress
|
||||||
|
queryset = IPAddress.objects.select_related('vrf__tenant', 'tenant', 'interface__device')
|
||||||
filter = filters.IPAddressFilter
|
filter = filters.IPAddressFilter
|
||||||
|
table = tables.IPAddressTable
|
||||||
form = forms.IPAddressBulkEditForm
|
form = forms.IPAddressBulkEditForm
|
||||||
template_name = 'ipam/ipaddress_bulk_edit.html'
|
|
||||||
default_return_url = 'ipam:ipaddress_list'
|
default_return_url = 'ipam:ipaddress_list'
|
||||||
|
|
||||||
|
|
||||||
class IPAddressBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
class IPAddressBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
permission_required = 'ipam.delete_ipaddress'
|
permission_required = 'ipam.delete_ipaddress'
|
||||||
cls = IPAddress
|
cls = IPAddress
|
||||||
|
queryset = IPAddress.objects.select_related('vrf__tenant', 'tenant', 'interface__device')
|
||||||
filter = filters.IPAddressFilter
|
filter = filters.IPAddressFilter
|
||||||
|
table = tables.IPAddressTable
|
||||||
default_return_url = 'ipam:ipaddress_list'
|
default_return_url = 'ipam:ipaddress_list'
|
||||||
|
|
||||||
|
|
||||||
@@ -687,8 +709,8 @@ class VLANGroupListView(ObjectListView):
|
|||||||
template_name = 'ipam/vlangroup_list.html'
|
template_name = 'ipam/vlangroup_list.html'
|
||||||
|
|
||||||
|
|
||||||
class VLANGroupEditView(PermissionRequiredMixin, ObjectEditView):
|
class VLANGroupCreateView(PermissionRequiredMixin, ObjectEditView):
|
||||||
permission_required = 'ipam.change_vlangroup'
|
permission_required = 'ipam.add_vlangroup'
|
||||||
model = VLANGroup
|
model = VLANGroup
|
||||||
form_class = forms.VLANGroupForm
|
form_class = forms.VLANGroupForm
|
||||||
|
|
||||||
@@ -696,10 +718,16 @@ class VLANGroupEditView(PermissionRequiredMixin, ObjectEditView):
|
|||||||
return reverse('ipam:vlangroup_list')
|
return reverse('ipam:vlangroup_list')
|
||||||
|
|
||||||
|
|
||||||
|
class VLANGroupEditView(VLANGroupCreateView):
|
||||||
|
permission_required = 'ipam.change_vlangroup'
|
||||||
|
|
||||||
|
|
||||||
class VLANGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
class VLANGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
permission_required = 'ipam.delete_vlangroup'
|
permission_required = 'ipam.delete_vlangroup'
|
||||||
cls = VLANGroup
|
cls = VLANGroup
|
||||||
|
queryset = VLANGroup.objects.select_related('site').annotate(vlan_count=Count('vlans'))
|
||||||
filter = filters.VLANGroupFilter
|
filter = filters.VLANGroupFilter
|
||||||
|
table = tables.VLANGroupTable
|
||||||
default_return_url = 'ipam:vlangroup_list'
|
default_return_url = 'ipam:vlangroup_list'
|
||||||
|
|
||||||
|
|
||||||
@@ -711,7 +739,7 @@ class VLANListView(ObjectListView):
|
|||||||
queryset = VLAN.objects.select_related('site', 'group', 'tenant', 'role').prefetch_related('prefixes')
|
queryset = VLAN.objects.select_related('site', 'group', 'tenant', 'role').prefetch_related('prefixes')
|
||||||
filter = filters.VLANFilter
|
filter = filters.VLANFilter
|
||||||
filter_form = forms.VLANFilterForm
|
filter_form = forms.VLANFilterForm
|
||||||
table = tables.VLANTable
|
table = tables.VLANDetailTable
|
||||||
template_name = 'ipam/vlan_list.html'
|
template_name = 'ipam/vlan_list.html'
|
||||||
|
|
||||||
|
|
||||||
@@ -723,7 +751,7 @@ class VLANView(View):
|
|||||||
'site__region', 'tenant__group', 'role'
|
'site__region', 'tenant__group', 'role'
|
||||||
), pk=pk)
|
), pk=pk)
|
||||||
prefixes = Prefix.objects.filter(vlan=vlan).select_related('vrf', 'site', 'role')
|
prefixes = Prefix.objects.filter(vlan=vlan).select_related('vrf', 'site', 'role')
|
||||||
prefix_table = tables.PrefixBriefTable(list(prefixes))
|
prefix_table = tables.PrefixTable(list(prefixes), orderable=False)
|
||||||
prefix_table.exclude = ('vlan',)
|
prefix_table.exclude = ('vlan',)
|
||||||
|
|
||||||
return render(request, 'ipam/vlan.html', {
|
return render(request, 'ipam/vlan.html', {
|
||||||
@@ -732,14 +760,18 @@ class VLANView(View):
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
class VLANEditView(PermissionRequiredMixin, ObjectEditView):
|
class VLANCreateView(PermissionRequiredMixin, ObjectEditView):
|
||||||
permission_required = 'ipam.change_vlan'
|
permission_required = 'ipam.add_vlan'
|
||||||
model = VLAN
|
model = VLAN
|
||||||
form_class = forms.VLANForm
|
form_class = forms.VLANForm
|
||||||
template_name = 'ipam/vlan_edit.html'
|
template_name = 'ipam/vlan_edit.html'
|
||||||
default_return_url = 'ipam:vlan_list'
|
default_return_url = 'ipam:vlan_list'
|
||||||
|
|
||||||
|
|
||||||
|
class VLANEditView(VLANCreateView):
|
||||||
|
permission_required = 'ipam.change_vlan'
|
||||||
|
|
||||||
|
|
||||||
class VLANDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
class VLANDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||||
permission_required = 'ipam.delete_vlan'
|
permission_required = 'ipam.delete_vlan'
|
||||||
model = VLAN
|
model = VLAN
|
||||||
@@ -748,25 +780,27 @@ class VLANDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
|||||||
|
|
||||||
class VLANBulkImportView(PermissionRequiredMixin, BulkImportView):
|
class VLANBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||||
permission_required = 'ipam.add_vlan'
|
permission_required = 'ipam.add_vlan'
|
||||||
form = forms.VLANImportForm
|
model_form = forms.VLANCSVForm
|
||||||
table = tables.VLANTable
|
table = tables.VLANTable
|
||||||
template_name = 'ipam/vlan_import.html'
|
|
||||||
default_return_url = 'ipam:vlan_list'
|
default_return_url = 'ipam:vlan_list'
|
||||||
|
|
||||||
|
|
||||||
class VLANBulkEditView(PermissionRequiredMixin, BulkEditView):
|
class VLANBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||||
permission_required = 'ipam.change_vlan'
|
permission_required = 'ipam.change_vlan'
|
||||||
cls = VLAN
|
cls = VLAN
|
||||||
|
queryset = VLAN.objects.select_related('site', 'group', 'tenant', 'role')
|
||||||
filter = filters.VLANFilter
|
filter = filters.VLANFilter
|
||||||
|
table = tables.VLANTable
|
||||||
form = forms.VLANBulkEditForm
|
form = forms.VLANBulkEditForm
|
||||||
template_name = 'ipam/vlan_bulk_edit.html'
|
|
||||||
default_return_url = 'ipam:vlan_list'
|
default_return_url = 'ipam:vlan_list'
|
||||||
|
|
||||||
|
|
||||||
class VLANBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
class VLANBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
permission_required = 'ipam.delete_vlan'
|
permission_required = 'ipam.delete_vlan'
|
||||||
cls = VLAN
|
cls = VLAN
|
||||||
|
queryset = VLAN.objects.select_related('site', 'group', 'tenant', 'role')
|
||||||
filter = filters.VLANFilter
|
filter = filters.VLANFilter
|
||||||
|
table = tables.VLANTable
|
||||||
default_return_url = 'ipam:vlan_list'
|
default_return_url = 'ipam:vlan_list'
|
||||||
|
|
||||||
|
|
||||||
@@ -774,8 +808,8 @@ class VLANBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
|||||||
# Services
|
# Services
|
||||||
#
|
#
|
||||||
|
|
||||||
class ServiceEditView(PermissionRequiredMixin, ObjectEditView):
|
class ServiceCreateView(PermissionRequiredMixin, ObjectEditView):
|
||||||
permission_required = 'ipam.change_service'
|
permission_required = 'ipam.add_service'
|
||||||
model = Service
|
model = Service
|
||||||
form_class = forms.ServiceForm
|
form_class = forms.ServiceForm
|
||||||
template_name = 'ipam/service_edit.html'
|
template_name = 'ipam/service_edit.html'
|
||||||
@@ -789,6 +823,10 @@ class ServiceEditView(PermissionRequiredMixin, ObjectEditView):
|
|||||||
return obj.device.get_absolute_url()
|
return obj.device.get_absolute_url()
|
||||||
|
|
||||||
|
|
||||||
|
class ServiceEditView(ServiceCreateView):
|
||||||
|
permission_required = 'ipam.change_service'
|
||||||
|
|
||||||
|
|
||||||
class ServiceDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
class ServiceDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||||
permission_required = 'ipam.delete_service'
|
permission_required = 'ipam.delete_service'
|
||||||
model = Service
|
model = Service
|
||||||
|
|||||||
@@ -58,6 +58,11 @@ CORS_ORIGIN_REGEX_WHITELIST = [
|
|||||||
# r'^(https?://)?(\w+\.)?example\.com$',
|
# 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 settings
|
||||||
EMAIL = {
|
EMAIL = {
|
||||||
'SERVER': 'localhost',
|
'SERVER': 'localhost',
|
||||||
@@ -72,6 +77,10 @@ EMAIL = {
|
|||||||
# (all prefixes and IP addresses not assigned to a VRF), set ENFORCE_GLOBAL_UNIQUE to True.
|
# (all prefixes and IP addresses not assigned to a VRF), set ENFORCE_GLOBAL_UNIQUE to True.
|
||||||
ENFORCE_GLOBAL_UNIQUE = False
|
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
|
# 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.
|
# are permitted to access most data in NetBox (excluding secrets) but not make any changes.
|
||||||
LOGIN_REQUIRED = False
|
LOGIN_REQUIRED = False
|
||||||
@@ -79,6 +88,11 @@ LOGIN_REQUIRED = False
|
|||||||
# Setting this to True will display a "maintenance mode" banner at the top of every page.
|
# Setting this to True will display a "maintenance mode" banner at the top of every page.
|
||||||
MAINTENANCE_MODE = False
|
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).
|
# Credentials that NetBox will use to access live devices (future use).
|
||||||
NETBOX_USERNAME = ''
|
NETBOX_USERNAME = ''
|
||||||
NETBOX_PASSWORD = ''
|
NETBOX_PASSWORD = ''
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ except ImportError:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
VERSION = '2.0.4'
|
VERSION = '2.1.0'
|
||||||
|
|
||||||
# Import local configuration
|
# Import required configuration parameters
|
||||||
ALLOWED_HOSTS = DATABASE = SECRET_KEY = None
|
ALLOWED_HOSTS = DATABASE = SECRET_KEY = None
|
||||||
for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']:
|
for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']:
|
||||||
try:
|
try:
|
||||||
@@ -25,32 +25,35 @@ for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']:
|
|||||||
"Mandatory setting {} is missing from configuration.py.".format(setting)
|
"Mandatory setting {} is missing from configuration.py.".format(setting)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Default configurations
|
# Import optional configuration parameters
|
||||||
ADMINS = getattr(configuration, 'ADMINS', [])
|
ADMINS = getattr(configuration, 'ADMINS', [])
|
||||||
DEBUG = getattr(configuration, 'DEBUG', False)
|
BANNER_BOTTOM = getattr(configuration, 'BANNER_BOTTOM', False)
|
||||||
EMAIL = getattr(configuration, 'EMAIL', {})
|
BANNER_TOP = getattr(configuration, 'BANNER_TOP', False)
|
||||||
LOGIN_REQUIRED = getattr(configuration, 'LOGIN_REQUIRED', False)
|
|
||||||
BASE_PATH = getattr(configuration, 'BASE_PATH', '')
|
BASE_PATH = getattr(configuration, 'BASE_PATH', '')
|
||||||
if BASE_PATH:
|
if BASE_PATH:
|
||||||
BASE_PATH = BASE_PATH.strip('/') + '/' # Enforce trailing slash only
|
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)
|
MAINTENANCE_MODE = getattr(configuration, 'MAINTENANCE_MODE', False)
|
||||||
|
MAX_PAGE_SIZE = getattr(configuration, 'MAX_PAGE_SIZE', 1000)
|
||||||
PAGINATE_COUNT = getattr(configuration, 'PAGINATE_COUNT', 50)
|
PAGINATE_COUNT = getattr(configuration, 'PAGINATE_COUNT', 50)
|
||||||
|
PREFER_IPV4 = getattr(configuration, 'PREFER_IPV4', False)
|
||||||
NETBOX_USERNAME = getattr(configuration, 'NETBOX_USERNAME', '')
|
NETBOX_USERNAME = getattr(configuration, 'NETBOX_USERNAME', '')
|
||||||
NETBOX_PASSWORD = getattr(configuration, 'NETBOX_PASSWORD', '')
|
NETBOX_PASSWORD = getattr(configuration, 'NETBOX_PASSWORD', '')
|
||||||
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')
|
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')
|
SHORT_DATETIME_FORMAT = getattr(configuration, 'SHORT_DATETIME_FORMAT', 'Y-m-d H:i')
|
||||||
BANNER_TOP = getattr(configuration, 'BANNER_TOP', False)
|
SHORT_TIME_FORMAT = getattr(configuration, 'SHORT_TIME_FORMAT', 'H:i:s')
|
||||||
BANNER_BOTTOM = getattr(configuration, 'BANNER_BOTTOM', False)
|
TIME_FORMAT = getattr(configuration, 'TIME_FORMAT', 'g:i a')
|
||||||
PREFER_IPV4 = getattr(configuration, 'PREFER_IPV4', False)
|
TIME_ZONE = getattr(configuration, 'TIME_ZONE', 'UTC')
|
||||||
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
|
CSRF_TRUSTED_ORIGINS = ALLOWED_HOSTS
|
||||||
|
|
||||||
# Attempt to import LDAP configuration if it has been defined
|
# Attempt to import LDAP configuration if it has been defined
|
||||||
@@ -208,7 +211,7 @@ REST_FRAMEWORK = {
|
|||||||
'DEFAULT_FILTER_BACKENDS': (
|
'DEFAULT_FILTER_BACKENDS': (
|
||||||
'rest_framework.filters.DjangoFilterBackend',
|
'rest_framework.filters.DjangoFilterBackend',
|
||||||
),
|
),
|
||||||
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
|
'DEFAULT_PAGINATION_CLASS': 'utilities.api.OptionalLimitOffsetPagination',
|
||||||
'DEFAULT_PERMISSION_CLASSES': (
|
'DEFAULT_PERMISSION_CLASSES': (
|
||||||
'utilities.api.TokenPermissions',
|
'utilities.api.TokenPermissions',
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -11,20 +11,20 @@ from django.views.generic import View
|
|||||||
|
|
||||||
from circuits.filters import CircuitFilter, ProviderFilter
|
from circuits.filters import CircuitFilter, ProviderFilter
|
||||||
from circuits.models import Circuit, Provider
|
from circuits.models import Circuit, Provider
|
||||||
from circuits.tables import CircuitSearchTable, ProviderSearchTable
|
from circuits.tables import CircuitTable, ProviderTable
|
||||||
from dcim.filters import DeviceFilter, DeviceTypeFilter, RackFilter, SiteFilter
|
from dcim.filters import DeviceFilter, DeviceTypeFilter, RackFilter, SiteFilter
|
||||||
from dcim.models import ConsolePort, Device, DeviceType, InterfaceConnection, PowerPort, Rack, Site
|
from dcim.models import ConsolePort, Device, DeviceType, InterfaceConnection, PowerPort, Rack, Site
|
||||||
from dcim.tables import DeviceSearchTable, DeviceTypeSearchTable, RackSearchTable, SiteSearchTable
|
from dcim.tables import DeviceTable, DeviceTypeTable, RackTable, SiteTable
|
||||||
from extras.models import TopologyMap, UserAction
|
from extras.models import TopologyMap, UserAction
|
||||||
from ipam.filters import AggregateFilter, IPAddressFilter, PrefixFilter, VLANFilter, VRFFilter
|
from ipam.filters import AggregateFilter, IPAddressFilter, PrefixFilter, VLANFilter, VRFFilter
|
||||||
from ipam.models import Aggregate, IPAddress, Prefix, VLAN, VRF
|
from ipam.models import Aggregate, IPAddress, Prefix, VLAN, VRF
|
||||||
from ipam.tables import AggregateSearchTable, IPAddressSearchTable, PrefixSearchTable, VLANSearchTable, VRFSearchTable
|
from ipam.tables import AggregateTable, IPAddressTable, PrefixTable, VLANTable, VRFTable
|
||||||
from secrets.filters import SecretFilter
|
from secrets.filters import SecretFilter
|
||||||
from secrets.models import Secret
|
from secrets.models import Secret
|
||||||
from secrets.tables import SecretSearchTable
|
from secrets.tables import SecretTable
|
||||||
from tenancy.filters import TenantFilter
|
from tenancy.filters import TenantFilter
|
||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
from tenancy.tables import TenantSearchTable
|
from tenancy.tables import TenantTable
|
||||||
from .forms import SearchForm
|
from .forms import SearchForm
|
||||||
|
|
||||||
|
|
||||||
@@ -34,83 +34,85 @@ SEARCH_TYPES = OrderedDict((
|
|||||||
('provider', {
|
('provider', {
|
||||||
'queryset': Provider.objects.all(),
|
'queryset': Provider.objects.all(),
|
||||||
'filter': ProviderFilter,
|
'filter': ProviderFilter,
|
||||||
'table': ProviderSearchTable,
|
'table': ProviderTable,
|
||||||
'url': 'circuits:provider_list',
|
'url': 'circuits:provider_list',
|
||||||
}),
|
}),
|
||||||
('circuit', {
|
('circuit', {
|
||||||
'queryset': Circuit.objects.select_related('type', 'provider', 'tenant').prefetch_related('terminations__site'),
|
'queryset': Circuit.objects.select_related('type', 'provider', 'tenant').prefetch_related('terminations__site'),
|
||||||
'filter': CircuitFilter,
|
'filter': CircuitFilter,
|
||||||
'table': CircuitSearchTable,
|
'table': CircuitTable,
|
||||||
'url': 'circuits:circuit_list',
|
'url': 'circuits:circuit_list',
|
||||||
}),
|
}),
|
||||||
# DCIM
|
# DCIM
|
||||||
('site', {
|
('site', {
|
||||||
'queryset': Site.objects.select_related('region', 'tenant'),
|
'queryset': Site.objects.select_related('region', 'tenant'),
|
||||||
'filter': SiteFilter,
|
'filter': SiteFilter,
|
||||||
'table': SiteSearchTable,
|
'table': SiteTable,
|
||||||
'url': 'dcim:site_list',
|
'url': 'dcim:site_list',
|
||||||
}),
|
}),
|
||||||
('rack', {
|
('rack', {
|
||||||
'queryset': Rack.objects.select_related('site', 'group', 'tenant', 'role'),
|
'queryset': Rack.objects.select_related('site', 'group', 'tenant', 'role'),
|
||||||
'filter': RackFilter,
|
'filter': RackFilter,
|
||||||
'table': RackSearchTable,
|
'table': RackTable,
|
||||||
'url': 'dcim:rack_list',
|
'url': 'dcim:rack_list',
|
||||||
}),
|
}),
|
||||||
('devicetype', {
|
('devicetype', {
|
||||||
'queryset': DeviceType.objects.select_related('manufacturer'),
|
'queryset': DeviceType.objects.select_related('manufacturer'),
|
||||||
'filter': DeviceTypeFilter,
|
'filter': DeviceTypeFilter,
|
||||||
'table': DeviceTypeSearchTable,
|
'table': DeviceTypeTable,
|
||||||
'url': 'dcim:devicetype_list',
|
'url': 'dcim:devicetype_list',
|
||||||
}),
|
}),
|
||||||
('device', {
|
('device', {
|
||||||
'queryset': Device.objects.select_related('device_type__manufacturer', 'device_role', 'tenant', 'site', 'rack'),
|
'queryset': Device.objects.select_related(
|
||||||
|
'device_type__manufacturer', 'device_role', 'tenant', 'site', 'rack'
|
||||||
|
),
|
||||||
'filter': DeviceFilter,
|
'filter': DeviceFilter,
|
||||||
'table': DeviceSearchTable,
|
'table': DeviceTable,
|
||||||
'url': 'dcim:device_list',
|
'url': 'dcim:device_list',
|
||||||
}),
|
}),
|
||||||
# IPAM
|
# IPAM
|
||||||
('vrf', {
|
('vrf', {
|
||||||
'queryset': VRF.objects.select_related('tenant'),
|
'queryset': VRF.objects.select_related('tenant'),
|
||||||
'filter': VRFFilter,
|
'filter': VRFFilter,
|
||||||
'table': VRFSearchTable,
|
'table': VRFTable,
|
||||||
'url': 'ipam:vrf_list',
|
'url': 'ipam:vrf_list',
|
||||||
}),
|
}),
|
||||||
('aggregate', {
|
('aggregate', {
|
||||||
'queryset': Aggregate.objects.select_related('rir'),
|
'queryset': Aggregate.objects.select_related('rir'),
|
||||||
'filter': AggregateFilter,
|
'filter': AggregateFilter,
|
||||||
'table': AggregateSearchTable,
|
'table': AggregateTable,
|
||||||
'url': 'ipam:aggregate_list',
|
'url': 'ipam:aggregate_list',
|
||||||
}),
|
}),
|
||||||
('prefix', {
|
('prefix', {
|
||||||
'queryset': Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role'),
|
'queryset': Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role'),
|
||||||
'filter': PrefixFilter,
|
'filter': PrefixFilter,
|
||||||
'table': PrefixSearchTable,
|
'table': PrefixTable,
|
||||||
'url': 'ipam:prefix_list',
|
'url': 'ipam:prefix_list',
|
||||||
}),
|
}),
|
||||||
('ipaddress', {
|
('ipaddress', {
|
||||||
'queryset': IPAddress.objects.select_related('vrf__tenant', 'tenant', 'interface__device'),
|
'queryset': IPAddress.objects.select_related('vrf__tenant', 'tenant', 'interface__device'),
|
||||||
'filter': IPAddressFilter,
|
'filter': IPAddressFilter,
|
||||||
'table': IPAddressSearchTable,
|
'table': IPAddressTable,
|
||||||
'url': 'ipam:ipaddress_list',
|
'url': 'ipam:ipaddress_list',
|
||||||
}),
|
}),
|
||||||
('vlan', {
|
('vlan', {
|
||||||
'queryset': VLAN.objects.select_related('site', 'group', 'tenant', 'role'),
|
'queryset': VLAN.objects.select_related('site', 'group', 'tenant', 'role'),
|
||||||
'filter': VLANFilter,
|
'filter': VLANFilter,
|
||||||
'table': VLANSearchTable,
|
'table': VLANTable,
|
||||||
'url': 'ipam:vlan_list',
|
'url': 'ipam:vlan_list',
|
||||||
}),
|
}),
|
||||||
# Secrets
|
# Secrets
|
||||||
('secret', {
|
('secret', {
|
||||||
'queryset': Secret.objects.select_related('role', 'device'),
|
'queryset': Secret.objects.select_related('role', 'device'),
|
||||||
'filter': SecretFilter,
|
'filter': SecretFilter,
|
||||||
'table': SecretSearchTable,
|
'table': SecretTable,
|
||||||
'url': 'secrets:secret_list',
|
'url': 'secrets:secret_list',
|
||||||
}),
|
}),
|
||||||
# Tenancy
|
# Tenancy
|
||||||
('tenant', {
|
('tenant', {
|
||||||
'queryset': Tenant.objects.select_related('group'),
|
'queryset': Tenant.objects.select_related('group'),
|
||||||
'filter': TenantFilter,
|
'filter': TenantFilter,
|
||||||
'table': TenantSearchTable,
|
'table': TenantTable,
|
||||||
'url': 'tenancy:tenant_list',
|
'url': 'tenancy:tenant_list',
|
||||||
}),
|
}),
|
||||||
))
|
))
|
||||||
@@ -189,7 +191,7 @@ class SearchView(View):
|
|||||||
|
|
||||||
# Construct the results table for this object type
|
# Construct the results table for this object type
|
||||||
filtered_queryset = filter_cls({'q': form.cleaned_data['q']}, queryset=queryset).qs
|
filtered_queryset = filter_cls({'q': form.cleaned_data['q']}, queryset=queryset).qs
|
||||||
table = table(filtered_queryset)
|
table = table(filtered_queryset, orderable=False)
|
||||||
table.paginate(per_page=SEARCH_MAX_RESULTS)
|
table.paginate(per_page=SEARCH_MAX_RESULTS)
|
||||||
|
|
||||||
if table.page:
|
if table.page:
|
||||||
|
|||||||
@@ -333,6 +333,31 @@ table.component-list tr.ipaddress:hover td {
|
|||||||
background-color: #e6f7f7;
|
background-color: #e6f7f7;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* AJAX loader */
|
||||||
|
.loading {
|
||||||
|
position: fixed;
|
||||||
|
display: none;
|
||||||
|
z-index: 999;
|
||||||
|
height: 2em;
|
||||||
|
width: 2em;
|
||||||
|
overflow: show;
|
||||||
|
margin: auto;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
.loading:before {
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(0,0,0,0.3);
|
||||||
|
}
|
||||||
|
|
||||||
/* Misc */
|
/* Misc */
|
||||||
.banner-bottom {
|
.banner-bottom {
|
||||||
margin-bottom: 50px;
|
margin-bottom: 50px;
|
||||||
|
|||||||
BIN
netbox/project-static/img/ajax-loader.gif
Normal file
BIN
netbox/project-static/img/ajax-loader.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.5 KiB |
File diff suppressed because one or more lines are too long
@@ -1,6 +1,7 @@
|
|||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
var search_field = $('#id_livesearch');
|
var search_field = $('#id_livesearch');
|
||||||
var real_field = $('#id_' + search_field.attr('data-field'));
|
var real_field = $('#id_' + search_field.attr('data-field'));
|
||||||
|
var select_fields = $('#select select');
|
||||||
var search_key = search_field.attr('data-key');
|
var search_key = search_field.attr('data-key');
|
||||||
var label = search_field.attr('data-label');
|
var label = search_field.attr('data-label');
|
||||||
if (!label) {
|
if (!label) {
|
||||||
@@ -40,13 +41,22 @@ $(document).ready(function() {
|
|||||||
select: function(event, ui) {
|
select: function(event, ui) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
search_field.val(ui.item.label);
|
search_field.val(ui.item.label);
|
||||||
|
select_fields.val('');
|
||||||
|
select_fields.attr('disabled', 'disabled');
|
||||||
real_field.empty();
|
real_field.empty();
|
||||||
real_field.append($("<option></option>").attr('value', ui.item.value).text(ui.item.label));
|
real_field.append($("<option></option>").attr('value', ui.item.value).text(ui.item.label));
|
||||||
real_field.change();
|
real_field.change();
|
||||||
// If the field has a parent helper, reset the parent to no selection
|
// Disable parent selection fields
|
||||||
$('select[filter-for="' + real_field.attr('name') + '"]').val('');
|
// $('select[filter-for="' + real_field.attr('name') + '"]').val('');
|
||||||
},
|
},
|
||||||
minLength: 4,
|
minLength: 4,
|
||||||
delay: 500
|
delay: 500
|
||||||
});
|
});
|
||||||
|
|
||||||
|
search_field.change(function() {
|
||||||
|
if (!search_field.val()) {
|
||||||
|
select_fields.removeAttr('disabled');
|
||||||
|
select_fields.val('');
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -42,13 +42,15 @@ class UserKeyAdmin(admin.ModelAdmin):
|
|||||||
if 'activate' in request.POST:
|
if 'activate' in request.POST:
|
||||||
form = ActivateUserKeyForm(request.POST)
|
form = ActivateUserKeyForm(request.POST)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
try:
|
|
||||||
master_key = my_userkey.get_master_key(form.cleaned_data['secret_key'])
|
master_key = my_userkey.get_master_key(form.cleaned_data['secret_key'])
|
||||||
|
if master_key is not None:
|
||||||
for uk in form.cleaned_data['_selected_action']:
|
for uk in form.cleaned_data['_selected_action']:
|
||||||
uk.activate(master_key)
|
uk.activate(master_key)
|
||||||
return redirect('admin:secrets_userkey_changelist')
|
return redirect('admin:secrets_userkey_changelist')
|
||||||
except ValueError:
|
else:
|
||||||
messages.error(request, "Invalid private key provided. Unable to retrieve master key.")
|
messages.error(
|
||||||
|
request, "Invalid private key provided. Unable to retrieve master key.", extra_tags='error'
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
form = ActivateUserKeyForm(initial={'_selected_action': request.POST.getlist(admin.ACTION_CHECKBOX_NAME)})
|
form = ActivateUserKeyForm(initial={'_selected_action': request.POST.getlist(admin.ACTION_CHECKBOX_NAME)})
|
||||||
|
|
||||||
|
|||||||
@@ -5,13 +5,14 @@ from rest_framework.validators import UniqueTogetherValidator
|
|||||||
|
|
||||||
from dcim.api.serializers import NestedDeviceSerializer
|
from dcim.api.serializers import NestedDeviceSerializer
|
||||||
from secrets.models import Secret, SecretRole
|
from secrets.models import Secret, SecretRole
|
||||||
|
from utilities.api import ModelValidationMixin
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# SecretRoles
|
# SecretRoles
|
||||||
#
|
#
|
||||||
|
|
||||||
class SecretRoleSerializer(serializers.ModelSerializer):
|
class SecretRoleSerializer(ModelValidationMixin, serializers.ModelSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = SecretRole
|
model = SecretRole
|
||||||
@@ -55,4 +56,7 @@ class WritableSecretSerializer(serializers.ModelSerializer):
|
|||||||
validator.set_context(self)
|
validator.set_context(self)
|
||||||
validator(data)
|
validator(data)
|
||||||
|
|
||||||
|
# Enforce model validation
|
||||||
|
super(WritableSecretSerializer, self).validate(data)
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ class SecretRoleViewSet(ModelViewSet):
|
|||||||
queryset = SecretRole.objects.all()
|
queryset = SecretRole.objects.all()
|
||||||
serializer_class = serializers.SecretRoleSerializer
|
serializer_class = serializers.SecretRoleSerializer
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated]
|
||||||
|
filter_class = filters.SecretRoleFilter
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -9,6 +9,13 @@ from dcim.models import Device
|
|||||||
from utilities.filters import NumericInFilter
|
from utilities.filters import NumericInFilter
|
||||||
|
|
||||||
|
|
||||||
|
class SecretRoleFilter(django_filters.FilterSet):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = SecretRole
|
||||||
|
fields = ['name', 'slug']
|
||||||
|
|
||||||
|
|
||||||
class SecretFilter(django_filters.FilterSet):
|
class SecretFilter(django_filters.FilterSet):
|
||||||
id__in = NumericInFilter(name='id', lookup_expr='in')
|
id__in = NumericInFilter(name='id', lookup_expr='in')
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
@@ -16,7 +23,6 @@ class SecretFilter(django_filters.FilterSet):
|
|||||||
label='Search',
|
label='Search',
|
||||||
)
|
)
|
||||||
role_id = django_filters.ModelMultipleChoiceFilter(
|
role_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
name='role',
|
|
||||||
queryset=SecretRole.objects.all(),
|
queryset=SecretRole.objects.all(),
|
||||||
label='Role (ID)',
|
label='Role (ID)',
|
||||||
)
|
)
|
||||||
@@ -27,12 +33,11 @@ class SecretFilter(django_filters.FilterSet):
|
|||||||
label='Role (slug)',
|
label='Role (slug)',
|
||||||
)
|
)
|
||||||
device_id = django_filters.ModelMultipleChoiceFilter(
|
device_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
name='device',
|
|
||||||
queryset=Device.objects.all(),
|
queryset=Device.objects.all(),
|
||||||
label='Device (ID)',
|
label='Device (ID)',
|
||||||
)
|
)
|
||||||
device = django_filters.ModelMultipleChoiceFilter(
|
device = django_filters.ModelMultipleChoiceFilter(
|
||||||
name='device',
|
name='device__name',
|
||||||
queryset=Device.objects.all(),
|
queryset=Device.objects.all(),
|
||||||
to_field_name='name',
|
to_field_name='name',
|
||||||
label='Device (name)',
|
label='Device (name)',
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from django import forms
|
|||||||
from django.db.models import Count
|
from django.db.models import Count
|
||||||
|
|
||||||
from dcim.models import Device
|
from dcim.models import Device
|
||||||
from utilities.forms import BootstrapMixin, BulkEditForm, BulkImportForm, CSVDataField, FilterChoiceField, SlugField
|
from utilities.forms import BootstrapMixin, BulkEditForm, FilterChoiceField, FlexibleModelChoiceField, SlugField
|
||||||
from .models import Secret, SecretRole, UserKey
|
from .models import Secret, SecretRole, UserKey
|
||||||
|
|
||||||
|
|
||||||
@@ -40,7 +40,7 @@ class SecretRoleForm(BootstrapMixin, forms.ModelForm):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = SecretRole
|
model = SecretRole
|
||||||
fields = ['name', 'slug']
|
fields = ['name', 'slug', 'users', 'groups']
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -65,27 +65,40 @@ class SecretForm(BootstrapMixin, forms.ModelForm):
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
class SecretFromCSVForm(forms.ModelForm):
|
class SecretCSVForm(forms.ModelForm):
|
||||||
device = forms.ModelChoiceField(queryset=Device.objects.all(), required=False, to_field_name='name',
|
device = FlexibleModelChoiceField(
|
||||||
error_messages={'invalid_choice': 'Device not found.'})
|
queryset=Device.objects.all(),
|
||||||
role = forms.ModelChoiceField(queryset=SecretRole.objects.all(), to_field_name='name',
|
to_field_name='name',
|
||||||
error_messages={'invalid_choice': 'Invalid secret role.'})
|
help_text='Device name or ID',
|
||||||
plaintext = forms.CharField()
|
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 Meta:
|
class Meta:
|
||||||
model = Secret
|
model = Secret
|
||||||
fields = ['device', 'role', 'name', 'plaintext']
|
fields = ['device', 'role', 'name', 'plaintext']
|
||||||
|
help_texts = {
|
||||||
|
'name': 'Name or username',
|
||||||
|
}
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
s = super(SecretFromCSVForm, self).save(*args, **kwargs)
|
s = super(SecretCSVForm, self).save(*args, **kwargs)
|
||||||
s.plaintext = str(self.cleaned_data['plaintext'])
|
s.plaintext = str(self.cleaned_data['plaintext'])
|
||||||
return s
|
return s
|
||||||
|
|
||||||
|
|
||||||
class SecretImportForm(BootstrapMixin, BulkImportForm):
|
|
||||||
csv = CSVDataField(csv_form=SecretFromCSVForm, widget=forms.Textarea(attrs={'class': 'requires-session-key'}))
|
|
||||||
|
|
||||||
|
|
||||||
class SecretBulkEditForm(BootstrapMixin, BulkEditForm):
|
class SecretBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||||
pk = forms.ModelMultipleChoiceField(queryset=Secret.objects.all(), widget=forms.MultipleHiddenInput)
|
pk = forms.ModelMultipleChoiceField(queryset=Secret.objects.all(), widget=forms.MultipleHiddenInput)
|
||||||
role = forms.ModelChoiceField(queryset=SecretRole.objects.all(), required=False)
|
role = forms.ModelChoiceField(queryset=SecretRole.objects.all(), required=False)
|
||||||
|
|||||||
@@ -291,6 +291,7 @@ class Secret(CreatedUpdatedModel):
|
|||||||
hash = models.CharField(max_length=128, editable=False)
|
hash = models.CharField(max_length=128, editable=False)
|
||||||
|
|
||||||
plaintext = None
|
plaintext = None
|
||||||
|
csv_headers = ['device', 'role', 'name', 'plaintext']
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['device', 'role', 'name']
|
ordering = ['device', 'role', 'name']
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
import django_tables2 as tables
|
import django_tables2 as tables
|
||||||
|
|
||||||
from utilities.tables import BaseTable, SearchTable, ToggleColumn
|
from utilities.tables import BaseTable, ToggleColumn
|
||||||
|
|
||||||
from .models import SecretRole, Secret
|
from .models import SecretRole, Secret
|
||||||
|
|
||||||
@@ -43,11 +43,3 @@ class SecretTable(BaseTable):
|
|||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = Secret
|
model = Secret
|
||||||
fields = ('pk', 'device', 'role', 'name', 'last_updated')
|
fields = ('pk', 'device', 'role', 'name', 'last_updated')
|
||||||
|
|
||||||
|
|
||||||
class SecretSearchTable(SearchTable):
|
|
||||||
device = tables.LinkColumn()
|
|
||||||
|
|
||||||
class Meta(SearchTable.Meta):
|
|
||||||
model = Secret
|
|
||||||
fields = ('device', 'role', 'name', 'last_updated')
|
|
||||||
|
|||||||
@@ -10,13 +10,13 @@ urlpatterns = [
|
|||||||
|
|
||||||
# Secret roles
|
# Secret roles
|
||||||
url(r'^secret-roles/$', views.SecretRoleListView.as_view(), name='secretrole_list'),
|
url(r'^secret-roles/$', views.SecretRoleListView.as_view(), name='secretrole_list'),
|
||||||
url(r'^secret-roles/add/$', views.SecretRoleEditView.as_view(), name='secretrole_add'),
|
url(r'^secret-roles/add/$', views.SecretRoleCreateView.as_view(), name='secretrole_add'),
|
||||||
url(r'^secret-roles/delete/$', views.SecretRoleBulkDeleteView.as_view(), name='secretrole_bulk_delete'),
|
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'),
|
url(r'^secret-roles/(?P<slug>[\w-]+)/edit/$', views.SecretRoleEditView.as_view(), name='secretrole_edit'),
|
||||||
|
|
||||||
# Secrets
|
# Secrets
|
||||||
url(r'^secrets/$', views.SecretListView.as_view(), name='secret_list'),
|
url(r'^secrets/$', views.SecretListView.as_view(), name='secret_list'),
|
||||||
url(r'^secrets/import/$', views.secret_import, name='secret_import'),
|
url(r'^secrets/import/$', views.SecretBulkImportView.as_view(), name='secret_import'),
|
||||||
url(r'^secrets/edit/$', views.SecretBulkEditView.as_view(), name='secret_bulk_edit'),
|
url(r'^secrets/edit/$', views.SecretBulkEditView.as_view(), name='secret_bulk_edit'),
|
||||||
url(r'^secrets/delete/$', views.SecretBulkDeleteView.as_view(), name='secret_bulk_delete'),
|
url(r'^secrets/delete/$', views.SecretBulkDeleteView.as_view(), name='secret_bulk_delete'),
|
||||||
url(r'^secrets/(?P<pk>\d+)/$', views.SecretView.as_view(), name='secret'),
|
url(r'^secrets/(?P<pk>\d+)/$', views.SecretView.as_view(), name='secret'),
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import base64
|
|||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.auth.decorators import permission_required, login_required
|
from django.contrib.auth.decorators import permission_required, login_required
|
||||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||||
from django.db import transaction, IntegrityError
|
|
||||||
from django.db.models import Count
|
from django.db.models import Count
|
||||||
from django.shortcuts import get_object_or_404, redirect, render
|
from django.shortcuts import get_object_or_404, redirect, render
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
@@ -12,7 +11,9 @@ from django.utils.decorators import method_decorator
|
|||||||
from django.views.generic import View
|
from django.views.generic import View
|
||||||
|
|
||||||
from dcim.models import Device
|
from dcim.models import Device
|
||||||
from utilities.views import BulkDeleteView, BulkEditView, ObjectDeleteView, ObjectEditView, ObjectListView
|
from utilities.views import (
|
||||||
|
BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
|
||||||
|
)
|
||||||
from . import filters, forms, tables
|
from . import filters, forms, tables
|
||||||
from .decorators import userkey_required
|
from .decorators import userkey_required
|
||||||
from .models import SecretRole, Secret, SessionKey
|
from .models import SecretRole, Secret, SessionKey
|
||||||
@@ -38,8 +39,8 @@ class SecretRoleListView(ObjectListView):
|
|||||||
template_name = 'secrets/secretrole_list.html'
|
template_name = 'secrets/secretrole_list.html'
|
||||||
|
|
||||||
|
|
||||||
class SecretRoleEditView(PermissionRequiredMixin, ObjectEditView):
|
class SecretRoleCreateView(PermissionRequiredMixin, ObjectEditView):
|
||||||
permission_required = 'secrets.change_secretrole'
|
permission_required = 'secrets.add_secretrole'
|
||||||
model = SecretRole
|
model = SecretRole
|
||||||
form_class = forms.SecretRoleForm
|
form_class = forms.SecretRoleForm
|
||||||
|
|
||||||
@@ -47,9 +48,15 @@ class SecretRoleEditView(PermissionRequiredMixin, ObjectEditView):
|
|||||||
return reverse('secrets:secretrole_list')
|
return reverse('secrets:secretrole_list')
|
||||||
|
|
||||||
|
|
||||||
|
class SecretRoleEditView(SecretRoleCreateView):
|
||||||
|
permission_required = 'secrets.change_secretrole'
|
||||||
|
|
||||||
|
|
||||||
class SecretRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
class SecretRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
permission_required = 'secrets.delete_secretrole'
|
permission_required = 'secrets.delete_secretrole'
|
||||||
cls = SecretRole
|
cls = SecretRole
|
||||||
|
queryset = SecretRole.objects.annotate(secret_count=Count('secrets'))
|
||||||
|
table = tables.SecretRoleTable
|
||||||
default_return_url = 'secrets:secretrole_list'
|
default_return_url = 'secrets:secretrole_list'
|
||||||
|
|
||||||
|
|
||||||
@@ -185,71 +192,66 @@ class SecretDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
|||||||
default_return_url = 'secrets:secret_list'
|
default_return_url = 'secrets:secret_list'
|
||||||
|
|
||||||
|
|
||||||
@permission_required('secrets.add_secret')
|
class SecretBulkImportView(BulkImportView):
|
||||||
@userkey_required()
|
permission_required = 'ipam.add_vlan'
|
||||||
def secret_import(request):
|
model_form = forms.SecretCSVForm
|
||||||
|
table = tables.SecretTable
|
||||||
|
default_return_url = 'secrets:secret_list'
|
||||||
|
|
||||||
session_key = request.COOKIES.get('session_key', None)
|
|
||||||
|
|
||||||
if request.method == 'POST':
|
|
||||||
form = forms.SecretImportForm(request.POST)
|
|
||||||
|
|
||||||
if session_key is None:
|
|
||||||
form.add_error(None, "No session key was provided with the request. Unable to encrypt secret data.")
|
|
||||||
|
|
||||||
if form.is_valid():
|
|
||||||
|
|
||||||
new_secrets = []
|
|
||||||
|
|
||||||
session_key = base64.b64decode(session_key)
|
|
||||||
master_key = None
|
master_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
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
|
||||||
|
# Grab the session key from cookies.
|
||||||
|
session_key = request.COOKIES.get('session_key')
|
||||||
|
if session_key:
|
||||||
|
|
||||||
|
# Attempt to derive the master key using the provided session key.
|
||||||
try:
|
try:
|
||||||
sk = SessionKey.objects.get(userkey__user=request.user)
|
sk = SessionKey.objects.get(userkey__user=request.user)
|
||||||
master_key = sk.get_master_key(session_key)
|
self.master_key = sk.get_master_key(base64.b64decode(session_key))
|
||||||
except SessionKey.DoesNotExist:
|
except SessionKey.DoesNotExist:
|
||||||
form.add_error(None, "No session key found for this user.")
|
messages.error(request, "No session key found for this user.")
|
||||||
|
|
||||||
if master_key is None:
|
if self.master_key is not None:
|
||||||
form.add_error(None, "Invalid private key! Unable to encrypt secret data.")
|
return super(SecretBulkImportView, self).post(request)
|
||||||
else:
|
else:
|
||||||
try:
|
messages.error(request, "Invalid private key! Unable to encrypt secret data.")
|
||||||
with transaction.atomic():
|
|
||||||
for secret in form.cleaned_data['csv']:
|
|
||||||
secret.encrypt(master_key)
|
|
||||||
secret.save()
|
|
||||||
new_secrets.append(secret)
|
|
||||||
|
|
||||||
table = tables.SecretTable(new_secrets)
|
|
||||||
messages.success(request, "Imported {} new secrets.".format(len(new_secrets)))
|
|
||||||
|
|
||||||
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:
|
else:
|
||||||
form = forms.SecretImportForm()
|
messages.error(request, "No session key was provided with the request. Unable to encrypt secret data.")
|
||||||
|
|
||||||
return render(request, 'secrets/secret_import.html', {
|
return render(request, self.template_name, {
|
||||||
'form': form,
|
'form': self._import_form(request.POST),
|
||||||
'return_url': 'secrets:secret_list',
|
'fields': self.model_form().fields,
|
||||||
|
'obj_type': self.model_form._meta.model._meta.verbose_name,
|
||||||
|
'return_url': self.default_return_url,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
class SecretBulkEditView(PermissionRequiredMixin, BulkEditView):
|
class SecretBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||||
permission_required = 'secrets.change_secret'
|
permission_required = 'secrets.change_secret'
|
||||||
cls = Secret
|
cls = Secret
|
||||||
|
queryset = Secret.objects.select_related('role', 'device')
|
||||||
filter = filters.SecretFilter
|
filter = filters.SecretFilter
|
||||||
|
table = tables.SecretTable
|
||||||
form = forms.SecretBulkEditForm
|
form = forms.SecretBulkEditForm
|
||||||
template_name = 'secrets/secret_bulk_edit.html'
|
|
||||||
default_return_url = 'secrets:secret_list'
|
default_return_url = 'secrets:secret_list'
|
||||||
|
|
||||||
|
|
||||||
class SecretBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
class SecretBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
permission_required = 'secrets.delete_secret'
|
permission_required = 'secrets.delete_secret'
|
||||||
cls = Secret
|
cls = Secret
|
||||||
|
queryset = Secret.objects.select_related('role', 'device')
|
||||||
filter = filters.SecretFilter
|
filter = filters.SecretFilter
|
||||||
|
table = tables.SecretTable
|
||||||
default_return_url = 'secrets:secret_list'
|
default_return_url = 'secrets:secret_list'
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<title>NetBox - {% block title %}Home{% endblock %}</title>
|
<title>{% block title %}Home{% endblock %} - NetBox</title>
|
||||||
<link rel="stylesheet" href="{% static 'bootstrap-3.3.7-dist/css/bootstrap.min.css' %}">
|
<link rel="stylesheet" href="{% static 'bootstrap-3.3.7-dist/css/bootstrap.min.css' %}">
|
||||||
<link rel="stylesheet" href="{% static 'font-awesome-4.7.0/css/font-awesome.min.css' %}">
|
<link rel="stylesheet" href="{% static 'font-awesome-4.7.0/css/font-awesome.min.css' %}">
|
||||||
<link rel="stylesheet" href="{% static 'jquery-ui-1.12.1/jquery-ui.css' %}">
|
<link rel="stylesheet" href="{% static 'jquery-ui-1.12.1/jquery-ui.css' %}">
|
||||||
@@ -323,13 +323,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
<script type="text/javascript">
|
<script src="{% static 'js/jquery-3.2.1.min.js' %}"></script>
|
||||||
var netbox_api_path = "/{{ settings.BASE_PATH }}api/";
|
|
||||||
</script>
|
|
||||||
<script src="{% static 'js/jquery-3.2.0.min.js' %}"></script>
|
|
||||||
<script src="{% static 'jquery-ui-1.12.1/jquery-ui.min.js' %}"></script>
|
<script src="{% static 'jquery-ui-1.12.1/jquery-ui.min.js' %}"></script>
|
||||||
<script src="{% static 'bootstrap-3.3.7-dist/js/bootstrap.min.js' %}"></script>
|
<script src="{% static 'bootstrap-3.3.7-dist/js/bootstrap.min.js' %}"></script>
|
||||||
<script src="{% static 'js/forms.js' %}?v{{ settings.VERSION }}"></script>
|
<script src="{% static 'js/forms.js' %}?v{{ settings.VERSION }}"></script>
|
||||||
|
<script type="text/javascript">
|
||||||
|
var netbox_api_path = "/{{ settings.BASE_PATH }}api/";
|
||||||
|
var loading = $(".loading");
|
||||||
|
$(document).ajaxStart(function() {
|
||||||
|
loading.show();
|
||||||
|
}).ajaxStop(function() {
|
||||||
|
loading.hide();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
{% block javascript %}{% endblock %}
|
{% block javascript %}{% endblock %}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
{% extends 'utilities/bulk_edit_form.html' %}
|
|
||||||
{% load form_helpers %}
|
|
||||||
|
|
||||||
{% block title %}Circuit Bulk Edit{% endblock %}
|
|
||||||
|
|
||||||
{% block selected_objects_table %}
|
|
||||||
<tr>
|
|
||||||
<th>Circuit</th>
|
|
||||||
<th>Type</th>
|
|
||||||
<th>Provider</th>
|
|
||||||
<th>Port speed</th>
|
|
||||||
<th>Commit rate</th>
|
|
||||||
</tr>
|
|
||||||
{% for circuit in selected_objects %}
|
|
||||||
<tr>
|
|
||||||
<td><a href="{% url 'circuits:circuit' pk=circuit.pk %}">{{ circuit }}</a></td>
|
|
||||||
<td>{{ circuit.type }}</td>
|
|
||||||
<td>{{ circuit.provider }}</td>
|
|
||||||
<td>{{ circuit.port_speed_human }}</td>
|
|
||||||
<td>{{ circuit.commit_rate_human }}</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
{% endblock %}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
{% 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>
|
</tr>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="text-muted">None</td>
|
<td colspan="6" class="text-muted">None</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
{% extends 'utilities/bulk_edit_form.html' %}
|
|
||||||
{% load form_helpers %}
|
|
||||||
|
|
||||||
{% block title %}Provider Bulk Edit{% endblock %}
|
|
||||||
|
|
||||||
{% block selected_objects_table %}
|
|
||||||
<tr>
|
|
||||||
<th>Provider</th>
|
|
||||||
<th>Account</th>
|
|
||||||
<th>ASN</th>
|
|
||||||
</tr>
|
|
||||||
{% for provider in selected_objects %}
|
|
||||||
<tr>
|
|
||||||
<td><a href="{% url 'circuits:provider' slug=provider.slug %}">{{ provider }}</a></td>
|
|
||||||
<td>{{ provider.account }}</td>
|
|
||||||
<td>{{ provider.asn }}</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
{% endblock %}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
{% 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 %}
|
|
||||||
13
netbox/templates/dcim/bulk_disconnect.html
Normal file
13
netbox/templates/dcim/bulk_disconnect.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{% 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 %}
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
{% 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 %}
|
{% block content %}
|
||||||
{% include 'dcim/inc/device_header.html' with active_tab='info' %}
|
{% include 'dcim/inc/device_header.html' with active_tab='info' %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-5 col-lg-6">
|
<div class="col-md-5">
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<strong>Device</strong>
|
<strong>Device</strong>
|
||||||
@@ -204,7 +204,7 @@
|
|||||||
None
|
None
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.dcim.add_service %}
|
{% if perms.ipam.add_service %}
|
||||||
<div class="panel-footer text-right">
|
<div class="panel-footer text-right">
|
||||||
<a href="{% url 'dcim:service_assign' device=device.pk %}" class="btn btn-xs btn-primary">
|
<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
|
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Assign service
|
||||||
@@ -214,23 +214,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<strong>Critical Connections</strong>
|
<strong>Console / Power</strong>
|
||||||
</div>
|
</div>
|
||||||
<table class="table table-hover panel-body component-list">
|
<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 %}
|
{% for cp in console_ports %}
|
||||||
{% include 'dcim/inc/consoleport.html' %}
|
{% include 'dcim/inc/consoleport.html' %}
|
||||||
{% empty %}
|
{% empty %}
|
||||||
@@ -262,11 +248,6 @@
|
|||||||
</table>
|
</table>
|
||||||
{% if perms.dcim.add_interface or perms.dcim.add_consoleport or perms.dcim.add_powerport %}
|
{% if perms.dcim.add_interface or perms.dcim.add_consoleport or perms.dcim.add_powerport %}
|
||||||
<div class="panel-footer text-right">
|
<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 %}
|
{% if perms.dcim.add_consoleport %}
|
||||||
<a href="{% url 'dcim:consoleport_add' pk=device.pk %}" class="btn btn-xs btn-primary">
|
<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
|
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add console port
|
||||||
@@ -333,7 +314,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-7 col-lg-6">
|
<div class="col-md-7">
|
||||||
{% if device_bays or device.device_type.is_parent_device %}
|
{% if device_bays or device.device_type.is_parent_device %}
|
||||||
{% if perms.dcim.delete_devicebay %}
|
{% if perms.dcim.delete_devicebay %}
|
||||||
<form method="post" action="{% url 'dcim:devicebay_bulk_delete' pk=device.pk %}">
|
<form method="post" action="{% url 'dcim:devicebay_bulk_delete' pk=device.pk %}">
|
||||||
@@ -424,12 +405,17 @@
|
|||||||
<div class="panel-footer">
|
<div class="panel-footer">
|
||||||
{% if interfaces and perms.dcim.change_interface %}
|
{% 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">
|
<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 selected
|
<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
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if interfaces and perms.dcim.delete_interface %}
|
{% 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">
|
<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 selected
|
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.dcim.add_interface %}
|
{% if perms.dcim.add_interface %}
|
||||||
@@ -479,9 +465,14 @@
|
|||||||
</table>
|
</table>
|
||||||
{% if perms.dcim.add_consoleserverport or perms.dcim.delete_consoleserverport %}
|
{% if perms.dcim.add_consoleserverport or perms.dcim.delete_consoleserverport %}
|
||||||
<div class="panel-footer">
|
<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 %}
|
{% if cs_ports and perms.dcim.delete_consoleserverport %}
|
||||||
<button type="submit" class="btn btn-danger btn-xs">
|
<button type="submit" class="btn btn-danger btn-xs">
|
||||||
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete selected
|
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.dcim.add_consoleserverport %}
|
{% if perms.dcim.add_consoleserverport %}
|
||||||
@@ -531,9 +522,14 @@
|
|||||||
</table>
|
</table>
|
||||||
{% if perms.dcim.add_poweroutlet or perms.dcim.delete_poweroutlet %}
|
{% if perms.dcim.add_poweroutlet or perms.dcim.delete_poweroutlet %}
|
||||||
<div class="panel-footer">
|
<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 %}
|
{% if power_outlets and perms.dcim.delete_poweroutlet %}
|
||||||
<button type="submit" class="btn btn-danger btn-xs">
|
<button type="submit" class="btn btn-danger btn-xs">
|
||||||
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete selected
|
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.dcim.add_poweroutlet %}
|
{% if perms.dcim.add_poweroutlet %}
|
||||||
@@ -576,7 +572,7 @@ function toggleConnection(elem, api_url) {
|
|||||||
success: function() {
|
success: function() {
|
||||||
elem.parents('tr').removeClass('success').addClass('info');
|
elem.parents('tr').removeClass('success').addClass('info');
|
||||||
elem.removeClass('connected btn-warning').addClass('btn-success');
|
elem.removeClass('connected btn-warning').addClass('btn-success');
|
||||||
elem.attr('title', 'Mark connected');
|
elem.attr('title', 'Mark installed');
|
||||||
elem.children('i').removeClass('glyphicon glyphicon-ban-circle').addClass('fa fa-plug')
|
elem.children('i').removeClass('glyphicon glyphicon-ban-circle').addClass('fa fa-plug')
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -595,7 +591,7 @@ function toggleConnection(elem, api_url) {
|
|||||||
success: function() {
|
success: function() {
|
||||||
elem.parents('tr').removeClass('info').addClass('success');
|
elem.parents('tr').removeClass('info').addClass('success');
|
||||||
elem.removeClass('btn-success').addClass('connected btn-warning');
|
elem.removeClass('btn-success').addClass('connected btn-warning');
|
||||||
elem.attr('title', 'Mark disconnected');
|
elem.attr('title', 'Mark planned');
|
||||||
elem.children('i').removeClass('fa fa-plug').addClass('glyphicon glyphicon-ban-circle')
|
elem.children('i').removeClass('fa fa-plug').addClass('glyphicon glyphicon-ban-circle')
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
{% extends 'utilities/bulk_edit_form.html' %}
|
|
||||||
{% load form_helpers %}
|
|
||||||
|
|
||||||
{% block title %}Device Bulk Edit{% endblock %}
|
|
||||||
|
|
||||||
{% block selected_objects_table %}
|
|
||||||
<tr>
|
|
||||||
<th>Device</th>
|
|
||||||
<th>Type</th>
|
|
||||||
<th>Role</th>
|
|
||||||
<th>Tenant</th>
|
|
||||||
<th>Serial</th>
|
|
||||||
</tr>
|
|
||||||
{% for device in selected_objects %}
|
|
||||||
<tr>
|
|
||||||
<td><a href="{% url 'dcim:device' pk=device.pk %}">{{ device }}</a></td>
|
|
||||||
<td>{{ device.device_type.full_name }}</td>
|
|
||||||
<td>{{ device.device_role }}</td>
|
|
||||||
<td>{{ device.tenant }}</td>
|
|
||||||
<td>{{ device.serial }}</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
{% endblock %}
|
|
||||||
53
netbox/templates/dcim/device_config.html
Normal file
53
netbox/templates/dcim/device_config.html
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
{% extends '_base.html' %}
|
||||||
|
{% load staticfiles %}
|
||||||
|
|
||||||
|
{% block title %}{{ device }} - Config{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% include 'inc/ajax_loader.html' %}
|
||||||
|
{% include 'dcim/inc/device_header.html' with active_tab='config' %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-10 col-md-offset-1">
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading"><strong>Device Configuration</strong></div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<ul class="nav nav-tabs" role="tablist">
|
||||||
|
<li role="presentation" class="active"><a href="#running" aria-controls="running" role="tab" data-toggle="tab">Running</a></li>
|
||||||
|
<li role="presentation"><a href="#startup" aria-controls="startup" role="tab" data-toggle="tab">Startup</a></li>
|
||||||
|
<li role="presentation"><a href="#candidate" aria-controls="candidate" role="tab" data-toggle="tab">Candidate</a></li>
|
||||||
|
</ul>
|
||||||
|
<div class="tab-content">
|
||||||
|
<div role="tabpanel" class="tab-pane active" id="running">
|
||||||
|
<pre id="running_config"></pre>
|
||||||
|
</div>
|
||||||
|
<div role="tabpanel" class="tab-pane" id="startup">
|
||||||
|
<pre id="startup_config"></pre>
|
||||||
|
</div>
|
||||||
|
<div role="tabpanel" class="tab-pane" id="candidate">
|
||||||
|
<pre id="candidate_config"></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block javascript %}
|
||||||
|
<script type="text/javascript">
|
||||||
|
$(document).ready(function() {
|
||||||
|
$.ajax({
|
||||||
|
url: "{% url 'dcim-api:device-napalm' pk=device.pk %}?method=get_config",
|
||||||
|
dataType: 'json',
|
||||||
|
success: function(json) {
|
||||||
|
$('#running_config').html($.trim(json['get_config']['running']));
|
||||||
|
$('#startup_config').html($.trim(json['get_config']['startup']));
|
||||||
|
$('#candidate_config').html($.trim(json['get_config']['candidate']));
|
||||||
|
},
|
||||||
|
error: function(xhr) {
|
||||||
|
alert(xhr.responseText);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
@@ -1,103 +1,5 @@
|
|||||||
{% extends '_base.html' %}
|
{% extends 'utilities/obj_import.html' %}
|
||||||
{% load form_helpers %}
|
|
||||||
|
|
||||||
{% block title %}Device Import{% endblock %}
|
{% block tabs %}
|
||||||
|
{% include 'dcim/inc/device_import_header.html' %}
|
||||||
{% 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 %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,93 +1,5 @@
|
|||||||
{% extends '_base.html' %}
|
{% extends 'utilities/obj_import.html' %}
|
||||||
{% load form_helpers %}
|
|
||||||
|
|
||||||
{% block title %}Device Import{% endblock %}
|
{% block tabs %}
|
||||||
|
{% include 'dcim/inc/device_import_header.html' with active_tab='child_import' %}
|
||||||
{% 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 %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -51,6 +51,8 @@
|
|||||||
<th>Manufacturer</th>
|
<th>Manufacturer</th>
|
||||||
<th>Part Number</th>
|
<th>Part Number</th>
|
||||||
<th>Serial Number</th>
|
<th>Serial Number</th>
|
||||||
|
<th>Asset Tag</th>
|
||||||
|
<th>Description</th>
|
||||||
<th></th>
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|||||||
@@ -3,8 +3,9 @@
|
|||||||
{% block title %}{{ device }} - LLDP Neighbors{% endblock %}
|
{% block title %}{{ device }} - LLDP Neighbors{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% include 'dcim/inc/device_header.html' with active_tab='lldp-neighbors' %}
|
{% include 'inc/ajax_loader.html' %}
|
||||||
<div class="panel panel-default">
|
{% include 'dcim/inc/device_header.html' with active_tab='lldp-neighbors' %}
|
||||||
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<strong>LLDP Neighbors</strong>
|
<strong>LLDP Neighbors</strong>
|
||||||
</div>
|
</div>
|
||||||
@@ -20,7 +21,7 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for iface in interfaces %}
|
{% for iface in interfaces %}
|
||||||
<tr id="{{ iface }}">
|
<tr id="{{ iface.name }}">
|
||||||
<td>{{ iface }}</td>
|
<td>{{ iface }}</td>
|
||||||
{% if iface.connection %}
|
{% if iface.connection %}
|
||||||
{% with iface.connected_interface as connected_iface %}
|
{% with iface.connected_interface as connected_iface %}
|
||||||
@@ -40,27 +41,28 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block javascript %}
|
{% block javascript %}
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: "{% url 'dcim-api:device-lldp-neighbors' pk=device.pk %}",
|
url: "{% url 'dcim-api:device-napalm' pk=device.pk %}?method=get_lldp_neighbors",
|
||||||
dataType: 'json',
|
dataType: 'json',
|
||||||
success: function(json) {
|
success: function(json) {
|
||||||
$.each(json, function(i, neighbor) {
|
$.each(json['get_lldp_neighbors'], function(iface, neighbors) {
|
||||||
var row = $('#' + neighbor['local-interface'].replace(/(\/)/g, "\\$1"));
|
var neighbor = neighbors[0];
|
||||||
|
var row = $('#' + iface.replace(/(\/)/g, "\\$1"));
|
||||||
var configured_device = row.children('td.configured_device').attr('data');
|
var configured_device = row.children('td.configured_device').attr('data');
|
||||||
var configured_interface = row.children('td.configured_interface').attr('data');
|
var configured_interface = row.children('td.configured_interface').attr('data');
|
||||||
// Add LLDP neighbors to table
|
// Add LLDP neighbors to table
|
||||||
row.children('td.device').html(neighbor['name']);
|
row.children('td.device').html(neighbor['hostname']);
|
||||||
row.children('td.interface').html(neighbor['remote-interface']);
|
row.children('td.interface').html(neighbor['port']);
|
||||||
// Apply colors to rows
|
// Apply colors to rows
|
||||||
if (!configured_device && neighbor['name']) {
|
if (!configured_device && neighbor['hostname']) {
|
||||||
row.addClass('info');
|
row.addClass('info');
|
||||||
} else if (configured_device == neighbor['name'] && configured_interface == neighbor['remote-interface']) {
|
} else if (configured_device == neighbor['hostname'] && configured_interface == neighbor['port']) {
|
||||||
row.addClass('success');
|
row.addClass('success');
|
||||||
} else {
|
} else {
|
||||||
row.addClass('danger');
|
row.addClass('danger');
|
||||||
|
|||||||
125
netbox/templates/dcim/device_status.html
Normal file
125
netbox/templates/dcim/device_status.html
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
{% extends '_base.html' %}
|
||||||
|
{% load staticfiles %}
|
||||||
|
|
||||||
|
{% block title %}{{ device }} - Status{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% include 'inc/ajax_loader.html' %}
|
||||||
|
{% include 'dcim/inc/device_header.html' with active_tab='status' %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading"><strong>Device Facts</strong></div>
|
||||||
|
<table class="table panel-body">
|
||||||
|
<tr>
|
||||||
|
<th>Hostname</th>
|
||||||
|
<td id="hostname"></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>FQDN</th>
|
||||||
|
<td id="fqdn"></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Vendor</th>
|
||||||
|
<td id="vendor"></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Model</th>
|
||||||
|
<td id="model"></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Serial Number</th>
|
||||||
|
<td id="serial_number"></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>OS Version</th>
|
||||||
|
<td id="os_version"></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Uptime</th>
|
||||||
|
<td id="uptime"></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading"><strong>Environment</strong></div>
|
||||||
|
<table class="table panel-body">
|
||||||
|
<tr id="cpu">
|
||||||
|
<th colspan="2"><i class="fa fa-tachometer"></i> CPU</th>
|
||||||
|
</tr>
|
||||||
|
<tr id="memory">
|
||||||
|
<th colspan="2"><i class="fa fa-microchip"></i> Memory</th>
|
||||||
|
</tr>
|
||||||
|
<tr id="temperature">
|
||||||
|
<th colspan="2"><i class="fa fa-thermometer"></i> Temperature</th>
|
||||||
|
</tr>
|
||||||
|
<tr id="fans">
|
||||||
|
<th colspan="2"><i class="fa fa-superpowers"></i> Fans</th>
|
||||||
|
</tr>
|
||||||
|
<tr id="power">
|
||||||
|
<th colspan="2"><i class="fa fa-bolt"></i> Power</th>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block javascript %}
|
||||||
|
<script type="text/javascript">
|
||||||
|
$(document).ready(function() {
|
||||||
|
$.ajax({
|
||||||
|
url: "{% url 'dcim-api:device-napalm' pk=device.pk %}?method=get_facts&method=get_environment",
|
||||||
|
dataType: 'json',
|
||||||
|
success: function(json) {
|
||||||
|
$('#hostname').html(json['get_facts']['hostname']);
|
||||||
|
$('#fqdn').html(json['get_facts']['fqdn']);
|
||||||
|
$('#vendor').html(json['get_facts']['vendor']);
|
||||||
|
$('#model').html(json['get_facts']['model']);
|
||||||
|
$('#serial_number').html(json['get_facts']['serial_number']);
|
||||||
|
$('#os_version').html(json['get_facts']['os_version']);
|
||||||
|
$('#uptime').html(json['get_facts']['uptime']);
|
||||||
|
$.each(json['get_environment']['cpu'], function(name, obj) {
|
||||||
|
var row="<tr><td>" + name + "</td><td>" + obj['%usage'] + "%</td></tr>";
|
||||||
|
$("#cpu").after(row)
|
||||||
|
});
|
||||||
|
$('#memory').after("<tr><td>Used</td><td>" + json['get_environment']['memory']['used_ram'] + "MB</td></tr>");
|
||||||
|
$('#memory').after("<tr><td>Available</td><td>" + json['get_environment']['memory']['available_ram'] + "MB</td></tr>");
|
||||||
|
$.each(json['get_environment']['temperature'], function(name, obj) {
|
||||||
|
var style = "success";
|
||||||
|
if (obj['is_alert']) {
|
||||||
|
style = "warning";
|
||||||
|
} else if (obj['is_critical']) {
|
||||||
|
style = "danger";
|
||||||
|
}
|
||||||
|
var row="<tr class=\"" + style +"\"><td>" + name + "</td><td>" + obj['temperature'] + "°C</td></tr>";
|
||||||
|
$("#temperature").after(row)
|
||||||
|
});
|
||||||
|
$.each(json['get_environment']['fans'], function(name, obj) {
|
||||||
|
var row;
|
||||||
|
if (obj['status']) {
|
||||||
|
row="<tr class=\"success\"><td>" + name + "</td><td><i class=\"fa fa-check text-success\"></i></td></tr>";
|
||||||
|
} else {
|
||||||
|
row="<tr class=\"error\"><td>" + name + "</td><td><i class=\"fa fa-close text-error\"></i></td></tr>";
|
||||||
|
}
|
||||||
|
$("#fans").after(row)
|
||||||
|
});
|
||||||
|
$.each(json['get_environment']['power'], function(name, obj) {
|
||||||
|
var row;
|
||||||
|
if (obj['status']) {
|
||||||
|
row="<tr class=\"success\"><td>" + name + "</td><td><i class=\"fa fa-check text-success\"></i></td></tr>";
|
||||||
|
} else {
|
||||||
|
row="<tr class=\"danger\"><td>" + name + "</td><td><i class=\"fa fa-close text-danger\"></i></td></tr>";
|
||||||
|
}
|
||||||
|
$("#power").after(row)
|
||||||
|
});
|
||||||
|
},
|
||||||
|
error: function(xhr) {
|
||||||
|
alert(xhr.responseText);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
|
|
||||||
<h1>{{ devicetype.manufacturer }} {{ devicetype.model }}</h1>
|
<h1>{{ devicetype.manufacturer }} {{ devicetype.model }}</h1>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-5">
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<strong>Chassis</strong>
|
<strong>Chassis</strong>
|
||||||
@@ -163,21 +163,20 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</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=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' %}
|
{% 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' %}
|
||||||
{% 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' %}
|
{% if devicetype.is_parent_device or devicebay_table.rows %}
|
||||||
</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' %}
|
{% 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 %}
|
{% endif %}
|
||||||
{% if devicetype.is_network_device %}
|
{% if devicetype.is_network_device or interface_table.rows %}
|
||||||
{% 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' %}
|
{% 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 %}
|
{% endif %}
|
||||||
{% if devicetype.is_console_server %}
|
{% if devicetype.is_console_server or consoleserverport_table.rows %}
|
||||||
{% 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' %}
|
{% 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 %}
|
{% endif %}
|
||||||
{% if devicetype.is_pdu %}
|
{% if devicetype.is_pdu or poweroutlet_table.rows %}
|
||||||
{% 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' %}
|
{% 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 %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
{% extends 'utilities/bulk_edit_form.html' %}
|
|
||||||
{% load form_helpers %}
|
|
||||||
|
|
||||||
{% block title %}Device Type Bulk Edit{% endblock %}
|
|
||||||
|
|
||||||
{% block selected_objects_table %}
|
|
||||||
<tr>
|
|
||||||
<th>Device type</th>
|
|
||||||
<th>Manufacturer</th>
|
|
||||||
<th>Height</th>
|
|
||||||
</tr>
|
|
||||||
{% for devicetype in selected_objects %}
|
|
||||||
<tr>
|
|
||||||
<td><a href="{% url 'dcim:devicetype' pk=devicetype.pk %}">{{ devicetype.model }}</a></td>
|
|
||||||
<td>{{ devicetype.manufacturer }}</td>
|
|
||||||
<td>{{ devicetype.u_height }}U</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
{% endblock %}
|
|
||||||
@@ -1,13 +1,7 @@
|
|||||||
<tr class="consoleport{% if cp.cs_port and not cp.connection_status %} info{% endif %}">
|
<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>
|
<td>
|
||||||
<i class="fa fa-fw fa-keyboard-o"></i> {{ cp.name }}
|
<i class="fa fa-fw fa-keyboard-o"></i> {{ cp.name }}
|
||||||
</td>
|
</td>
|
||||||
<td></td>
|
|
||||||
{% if cp.cs_port %}
|
{% if cp.cs_port %}
|
||||||
<td>
|
<td>
|
||||||
<a href="{% url 'dcim:device' pk=cp.cs_port.device.pk %}">{{ cp.cs_port.device }}</a>
|
<a href="{% url 'dcim:device' pk=cp.cs_port.device.pk %}">{{ cp.cs_port.device }}</a>
|
||||||
@@ -20,28 +14,28 @@
|
|||||||
<span class="text-muted">Not connected</span>
|
<span class="text-muted">Not connected</span>
|
||||||
</td>
|
</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<td colspan="2" class="text-right">
|
<td class="text-right">
|
||||||
{% if perms.dcim.change_consoleport %}
|
{% if perms.dcim.change_consoleport %}
|
||||||
{% if cp.cs_port %}
|
{% if cp.cs_port %}
|
||||||
{% if cp.connection_status %}
|
{% if cp.connection_status %}
|
||||||
<a href="#" class="btn btn-warning btn-xs consoleport-toggle connected" data="{{ cp.pk }}">
|
<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" title="Mark planned"></i>
|
<i class="glyphicon glyphicon-ban-circle" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="#" class="btn btn-success btn-xs consoleport-toggle" data="{{ cp.pk }}">
|
<a href="#" class="btn btn-success btn-xs consoleport-toggle" title="Mark installed" data="{{ cp.pk }}">
|
||||||
<i class="fa fa-plug" aria-hidden="true" title="Mark connected"></i>
|
<i class="fa fa-plug" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a href="{% url 'dcim:consoleport_disconnect' pk=cp.pk %}" class="btn btn-danger btn-xs">
|
<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" title="Delete connection"></i>
|
<i class="glyphicon glyphicon-resize-full" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="{% url 'dcim:consoleport_connect' pk=cp.pk %}" class="btn btn-success btn-xs">
|
<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" title="Connect"></i>
|
<i class="glyphicon glyphicon-resize-small" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a href="{% url 'dcim:consoleport_edit' pk=cp.pk %}" class="btn btn-info btn-xs">
|
<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" title="Edit port"></i>
|
<i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.dcim.delete_consoleport %}
|
{% if perms.dcim.delete_consoleport %}
|
||||||
@@ -50,8 +44,8 @@
|
|||||||
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
|
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="{% url 'dcim:consoleport_delete' pk=cp.pk %}" class="btn btn-danger btn-xs">
|
<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" title="Delete port"></i>
|
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -24,24 +24,24 @@
|
|||||||
{% if perms.dcim.change_consoleserverport %}
|
{% if perms.dcim.change_consoleserverport %}
|
||||||
{% if csp.connected_console %}
|
{% if csp.connected_console %}
|
||||||
{% if csp.connected_console.connection_status %}
|
{% if csp.connected_console.connection_status %}
|
||||||
<a href="#" class="btn btn-warning btn-xs consoleport-toggle connected" data="{{ csp.connected_console.pk }}">
|
<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" title="Mark planned"></i>
|
<i class="glyphicon glyphicon-ban-circle" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="#" class="btn btn-success btn-xs consoleport-toggle" data="{{ csp.connected_console.pk }}">
|
<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" title="Mark connected"></i>
|
<i class="fa fa-plug" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a href="{% url 'dcim:consoleserverport_disconnect' pk=csp.pk %}" class="btn btn-danger btn-xs">
|
<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" title="Delete connection"></i>
|
<i class="glyphicon glyphicon-resize-full" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="{% url 'dcim:consoleserverport_connect' pk=csp.pk %}" class="btn btn-success btn-xs">
|
<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" title="Connect"></i>
|
<i class="glyphicon glyphicon-resize-small" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a href="{% url 'dcim:consoleserverport_edit' pk=csp.pk %}" class="btn btn-info btn-xs">
|
<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" title="Edit port"></i>
|
<i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.dcim.delete_consoleserverport %}
|
{% if perms.dcim.delete_consoleserverport %}
|
||||||
@@ -50,8 +50,8 @@
|
|||||||
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
|
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="{% url 'dcim:consoleserverport_delete' pk=csp.pk %}" class="btn btn-danger btn-xs">
|
<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" title="Delete port"></i>
|
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -45,7 +45,15 @@
|
|||||||
<ul class="nav nav-tabs" style="margin-bottom: 20px">
|
<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 == '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>
|
<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 %}
|
{% if perms.dcim.napalm_read %}
|
||||||
|
{% if device.status == 1 and device.platform.napalm_driver and device.primary_ip %}
|
||||||
|
<li role="presentation"{% if active_tab == 'status' %} class="active"{% endif %}><a href="{% url 'dcim:device_status' pk=device.pk %}">Status</a></li>
|
||||||
<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>
|
<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>
|
||||||
|
<li role="presentation"{% if active_tab == 'config' %} class="active"{% endif %}><a href="{% url 'dcim:device_config' pk=device.pk %}">Configuration</a></li>
|
||||||
|
{% else %}
|
||||||
|
<li role="presentation" class="disabled"><a href="#">Status</a></li>
|
||||||
|
<li role="presentation" class="disabled"><a href="#">LLDP Neighbors</a></li>
|
||||||
|
<li role="presentation" class="disabled"><a href="#">Configuration</a></li>
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
<h1>Device Import</h1>
|
|
||||||
<ul class="nav nav-tabs" style="margin-bottom: 20px">
|
<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 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>
|
<li role="presentation"{% if active_tab == 'child_import' %} class="active"{% endif %}><a href="{% url 'dcim:device_import_child' %}">Child Devices</a></li>
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
<tr class="interface{% if iface.connection and not iface.connection.connection_status %} info{% endif %}">
|
<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 %}">
|
||||||
{% if selectable and perms.dcim.change_interface or perms.dcim.delete_interface %}
|
{% if selectable and perms.dcim.change_interface or perms.dcim.delete_interface %}
|
||||||
<td class="pk">
|
<td class="pk">
|
||||||
<input name="pk" type="checkbox" value="{{ iface.pk }}" />
|
<input name="pk" type="checkbox" value="{{ iface.pk }}" />
|
||||||
</td>
|
</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<td>
|
<td>
|
||||||
<i class="fa fa-fw fa-{{ icon|default:"exchange" }}"></i> <span title="{{ iface.get_form_factor_display }}">{{ iface.name }}</span>
|
<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>
|
||||||
{% if iface.lag %}
|
{% if iface.lag %}
|
||||||
<span class="label label-primary">{{ iface.lag.name }}</span>
|
<span class="label label-primary">{{ iface.lag.name }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -13,6 +14,7 @@
|
|||||||
<i class="fa fa-fw fa-comment-o" title="{{ iface.description }}"></i>
|
<i class="fa fa-fw fa-comment-o" title="{{ iface.description }}"></i>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
|
<td>{{ iface.mtu|default:"" }}</td>
|
||||||
<td>{{ iface.mac_address|default:"" }}</td>
|
<td>{{ iface.mac_address|default:"" }}</td>
|
||||||
{% if iface.is_lag %}
|
{% if iface.is_lag %}
|
||||||
<td colspan="2" class="text-muted">
|
<td colspan="2" class="text-muted">
|
||||||
@@ -21,6 +23,8 @@
|
|||||||
</td>
|
</td>
|
||||||
{% elif iface.is_virtual %}
|
{% elif iface.is_virtual %}
|
||||||
<td colspan="2" class="text-muted">Virtual interface</td>
|
<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 %}
|
{% elif iface.connection %}
|
||||||
{% with iface.connected_interface as connected_iface %}
|
{% with iface.connected_interface as connected_iface %}
|
||||||
<td>
|
<td>
|
||||||
@@ -72,7 +76,7 @@
|
|||||||
<i class="glyphicon glyphicon-ban-circle" aria-hidden="true"></i>
|
<i class="glyphicon glyphicon-ban-circle" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="#" class="btn btn-success btn-xs interface-toggle" data="{{ iface.connection.pk }}" title="Mark connected">
|
<a href="#" class="btn btn-success btn-xs interface-toggle" data="{{ iface.connection.pk }}" title="Mark installed">
|
||||||
<i class="fa fa-plug" aria-hidden="true"></i>
|
<i class="fa fa-plug" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -114,7 +118,7 @@
|
|||||||
{% if selectable and perms.dcim.change_interface or perms.dcim.delete_interface %}
|
{% if selectable and perms.dcim.change_interface or perms.dcim.delete_interface %}
|
||||||
<td></td>
|
<td></td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<td colspan="2">
|
<td colspan="3">
|
||||||
<a href="{% url 'ipam:ipaddress' pk=ip.pk %}">{{ ip }}</a>
|
<a href="{% url 'ipam:ipaddress' pk=ip.pk %}">{{ ip }}</a>
|
||||||
{% if ip.description %}
|
{% if ip.description %}
|
||||||
<i class="fa fa-fw fa-comment-o" title="{{ ip.description }}"></i>
|
<i class="fa fa-fw fa-comment-o" title="{{ ip.description }}"></i>
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td style="padding-left: {{ indent|add:5 }}px">{{ item.name }}</td>
|
<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>{% 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.part_id }}</td>
|
||||||
<td>{{ item.serial }}</td>
|
<td>{{ item.serial }}</td>
|
||||||
|
<td>{{ item.asset_tag|default:"" }}</td>
|
||||||
|
<td>{{ item.description }}</td>
|
||||||
<td class="text-right">
|
<td class="text-right">
|
||||||
{% if perms.dcim.change_inventory_item %}
|
{% if perms.dcim.change_inventoryitem %}
|
||||||
<a href="{% url 'dcim:inventoryitem_edit' pk=item.pk %}" class="btn btn-xs btn-warning"><span class="glyphicon glyphicon-pencil" aria-hidden="true"></span></a>
|
<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 %}
|
{% endif %}
|
||||||
{% if perms.dcim.delete_inventory_item %}
|
{% if perms.dcim.delete_inventoryitem %}
|
||||||
<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>
|
<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 %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -24,24 +24,24 @@
|
|||||||
{% if perms.dcim.change_poweroutlet %}
|
{% if perms.dcim.change_poweroutlet %}
|
||||||
{% if po.connected_port %}
|
{% if po.connected_port %}
|
||||||
{% if po.connected_port.connection_status %}
|
{% if po.connected_port.connection_status %}
|
||||||
<a href="#" class="btn btn-warning btn-xs powerport-toggle connected" data="{{ po.connected_port.pk }}">
|
<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" title="Mark planned"></i>
|
<i class="glyphicon glyphicon-ban-circle" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="#" class="btn btn-success btn-xs consoleport-toggle" data="{{ po.connected_port.pk }}">
|
<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" title="Mark connected"></i>
|
<i class="fa fa-plug" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a href="{% url 'dcim:poweroutlet_disconnect' pk=po.pk %}" class="btn btn-danger btn-xs">
|
<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" title="Delete connection"></i>
|
<i class="glyphicon glyphicon-resize-full" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="{% url 'dcim:poweroutlet_connect' pk=po.pk %}" class="btn btn-success btn-xs">
|
<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" title="Connect"></i>
|
<i class="glyphicon glyphicon-resize-small" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a href="{% url 'dcim:poweroutlet_edit' pk=po.pk %}" class="btn btn-info btn-xs">
|
<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" title="Edit outlet"></i>
|
<i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.dcim.delete_poweroutlet %}
|
{% if perms.dcim.delete_poweroutlet %}
|
||||||
@@ -50,8 +50,8 @@
|
|||||||
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
|
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="{% url 'dcim:poweroutlet_delete' pk=po.pk %}" class="btn btn-danger btn-xs">
|
<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" title="Delete outlet"></i>
|
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -1,13 +1,7 @@
|
|||||||
<tr class="powerport{% if pp.power_outlet and not pp.connection_status %} info{% endif %}">
|
<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>
|
<td>
|
||||||
<i class="fa fa-fw fa-bolt"></i> {{ pp.name }}
|
<i class="fa fa-fw fa-bolt"></i> {{ pp.name }}
|
||||||
</td>
|
</td>
|
||||||
<td></td>
|
|
||||||
{% if pp.power_outlet %}
|
{% if pp.power_outlet %}
|
||||||
<td>
|
<td>
|
||||||
<a href="{% url 'dcim:device' pk=pp.power_outlet.device.pk %}">{{ pp.power_outlet.device }}</a>
|
<a href="{% url 'dcim:device' pk=pp.power_outlet.device.pk %}">{{ pp.power_outlet.device }}</a>
|
||||||
@@ -20,28 +14,28 @@
|
|||||||
<span class="text-muted">Not connected</span>
|
<span class="text-muted">Not connected</span>
|
||||||
</td>
|
</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<td colspan="2" class="text-right">
|
<td class="text-right">
|
||||||
{% if perms.dcim.change_powerport %}
|
{% if perms.dcim.change_powerport %}
|
||||||
{% if pp.power_outlet %}
|
{% if pp.power_outlet %}
|
||||||
{% if pp.connection_status %}
|
{% if pp.connection_status %}
|
||||||
<a href="#" class="btn btn-warning btn-xs powerport-toggle connected" data="{{ pp.pk }}">
|
<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" title="Mark planned"></i>
|
<i class="glyphicon glyphicon-ban-circle" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="#" class="btn btn-success btn-xs powerport-toggle" data="{{ pp.pk }}">
|
<a href="#" class="btn btn-success btn-xs powerport-toggle" title="Mark installed" data="{{ pp.pk }}">
|
||||||
<i class="fa fa-plug" aria-hidden="true" title="Mark connected"></i>
|
<i class="fa fa-plug" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a href="{% url 'dcim:powerport_disconnect' pk=pp.pk %}" class="btn btn-danger btn-xs">
|
<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" title="Delete connection"></i>
|
<i class="glyphicon glyphicon-resize-full" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="{% url 'dcim:powerport_connect' pk=pp.pk %}" class="btn btn-success btn-xs">
|
<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" title="Connect"></i>
|
<i class="glyphicon glyphicon-resize-small" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a href="{% url 'dcim:powerport_edit' pk=pp.pk %}" class="btn btn-info btn-xs">
|
<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" title="Edit port"></i>
|
<i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.dcim.delete_powerport %}
|
{% if perms.dcim.delete_powerport %}
|
||||||
@@ -50,8 +44,8 @@
|
|||||||
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
|
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="{% url 'dcim:powerport_delete' pk=pp.pk %}" class="btn btn-danger btn-xs">
|
<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" title="Delete port"></i>
|
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
{% extends 'utilities/bulk_edit_form.html' %}
|
|
||||||
{% load form_helpers %}
|
|
||||||
|
|
||||||
{% block title %}Interface Bulk Edit{% endblock %}
|
|
||||||
|
|
||||||
{% block selected_objects_table %}
|
|
||||||
<tr>
|
|
||||||
<th>Name</th>
|
|
||||||
<th>Form Factor</th>
|
|
||||||
</tr>
|
|
||||||
{% for iface in selected_objects %}
|
|
||||||
<tr>
|
|
||||||
<td>{{ iface.name }}</td>
|
|
||||||
<td>{{ iface.get_form_factor_display }}</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
{% endblock %}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
{% 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 %}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
{% extends 'utilities/bulk_edit_form.html' %}
|
|
||||||
{% load form_helpers %}
|
|
||||||
|
|
||||||
{% block title %}Interface Template Bulk Edit{% endblock %}
|
|
||||||
|
|
||||||
{% block selected_objects_table %}
|
|
||||||
<tr>
|
|
||||||
<th>Name</th>
|
|
||||||
<th>Form Factor</th>
|
|
||||||
<th>Management</th>
|
|
||||||
</tr>
|
|
||||||
{% for iface in selected_objects %}
|
|
||||||
<tr>
|
|
||||||
<td>{{ iface.name }}</td>
|
|
||||||
<td>{{ iface.get_form_factor_display }}</td>
|
|
||||||
<td>
|
|
||||||
{% if iface.mgmt_only %}
|
|
||||||
<i class="glyphicon glyphicon-ok text-success" title="Yes"></i>
|
|
||||||
{% else %}
|
|
||||||
<i class="glyphicon glyphicon-remove text-danger" title="No"></i>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
{% endblock %}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
{% 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 %}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
{% extends 'utilities/bulk_edit_form.html' %}
|
|
||||||
{% load form_helpers %}
|
|
||||||
|
|
||||||
{% block title %}Rack Bulk Edit{% endblock %}
|
|
||||||
|
|
||||||
{% block selected_objects_table %}
|
|
||||||
<tr>
|
|
||||||
<th>Name</th>
|
|
||||||
<th>Site</th>
|
|
||||||
<th>Group</th>
|
|
||||||
<th>Tenant</th>
|
|
||||||
<th>Role</th>
|
|
||||||
<th>Type</th>
|
|
||||||
<th>Width</th>
|
|
||||||
<th>Height</th>
|
|
||||||
</tr>
|
|
||||||
{% for rack in selected_objects %}
|
|
||||||
<tr>
|
|
||||||
<td><a href="{% url 'dcim:rack' pk=rack.pk %}">{{ rack }}</a></td>
|
|
||||||
<td>{{ rack.site }}</td>
|
|
||||||
<td>{{ rack.group }}</td>
|
|
||||||
<td>{{ rack.tenant }}</td>
|
|
||||||
<td>{{ rack.role }}</td>
|
|
||||||
<td>{{ rack.get_type_display }}</td>
|
|
||||||
<td>{{ rack.get_width_display }}</td>
|
|
||||||
<td>{{ rack.u_height }}U</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
{% endblock %}
|
|
||||||
@@ -14,7 +14,8 @@
|
|||||||
{% for rack in page %}
|
{% for rack in page %}
|
||||||
<div style="display: inline-block; width: 266px">
|
<div style="display: inline-block; width: 266px">
|
||||||
<div class="rack_header">
|
<div class="rack_header">
|
||||||
<h4><a href="{% url 'dcim:rack' pk=rack.pk %}">{{ rack.name }}</a></h4>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
{% if face_id %}
|
{% 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 %}
|
{% include 'dcim/inc/rack_elevation.html' with primary_face=rack.get_rear_elevation secondary_face=rack.get_front_elevation face_id=1 %}
|
||||||
@@ -23,7 +24,8 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="clearfix"></div>
|
<div class="clearfix"></div>
|
||||||
<div class="rack_header">
|
<div class="rack_header">
|
||||||
<h4><a href="{% url 'dcim:rack' pk=rack.pk %}">{{ rack.name }}</a></h4>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
@@ -1,70 +0,0 @@
|
|||||||
{% 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 %}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user