mirror of
https://github.com/netbox-community/netbox.git
synced 2025-08-13 02:58:17 -06:00
Merge branch 'develop-2.7' into 822-bulk-import-of-device-components
This commit is contained in:
commit
0c866561d8
4
.gitignore
vendored
4
.gitignore
vendored
@ -12,5 +12,9 @@
|
||||
fabfile.py
|
||||
*.swp
|
||||
gunicorn_config.py
|
||||
gunicorn.conf
|
||||
netbox.log
|
||||
netbox.pid
|
||||
.DS_Store
|
||||
.vscode
|
||||
.coverage
|
||||
|
@ -10,6 +10,7 @@ python:
|
||||
install:
|
||||
- pip install -r requirements.txt
|
||||
- pip install pycodestyle
|
||||
- pip install coverage
|
||||
before_script:
|
||||
- psql --version
|
||||
- psql -U postgres -c 'SELECT version();'
|
||||
|
@ -54,10 +54,6 @@ djangorestframework
|
||||
# https://github.com/axnsan12/drf-yasg
|
||||
drf-yasg[validation]
|
||||
|
||||
# Python interface to the graphviz graph rendering utility
|
||||
# https://github.com/xflr6/graphviz
|
||||
graphviz
|
||||
|
||||
# Simple markup language for rendering HTML
|
||||
# https://github.com/Python-Markdown/markdown
|
||||
# py-gfm requires Markdown<3.0
|
||||
|
22
contrib/gunicorn.conf
Normal file
22
contrib/gunicorn.conf
Normal file
@ -0,0 +1,22 @@
|
||||
# Bind is the ip and port that the Netbox WSGI should bind to
|
||||
#
|
||||
bind='127.0.0.1:8001'
|
||||
|
||||
# Workers is the number of workers that GUnicorn should spawn.
|
||||
# Workers should be: cores * 2 + 1. So if you have 8 cores, it would be 17.
|
||||
#
|
||||
workers=3
|
||||
|
||||
# Threads
|
||||
# The number of threads for handling requests
|
||||
#
|
||||
threads=3
|
||||
|
||||
# Timeout is the timeout between gunicorn receiving a request and returning a response (or failing with a 500 error)
|
||||
#
|
||||
timeout=120
|
||||
|
||||
# ErrorLog
|
||||
# ErrorLog is the logfile for the ErrorLog
|
||||
#
|
||||
errorlog='/opt/netbox/netbox.log'
|
24
contrib/netbox-rq.service
Normal file
24
contrib/netbox-rq.service
Normal file
@ -0,0 +1,24 @@
|
||||
[Unit]
|
||||
Description=Netbox RQ Worker
|
||||
Documentation=https://netbox.readthedocs.io/en/stable/
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
|
||||
EnvironmentFile=/etc/sysconfig/netbox.env
|
||||
|
||||
User=www-data
|
||||
Group=www-data
|
||||
|
||||
WorkingDirectory=${WorkingDirectory}
|
||||
|
||||
ExecStart=/usr/bin/python3 ${WorkingDirectory}/netbox/manage.py rqworker
|
||||
|
||||
Restart=on-failure
|
||||
RestartSec=30
|
||||
PrivateTmp=true
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
15
contrib/netbox.env
Normal file
15
contrib/netbox.env
Normal file
@ -0,0 +1,15 @@
|
||||
# Name is the Process Name
|
||||
#
|
||||
Name = 'Netbox'
|
||||
|
||||
# ConfigPath is the path to the gunicorn config file.
|
||||
#
|
||||
ConfigPath=/opt/netbox/gunicorn.conf
|
||||
|
||||
# WorkingDirectory is the Working Directory for Netbox.
|
||||
#
|
||||
WorkingDirectory=/opt/netbox/
|
||||
|
||||
# PidPath is the path to the pid for the netbox WSGI
|
||||
#
|
||||
PidPath=/opt/netbox/netbox.pid
|
24
contrib/netbox.service
Normal file
24
contrib/netbox.service
Normal file
@ -0,0 +1,24 @@
|
||||
[Unit]
|
||||
Description=Netbox WSGI
|
||||
Documentation=https://netbox.readthedocs.io/en/stable/
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
|
||||
EnvironmentFile=/etc/sysconfig/netbox.env
|
||||
|
||||
User=www-data
|
||||
Group=www-data
|
||||
PIDFile=${PidPath}
|
||||
WorkingDirectory=${WorkingDirectory}
|
||||
|
||||
ExecStart=/usr/local/bin/gunicorn --pid ${PidPath} --pythonpath ${WorkingDirectory}/netbox --config ${ConfigPath} netbox.wsgi
|
||||
|
||||
Restart=on-failure
|
||||
RestartSec=30
|
||||
PrivateTmp=true
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
24
contrib/netbox@.service
Normal file
24
contrib/netbox@.service
Normal file
@ -0,0 +1,24 @@
|
||||
[Unit]
|
||||
Description=Netbox WSGI
|
||||
Documentation=https://netbox.readthedocs.io/en/stable/
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
|
||||
EnvironmentFile=/etc/sysconfig/netbox.%i.env
|
||||
|
||||
User=www-data
|
||||
Group=www-data
|
||||
PIDFile=${PidPath}
|
||||
WorkingDirectory=${WorkingDirectory}
|
||||
|
||||
ExecStart=/usr/local/bin/gunicorn --pid ${PidPath} --pythonpath ${WorkingDirectory}/netbox --config ${ConfigPath} netbox.wsgi
|
||||
|
||||
Restart=on-failure
|
||||
RestartSec=30
|
||||
PrivateTmp=true
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
@ -1,17 +0,0 @@
|
||||
# Topology Maps
|
||||
|
||||
NetBox can generate simple topology maps from the physical network connections recorded in its database. First, you'll need to create a topology map definition under the admin UI at Extras > Topology Maps.
|
||||
|
||||
Each topology map is associated with a site. A site can have multiple topology maps, which might each illustrate a different aspect of its infrastructure (for example, production versus backend infrastructure).
|
||||
|
||||
To define the scope of a topology map, decide which devices you want to include. The map will only include interface connections with both points terminated on an included device. Specify the devices to include in the **device patterns** field by entering a list of [regular expressions](https://en.wikipedia.org/wiki/Regular_expression) matching device names. For example, if you wanted to include "mgmt-switch1" through "mgmt-switch99", you might use the regex `mgmt-switch\d+`.
|
||||
|
||||
Each line of the **device patterns** field represents a hierarchical layer within the topology map. For example, you might map a traditional network with core, distribution, and access tiers like this:
|
||||
|
||||
```
|
||||
core-switch-[abcd]
|
||||
dist-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.
|
@ -25,7 +25,7 @@ NetBox requires access to a PostgreSQL database service to store data. This serv
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
```python
|
||||
DATABASE = {
|
||||
'NAME': 'netbox', # Database name
|
||||
'USER': 'netbox', # PostgreSQL username
|
||||
@ -42,40 +42,48 @@ DATABASE = {
|
||||
|
||||
[Redis](https://redis.io/) is an in-memory data store similar to memcached. While Redis has been an optional component of
|
||||
NetBox since the introduction of webhooks in version 2.4, it is required starting in 2.6 to support NetBox's caching
|
||||
functionality (as well as other planned features).
|
||||
functionality (as well as other planned features). In 2.7, the connection settings were broken down into two sections for
|
||||
webhooks and caching, allowing the user to connect to different Redis instances/databases per feature.
|
||||
|
||||
Redis is configured using a configuration setting similar to `DATABASE`:
|
||||
Redis is configured using a configuration setting similar to `DATABASE` and these settings are the same for both of the `webhooks` and `caching` subsections:
|
||||
|
||||
* `HOST` - Name or IP address of the Redis server (use `localhost` if running locally)
|
||||
* `PORT` - TCP port of the Redis service; leave blank for default port (6379)
|
||||
* `PASSWORD` - Redis password (if set)
|
||||
* `DATABASE` - Numeric database ID for webhooks
|
||||
* `CACHE_DATABASE` - Numeric database ID for caching
|
||||
* `DATABASE` - Numeric database ID
|
||||
* `DEFAULT_TIMEOUT` - Connection timeout in seconds
|
||||
* `SSL` - Use SSL connection to Redis
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
```python
|
||||
REDIS = {
|
||||
'webhooks': {
|
||||
'HOST': 'redis.example.com',
|
||||
'PORT': 1234,
|
||||
'PASSWORD': 'foobar',
|
||||
'DATABASE': 0,
|
||||
'DEFAULT_TIMEOUT': 300,
|
||||
'SSL': False,
|
||||
},
|
||||
'caching': {
|
||||
'HOST': 'localhost',
|
||||
'PORT': 6379,
|
||||
'PASSWORD': '',
|
||||
'DATABASE': 0,
|
||||
'CACHE_DATABASE': 1,
|
||||
'DATABASE': 1,
|
||||
'DEFAULT_TIMEOUT': 300,
|
||||
'SSL': False,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
!!! note:
|
||||
If you were using these settings in a prior release with webhooks, the `DATABASE` setting remains the same but
|
||||
an additional `CACHE_DATABASE` setting has been added with a default value of 1 to support the caching backend. The
|
||||
`DATABASE` setting will be renamed in a future release of NetBox to better relay the meaning of the setting.
|
||||
If you are upgrading from a version prior to v2.7, please note that the Redis connection configuration settings have
|
||||
changed. Manual modification to bring the `REDIS` section inline with the above specification is necessary
|
||||
|
||||
!!! warning:
|
||||
It is highly recommended to keep the webhook and cache databases seperate. Using the same database number for both may result in webhook
|
||||
processing data being lost in cache flushing events.
|
||||
It is highly recommended to keep the webhook and cache databases separate. Using the same database number on the
|
||||
same Redis instance for both may result in webhook processing data being lost during cache flushing events.
|
||||
|
||||
---
|
||||
|
||||
|
@ -5,14 +5,14 @@ This section of the documentation discusses installing and configuring the NetBo
|
||||
**Ubuntu**
|
||||
|
||||
```no-highlight
|
||||
# apt-get install -y python3 python3-pip python3-dev build-essential libxml2-dev libxslt1-dev libffi-dev graphviz libpq-dev libssl-dev redis-server zlib1g-dev
|
||||
# apt-get install -y python3 python3-pip python3-dev build-essential libxml2-dev libxslt1-dev libffi-dev libpq-dev libssl-dev redis-server zlib1g-dev
|
||||
```
|
||||
|
||||
**CentOS**
|
||||
|
||||
```no-highlight
|
||||
# yum install -y epel-release
|
||||
# yum install -y gcc python36 python36-devel python36-setuptools libxml2-devel libxslt-devel libffi-devel graphviz openssl-devel redhat-rpm-config redis
|
||||
# yum install -y gcc python36 python36-devel python36-setuptools libxml2-devel libxslt-devel libffi-devel openssl-devel redhat-rpm-config redis
|
||||
# easy_install-3.6 pip
|
||||
# ln -s /usr/bin/python36 /usr/bin/python3
|
||||
```
|
||||
@ -139,14 +139,23 @@ Redis is a in-memory key-value store required as part of the NetBox installation
|
||||
|
||||
```python
|
||||
REDIS = {
|
||||
'webhooks': {
|
||||
'HOST': 'redis.example.com',
|
||||
'PORT': 1234,
|
||||
'PASSWORD': 'foobar',
|
||||
'DATABASE': 0,
|
||||
'DEFAULT_TIMEOUT': 300,
|
||||
'SSL': False,
|
||||
},
|
||||
'caching': {
|
||||
'HOST': 'localhost',
|
||||
'PORT': 6379,
|
||||
'PASSWORD': '',
|
||||
'DATABASE': 0,
|
||||
'CACHE_DATABASE': 1,
|
||||
'DATABASE': 1,
|
||||
'DEFAULT_TIMEOUT': 300,
|
||||
'SSL': False,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## SECRET_KEY
|
||||
|
@ -1,4 +1,4 @@
|
||||
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 use systemd to enable service persistence.
|
||||
|
||||
!!! info
|
||||
For the sake of brevity, only Ubuntu 18.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.
|
||||
@ -107,7 +107,7 @@ Install gunicorn:
|
||||
# pip3 install gunicorn
|
||||
```
|
||||
|
||||
Save the following configuration in the root netbox installation path as `gunicorn_config.py` (e.g. `/opt/netbox/gunicorn_config.py` per our example installation). Be sure to verify the location of the gunicorn executable on your server (e.g. `which gunicorn`) and to update the `pythonpath` variable if needed. If using CentOS/RHEL, change the username from `www-data` to `nginx` or `apache`. More info on `max_requests` can be found in the [gunicorn docs](https://docs.gunicorn.org/en/stable/settings.html#max-requests).
|
||||
Save the following configuration in the root NetBox installation path as `gunicorn_config.py` (e.g. `/opt/netbox/gunicorn_config.py` per our example installation). Be sure to verify the location of the gunicorn executable on your server (e.g. `which gunicorn`) and to update the `pythonpath` variable if needed. If using CentOS/RHEL, change the username from `www-data` to `nginx` or `apache`. More info on `max_requests` can be found in the [gunicorn docs](https://docs.gunicorn.org/en/stable/settings.html#max-requests).
|
||||
|
||||
```no-highlight
|
||||
command = '/usr/bin/gunicorn'
|
||||
@ -119,32 +119,99 @@ max_requests = 5000
|
||||
max_requests_jitter = 500
|
||||
```
|
||||
|
||||
# supervisord Installation
|
||||
# systemd configuration
|
||||
|
||||
Install supervisor:
|
||||
Copy or link contrib/netbox.service and contrib/netbox-rq.service to /etc/systemd/system/netbox.service and /etc/systemd/system/netbox-rq.service
|
||||
|
||||
```no-highlight
|
||||
# apt-get install -y supervisor
|
||||
# cp contrib/netbox.service to /etc/systemd/system/netbox.service
|
||||
# cp contrib/netbox-rq.service to /etc/systemd/system/netbox-rq.service
|
||||
```
|
||||
|
||||
Save the following as `/etc/supervisor/conf.d/netbox.conf`. Update the `command` and `directory` paths as needed. If using CentOS/RHEL, change the username from `www-data` to `nginx` or `apache`.
|
||||
Edit /etc/systemd/system/netbox.service and /etc/systemd/system/netbox-rq.service. Be sure to verify the location of the gunicorn executable on your server (e.g. `which gunicorn`). If using CentOS/RHEL, change the username from `www-data` to `nginx` or `apache`:
|
||||
|
||||
```no-highlight
|
||||
[program:netbox]
|
||||
command = gunicorn -c /opt/netbox/gunicorn_config.py netbox.wsgi
|
||||
directory = /opt/netbox/netbox/
|
||||
user = www-data
|
||||
|
||||
[program:netbox-rqworker]
|
||||
command = python3 /opt/netbox/netbox/manage.py rqworker
|
||||
directory = /opt/netbox/netbox/
|
||||
user = www-data
|
||||
/usr/local/bin/gunicorn --pid ${PidPath} --pythonpath ${WorkingDirectory}/netbox --config ${ConfigPath} netbox.wsgi
|
||||
```
|
||||
|
||||
Then, restart the supervisor service to detect and run the gunicorn service:
|
||||
```no-highlight
|
||||
User=www-data
|
||||
Group=www-data
|
||||
```
|
||||
|
||||
Copy contrib/netbox.env to /etc/sysconfig/netbox.env
|
||||
|
||||
```no-highlight
|
||||
# service supervisor restart
|
||||
# cp contrib/netbox.env to /etc/sysconfig/netbox.env
|
||||
```
|
||||
|
||||
Edit /etc/sysconfig/netbox.env and change the settings as required. Update the `WorkingDirectory` variable if needed.
|
||||
|
||||
```no-highlight
|
||||
# Name is the Process Name
|
||||
#
|
||||
Name = 'Netbox'
|
||||
|
||||
# ConfigPath is the path to the gunicorn config file.
|
||||
#
|
||||
ConfigPath=/opt/netbox/gunicorn.conf
|
||||
|
||||
# WorkingDirectory is the Working Directory for Netbox.
|
||||
#
|
||||
WorkingDirectory=/opt/netbox/
|
||||
|
||||
# PidPath is the path to the pid for the netbox WSGI
|
||||
#
|
||||
PidPath=/var/run/netbox.pid
|
||||
```
|
||||
|
||||
Copy contrib/gunicorn.conf to gunicorn.conf
|
||||
|
||||
```no-highlight
|
||||
# cp contrib/gunicorn.conf to gunicorn.conf
|
||||
```
|
||||
|
||||
Edit gunicorn.conf and change the settings as required.
|
||||
|
||||
```
|
||||
# Bind is the ip and port that the Netbox WSGI should bind to
|
||||
#
|
||||
bind='127.0.0.1:8001'
|
||||
|
||||
# Workers is the number of workers that GUnicorn should spawn.
|
||||
# Workers should be: cores * 2 + 1. So if you have 8 cores, it would be 17.
|
||||
#
|
||||
workers=3
|
||||
|
||||
# Threads
|
||||
# The number of threads for handling requests
|
||||
# Threads should be: cores * 2 + 1. So if you have 4 cores, it would be 9.
|
||||
#
|
||||
threads=3
|
||||
|
||||
# Timeout is the timeout between gunicorn receiving a request and returning a response (or failing with a 500 error)
|
||||
#
|
||||
timeout=120
|
||||
|
||||
# ErrorLog
|
||||
# ErrorLog is the logfile for the ErrorLog
|
||||
#
|
||||
errorlog='/opt/netbox/netbox.log'
|
||||
```
|
||||
|
||||
Then, restart the systemd daemon service to detect the netbox service and start the netbox service:
|
||||
|
||||
```no-highlight
|
||||
# systemctl daemon-reload
|
||||
# systemctl start netbox.service
|
||||
# systemctl enable netbox.service
|
||||
```
|
||||
|
||||
If using webhooks, also start the Redis worker:
|
||||
|
||||
```no-highlight
|
||||
# systemctl start netbox-rq.service
|
||||
# systemctl enable netbox-rq.service
|
||||
```
|
||||
|
||||
At this point, you should be able to connect to the nginx HTTP service at the server name or IP address you provided. If you are unable to connect, check that the nginx service is running and properly configured. If you receive a 502 (bad gateway) error, this indicates that gunicorn is misconfigured or not running.
|
||||
|
@ -12,3 +12,5 @@ The following sections detail how to set up a new instance of NetBox:
|
||||
If you are upgrading from an existing installation, please consult the [upgrading guide](upgrading.md).
|
||||
|
||||
NetBox v2.5 and later requires Python 3.5 or higher. Please see the instructions for [migrating to Python 3](migrating-to-python3.md) if you are still using Python 2.
|
||||
|
||||
Netbox v2.5.9 and later moved to using systemd instead of supervisord. Please see the instructions for [migrating to systemd](migrating-to-systemd.md) if you are still using supervisord.
|
||||
|
105
docs/installation/migrating-to-systemd.md
Normal file
105
docs/installation/migrating-to-systemd.md
Normal file
@ -0,0 +1,105 @@
|
||||
# Migration
|
||||
|
||||
Migration is not required, as supervisord will still continue to function.
|
||||
|
||||
## Ubuntu
|
||||
|
||||
### Remove supervisord:
|
||||
|
||||
```no-highlight
|
||||
# apt-get remove -y supervisord
|
||||
```
|
||||
|
||||
### systemd configuration:
|
||||
|
||||
Copy or link contrib/netbox.service and contrib/netbox-rq.service to /etc/systemd/system/netbox.service and /etc/systemd/system/netbox-rq.service
|
||||
|
||||
```no-highlight
|
||||
# cp contrib/netbox.service /etc/systemd/system/netbox.service
|
||||
# cp contrib/netbox-rq.service /etc/systemd/system/netbox-rq.service
|
||||
```
|
||||
|
||||
Edit /etc/systemd/system/netbox.service and /etc/systemd/system/netbox-rq.service. Be sure to verify the location of the gunicorn executable on your server (e.g. `which gunicorn`). If using CentOS/RHEL. Change the username from `www-data` to `nginx` or `apache`:
|
||||
|
||||
```no-highlight
|
||||
/usr/local/bin/gunicorn --pid ${PidPath} --pythonpath ${WorkingDirectory}/netbox --config ${ConfigPath} netbox.wsgi
|
||||
```
|
||||
|
||||
```no-highlight
|
||||
User=www-data
|
||||
Group=www-data
|
||||
```
|
||||
|
||||
Copy contrib/netbox.env to /etc/sysconfig/netbox.env
|
||||
|
||||
```no-highlight
|
||||
# cp contrib/netbox.env /etc/sysconfig/netbox.env
|
||||
```
|
||||
|
||||
Edit /etc/sysconfig/netbox.env and change the settings as required. Update the `WorkingDirectory` variable if needed.
|
||||
|
||||
```no-highlight
|
||||
# Name is the Process Name
|
||||
#
|
||||
Name = 'Netbox'
|
||||
|
||||
# ConfigPath is the path to the gunicorn config file.
|
||||
#
|
||||
ConfigPath=/opt/netbox/gunicorn.conf
|
||||
|
||||
# WorkingDirectory is the Working Directory for Netbox.
|
||||
#
|
||||
WorkingDirectory=/opt/netbox/
|
||||
|
||||
# PidPath is the path to the pid for the netbox WSGI
|
||||
#
|
||||
PidPath=/var/run/netbox.pid
|
||||
```
|
||||
|
||||
Copy contrib/gunicorn.conf to gunicorn.conf
|
||||
|
||||
```no-highlight
|
||||
# cp contrib/gunicorn.conf to gunicorn.conf
|
||||
```
|
||||
|
||||
Edit gunicorn.conf and change the settings as required.
|
||||
|
||||
```
|
||||
# Bind is the ip and port that the Netbox WSGI should bind to
|
||||
#
|
||||
bind='127.0.0.1:8001'
|
||||
|
||||
# Workers is the number of workers that GUnicorn should spawn.
|
||||
# Workers should be: cores * 2 + 1. So if you have 8 cores, it would be 17.
|
||||
#
|
||||
workers=3
|
||||
|
||||
# Threads
|
||||
# The number of threads for handling requests
|
||||
#
|
||||
threads=3
|
||||
|
||||
# Timeout is the timeout between gunicorn receiving a request and returning a response (or failing with a 500 error)
|
||||
#
|
||||
timeout=120
|
||||
|
||||
# ErrorLog
|
||||
# ErrorLog is the logfile for the ErrorLog
|
||||
#
|
||||
errorlog='/opt/netbox/netbox.log'
|
||||
```
|
||||
|
||||
Then, restart the systemd daemon service to detect the netbox service and start the netbox service:
|
||||
|
||||
```no-highlight
|
||||
# systemctl daemon-reload
|
||||
# systemctl start netbox.service
|
||||
# systemctl enable netbox.service
|
||||
```
|
||||
|
||||
If using webhooks, also start the Redis worker:
|
||||
|
||||
```no-highlight
|
||||
# systemctl start netbox-rq.service
|
||||
# systemctl enable netbox-rq.service
|
||||
```
|
@ -84,14 +84,17 @@ This script:
|
||||
|
||||
# Restart the WSGI Service
|
||||
|
||||
Finally, restart the WSGI service to run the new code. If you followed this guide for the initial installation, this is done using `supervisorctl`:
|
||||
Finally, restart the WSGI service to run the new code. If you followed this guide for the initial installation, this is done using `systemctl:
|
||||
|
||||
```no-highlight
|
||||
# sudo supervisorctl restart netbox
|
||||
# sudo systemctl restart netbox
|
||||
```
|
||||
|
||||
If using webhooks, also restart the Redis worker:
|
||||
|
||||
```no-highlight
|
||||
# sudo supervisorctl restart netbox-rqworker
|
||||
# sudo systemctl restart netbox-rqworker
|
||||
```
|
||||
|
||||
!!! note
|
||||
It's possible you are still using supervisord instead of the linux native systemd. If you are still using supervisord you can restart the services by either restarting supervisord or by using supervisorctl to restart netbox.
|
@ -1 +1 @@
|
||||
version-2.6.md
|
||||
version-2.7.md
|
100
docs/release-notes/version-2.7.md
Normal file
100
docs/release-notes/version-2.7.md
Normal file
@ -0,0 +1,100 @@
|
||||
# v2.7.0 (FUTURE)
|
||||
|
||||
## New Features
|
||||
|
||||
### Enhanced Device Type Import ([#451](https://github.com/netbox-community/netbox/issues/451))
|
||||
|
||||
NetBox now supports the import of device types and related component templates using a YAML- or JSON-based definition.
|
||||
For example, the following will create a new device type with four network interfaces, two power ports, and a console
|
||||
port:
|
||||
|
||||
```yaml
|
||||
manufacturer: Acme
|
||||
model: Packet Shooter 9000
|
||||
slug: packet-shooter-9000
|
||||
u_height: 1
|
||||
interfaces:
|
||||
- name: ge-0/0/0
|
||||
type: 1000base-t
|
||||
- name: ge-0/0/1
|
||||
type: 1000base-t
|
||||
- name: ge-0/0/2
|
||||
type: 1000base-t
|
||||
- name: ge-0/0/3
|
||||
type: 1000base-t
|
||||
power-ports:
|
||||
- name: PSU0
|
||||
- name: PSU1
|
||||
console-ports:
|
||||
- name: Console
|
||||
```
|
||||
|
||||
This new functionality replaces the existing CSV-based import form, which did not allow for component template import.
|
||||
|
||||
## Changes
|
||||
|
||||
### Topology Maps Removed ([#2745](https://github.com/netbox-community/netbox/issues/2745))
|
||||
|
||||
The topology maps feature has been removed to help focus NetBox development efforts.
|
||||
|
||||
### Redis Configuration ([#3282](https://github.com/netbox-community/netbox/issues/3282))
|
||||
|
||||
v2.6.0 introduced caching and added the `CACHE_DATABASE` option to the existing `REDIS` database configuration section.
|
||||
This did not however, allow for using two different Redis connections for the seperate caching and webhooks features.
|
||||
This change separates the Redis connection configurations in the `REDIS` section into distinct `webhooks` and `caching` subsections.
|
||||
This requires modification of the `REDIS` section of the `configuration.py` file as follows:
|
||||
|
||||
Old Redis configuration:
|
||||
```python
|
||||
REDIS = {
|
||||
'HOST': 'localhost',
|
||||
'PORT': 6379,
|
||||
'PASSWORD': '',
|
||||
'DATABASE': 0,
|
||||
'CACHE_DATABASE': 1,
|
||||
'DEFAULT_TIMEOUT': 300,
|
||||
'SSL': False,
|
||||
}
|
||||
```
|
||||
|
||||
New Redis configuration:
|
||||
```python
|
||||
REDIS = {
|
||||
'webhooks': {
|
||||
'HOST': 'redis.example.com',
|
||||
'PORT': 1234,
|
||||
'PASSWORD': 'foobar',
|
||||
'DATABASE': 0,
|
||||
'DEFAULT_TIMEOUT': 300,
|
||||
'SSL': False,
|
||||
},
|
||||
'caching': {
|
||||
'HOST': 'localhost',
|
||||
'PORT': 6379,
|
||||
'PASSWORD': '',
|
||||
'DATABASE': 1,
|
||||
'DEFAULT_TIMEOUT': 300,
|
||||
'SSL': False,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Note that `CACHE_DATABASE` has been removed and the connection settings have been duplicated for both `webhooks` and `caching`.
|
||||
This allows the user to make use of separate Redis instances and/or databases if desired.
|
||||
Full connection details are required in both sections, even if they are the same.
|
||||
|
||||
## Enhancements
|
||||
|
||||
* [#1865](https://github.com/digitalocean/netbox/issues/1865) - Add console port and console server port types
|
||||
* [#2902](https://github.com/digitalocean/netbox/issues/2902) - Replace supervisord with systemd
|
||||
* [#3455](https://github.com/digitalocean/netbox/issues/3455) - Add tenant assignment to cluster
|
||||
* [#3538](https://github.com/digitalocean/netbox/issues/3538) -
|
||||
|
||||
## API Changes
|
||||
|
||||
* Introduced `/api/extras/scripts/` endpoint for retrieving and executing custom scripts
|
||||
* dcim.ConsolePort: Added field `type`
|
||||
* dcim.ConsolePortTemplate: Added field `type`
|
||||
* dcim.ConsoleServerPort: Added field `type`
|
||||
* dcim.ConsoleServerPortTemplate: Added field `type`
|
||||
* virtualization.Cluster: Added field `tenant`
|
@ -4,6 +4,7 @@ from rest_framework import serializers
|
||||
from rest_framework.validators import UniqueTogetherValidator
|
||||
from taggit_serializer.serializers import TaggitSerializer, TagListSerializerField
|
||||
|
||||
from dcim.choices import *
|
||||
from dcim.constants import *
|
||||
from dcim.models import (
|
||||
Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||
@ -200,30 +201,46 @@ class DeviceTypeSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
|
||||
class ConsolePortTemplateSerializer(ValidatedModelSerializer):
|
||||
device_type = NestedDeviceTypeSerializer()
|
||||
type = ChoiceField(
|
||||
choices=ConsolePortTypes.CHOICES,
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ConsolePortTemplate
|
||||
fields = ['id', 'device_type', 'name']
|
||||
fields = ['id', 'device_type', 'name', 'type']
|
||||
|
||||
|
||||
class ConsoleServerPortTemplateSerializer(ValidatedModelSerializer):
|
||||
device_type = NestedDeviceTypeSerializer()
|
||||
type = ChoiceField(
|
||||
choices=ConsolePortTypes.CHOICES,
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ConsoleServerPortTemplate
|
||||
fields = ['id', 'device_type', 'name']
|
||||
fields = ['id', 'device_type', 'name', 'type']
|
||||
|
||||
|
||||
class PowerPortTemplateSerializer(ValidatedModelSerializer):
|
||||
device_type = NestedDeviceTypeSerializer()
|
||||
type = ChoiceField(
|
||||
choices=PowerPortTypes.CHOICES,
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = PowerPortTemplate
|
||||
fields = ['id', 'device_type', 'name', 'maximum_draw', 'allocated_draw']
|
||||
fields = ['id', 'device_type', 'name', 'type', 'maximum_draw', 'allocated_draw']
|
||||
|
||||
|
||||
class PowerOutletTemplateSerializer(ValidatedModelSerializer):
|
||||
device_type = NestedDeviceTypeSerializer()
|
||||
type = ChoiceField(
|
||||
choices=PowerOutletTypes.CHOICES,
|
||||
required=False
|
||||
)
|
||||
power_port = PowerPortTemplateSerializer(
|
||||
required=False
|
||||
)
|
||||
@ -235,18 +252,16 @@ class PowerOutletTemplateSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = PowerOutletTemplate
|
||||
fields = ['id', 'device_type', 'name', 'power_port', 'feed_leg']
|
||||
fields = ['id', 'device_type', 'name', 'type', 'power_port', 'feed_leg']
|
||||
|
||||
|
||||
class InterfaceTemplateSerializer(ValidatedModelSerializer):
|
||||
device_type = NestedDeviceTypeSerializer()
|
||||
type = ChoiceField(choices=IFACE_TYPE_CHOICES, required=False)
|
||||
# TODO: Remove in v2.7 (backward-compatibility for form_factor)
|
||||
form_factor = ChoiceField(choices=IFACE_TYPE_CHOICES, required=False)
|
||||
|
||||
class Meta:
|
||||
model = InterfaceTemplate
|
||||
fields = ['id', 'device_type', 'name', 'type', 'form_factor', 'mgmt_only']
|
||||
fields = ['id', 'device_type', 'name', 'type', 'mgmt_only']
|
||||
|
||||
|
||||
class RearPortTemplateSerializer(ValidatedModelSerializer):
|
||||
@ -372,32 +387,44 @@ class DeviceWithConfigContextSerializer(DeviceSerializer):
|
||||
|
||||
class ConsoleServerPortSerializer(TaggitSerializer, ConnectedEndpointSerializer):
|
||||
device = NestedDeviceSerializer()
|
||||
type = ChoiceField(
|
||||
choices=ConsolePortTypes.CHOICES,
|
||||
required=False
|
||||
)
|
||||
cable = NestedCableSerializer(read_only=True)
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = ConsoleServerPort
|
||||
fields = [
|
||||
'id', 'device', 'name', 'description', 'connected_endpoint_type', 'connected_endpoint', 'connection_status',
|
||||
'cable', 'tags',
|
||||
'id', 'device', 'name', 'type', 'description', 'connected_endpoint_type', 'connected_endpoint',
|
||||
'connection_status', 'cable', 'tags',
|
||||
]
|
||||
|
||||
|
||||
class ConsolePortSerializer(TaggitSerializer, ConnectedEndpointSerializer):
|
||||
device = NestedDeviceSerializer()
|
||||
type = ChoiceField(
|
||||
choices=ConsolePortTypes.CHOICES,
|
||||
required=False
|
||||
)
|
||||
cable = NestedCableSerializer(read_only=True)
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = ConsolePort
|
||||
fields = [
|
||||
'id', 'device', 'name', 'description', 'connected_endpoint_type', 'connected_endpoint', 'connection_status',
|
||||
'cable', 'tags',
|
||||
'id', 'device', 'name', 'type', 'description', 'connected_endpoint_type', 'connected_endpoint',
|
||||
'connection_status', 'cable', 'tags',
|
||||
]
|
||||
|
||||
|
||||
class PowerOutletSerializer(TaggitSerializer, ConnectedEndpointSerializer):
|
||||
device = NestedDeviceSerializer()
|
||||
type = ChoiceField(
|
||||
choices=PowerOutletTypes.CHOICES,
|
||||
required=False
|
||||
)
|
||||
power_port = NestedPowerPortSerializer(
|
||||
required=False
|
||||
)
|
||||
@ -416,20 +443,24 @@ class PowerOutletSerializer(TaggitSerializer, ConnectedEndpointSerializer):
|
||||
class Meta:
|
||||
model = PowerOutlet
|
||||
fields = [
|
||||
'id', 'device', 'name', 'power_port', 'feed_leg', 'description', 'connected_endpoint_type',
|
||||
'id', 'device', 'name', 'type', 'power_port', 'feed_leg', 'description', 'connected_endpoint_type',
|
||||
'connected_endpoint', 'connection_status', 'cable', 'tags',
|
||||
]
|
||||
|
||||
|
||||
class PowerPortSerializer(TaggitSerializer, ConnectedEndpointSerializer):
|
||||
device = NestedDeviceSerializer()
|
||||
type = ChoiceField(
|
||||
choices=PowerPortTypes.CHOICES,
|
||||
required=False
|
||||
)
|
||||
cable = NestedCableSerializer(read_only=True)
|
||||
tags = TagListSerializerField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = PowerPort
|
||||
fields = [
|
||||
'id', 'device', 'name', 'maximum_draw', 'allocated_draw', 'description', 'connected_endpoint_type',
|
||||
'id', 'device', 'name', 'type', 'maximum_draw', 'allocated_draw', 'description', 'connected_endpoint_type',
|
||||
'connected_endpoint', 'connection_status', 'cable', 'tags',
|
||||
]
|
||||
|
||||
@ -437,8 +468,6 @@ class PowerPortSerializer(TaggitSerializer, ConnectedEndpointSerializer):
|
||||
class InterfaceSerializer(TaggitSerializer, ConnectedEndpointSerializer):
|
||||
device = NestedDeviceSerializer()
|
||||
type = ChoiceField(choices=IFACE_TYPE_CHOICES, required=False)
|
||||
# TODO: Remove in v2.7 (backward-compatibility for form_factor)
|
||||
form_factor = ChoiceField(choices=IFACE_TYPE_CHOICES, required=False)
|
||||
lag = NestedInterfaceSerializer(required=False, allow_null=True)
|
||||
mode = ChoiceField(choices=IFACE_MODE_CHOICES, required=False, allow_null=True)
|
||||
untagged_vlan = NestedVLANSerializer(required=False, allow_null=True)
|
||||
@ -454,9 +483,9 @@ class InterfaceSerializer(TaggitSerializer, ConnectedEndpointSerializer):
|
||||
class Meta:
|
||||
model = Interface
|
||||
fields = [
|
||||
'id', 'device', 'name', 'type', 'form_factor', 'enabled', 'lag', 'mtu', 'mac_address', 'mgmt_only',
|
||||
'description', 'connected_endpoint_type', 'connected_endpoint', 'connection_status', 'cable', 'mode',
|
||||
'untagged_vlan', 'tagged_vlans', 'tags', 'count_ipaddresses',
|
||||
'id', 'device', 'name', 'type', 'enabled', 'lag', 'mtu', 'mac_address', 'mgmt_only', 'description',
|
||||
'connected_endpoint_type', 'connected_endpoint', 'connection_status', 'cable', 'mode', 'untagged_vlan',
|
||||
'tagged_vlans', 'tags', 'count_ipaddresses',
|
||||
]
|
||||
|
||||
# TODO: This validation should be handled by Interface.clean()
|
||||
|
@ -42,16 +42,20 @@ from .exceptions import MissingFilterException
|
||||
class DCIMFieldChoicesViewSet(FieldChoicesViewSet):
|
||||
fields = (
|
||||
(Cable, ['length_unit', 'status', 'termination_a_type', 'termination_b_type', 'type']),
|
||||
(ConsolePort, ['connection_status']),
|
||||
(ConsolePort, ['type', 'connection_status']),
|
||||
(ConsolePortTemplate, ['type']),
|
||||
(ConsoleServerPort, ['type']),
|
||||
(ConsoleServerPortTemplate, ['type']),
|
||||
(Device, ['face', 'status']),
|
||||
(DeviceType, ['subdevice_role']),
|
||||
(FrontPort, ['type']),
|
||||
(FrontPortTemplate, ['type']),
|
||||
(Interface, ['type', 'mode']),
|
||||
(InterfaceTemplate, ['type']),
|
||||
(PowerOutlet, ['feed_leg']),
|
||||
(PowerOutletTemplate, ['feed_leg']),
|
||||
(PowerPort, ['connection_status']),
|
||||
(PowerOutlet, ['type', 'feed_leg']),
|
||||
(PowerOutletTemplate, ['type', 'feed_leg']),
|
||||
(PowerPort, ['type', 'connection_status']),
|
||||
(PowerPortTemplate, ['type']),
|
||||
(Rack, ['outer_unit', 'status', 'type', 'width']),
|
||||
(RearPort, ['type']),
|
||||
(RearPortTemplate, ['type']),
|
||||
|
594
netbox/dcim/choices.py
Normal file
594
netbox/dcim/choices.py
Normal file
@ -0,0 +1,594 @@
|
||||
from .constants import *
|
||||
|
||||
|
||||
#
|
||||
# Console port type values
|
||||
#
|
||||
|
||||
class ConsolePortTypes:
|
||||
"""
|
||||
ConsolePort/ConsoleServerPort.type slugs
|
||||
"""
|
||||
TYPE_DE9 = 'de-9'
|
||||
TYPE_DB25 = 'db-25'
|
||||
TYPE_RJ45 = 'rj-45'
|
||||
TYPE_USB_A = 'usb-a'
|
||||
TYPE_USB_B = 'usb-b'
|
||||
TYPE_USB_C = 'usb-c'
|
||||
TYPE_USB_MINI_A = 'usb-mini-a'
|
||||
TYPE_USB_MINI_B = 'usb-mini-b'
|
||||
TYPE_USB_MICRO_A = 'usb-micro-a'
|
||||
TYPE_USB_MICRO_B = 'usb-micro-b'
|
||||
TYPE_OTHER = 'other'
|
||||
|
||||
CHOICES = (
|
||||
('Serial', (
|
||||
(TYPE_DE9, 'DE-9'),
|
||||
(TYPE_DB25, 'DB-25'),
|
||||
(TYPE_RJ45, 'RJ-45'),
|
||||
)),
|
||||
('USB', (
|
||||
(TYPE_USB_A, 'USB Type A'),
|
||||
(TYPE_USB_B, 'USB Type B'),
|
||||
(TYPE_USB_C, 'USB Type C'),
|
||||
(TYPE_USB_MINI_A, 'USB Mini A'),
|
||||
(TYPE_USB_MINI_B, 'USB Mini B'),
|
||||
(TYPE_USB_MICRO_A, 'USB Micro A'),
|
||||
(TYPE_USB_MICRO_B, 'USB Micro B'),
|
||||
)),
|
||||
('Other', (
|
||||
(TYPE_OTHER, 'Other'),
|
||||
)),
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Power port types
|
||||
#
|
||||
|
||||
class PowerPortTypes:
|
||||
# TODO: Add more power port types
|
||||
# IEC 60320
|
||||
TYPE_IEC_C6 = 'iec-60320-c6'
|
||||
TYPE_IEC_C8 = 'iec-60320-c8'
|
||||
TYPE_IEC_C14 = 'iec-60320-c14'
|
||||
TYPE_IEC_C16 = 'iec-60320-c16'
|
||||
TYPE_IEC_C20 = 'iec-60320-c20'
|
||||
# IEC 60309
|
||||
TYPE_IEC_PNE4H = 'iec-60309-p-n-e-4h'
|
||||
TYPE_IEC_PNE6H = 'iec-60309-p-n-e-6h'
|
||||
TYPE_IEC_PNE9H = 'iec-60309-p-n-e-9h'
|
||||
TYPE_IEC_2PE4H = 'iec-60309-2p-e-4h'
|
||||
TYPE_IEC_2PE6H = 'iec-60309-2p-e-6h'
|
||||
TYPE_IEC_2PE9H = 'iec-60309-2p-e-9h'
|
||||
TYPE_IEC_3PE4H = 'iec-60309-3p-e-4h'
|
||||
TYPE_IEC_3PE6H = 'iec-60309-3p-e-6h'
|
||||
TYPE_IEC_3PE9H = 'iec-60309-3p-e-9h'
|
||||
TYPE_IEC_3PNE4H = 'iec-60309-3p-n-e-4h'
|
||||
TYPE_IEC_3PNE6H = 'iec-60309-3p-n-e-6h'
|
||||
TYPE_IEC_3PNE9H = 'iec-60309-3p-n-e-9h'
|
||||
# NEMA non-locking
|
||||
TYPE_NEMA_515P = 'nema-5-15p'
|
||||
TYPE_NEMA_520P = 'nema-5-20p'
|
||||
TYPE_NEMA_530P = 'nema-5-30p'
|
||||
TYPE_NEMA_550P = 'nema-5-50p'
|
||||
TYPE_NEMA_615P = 'nema-6-15p'
|
||||
TYPE_NEMA_620P = 'nema-6-20p'
|
||||
TYPE_NEMA_630P = 'nema-6-30p'
|
||||
TYPE_NEMA_650P = 'nema-6-50p'
|
||||
# NEMA locking
|
||||
TYPE_NEMA_L515P = 'nema-l5-15p'
|
||||
TYPE_NEMA_L520P = 'nema-l5-20p'
|
||||
TYPE_NEMA_L530P = 'nema-l5-30p'
|
||||
TYPE_NEMA_L615P = 'nema-l5-50p'
|
||||
TYPE_NEMA_L620P = 'nema-l6-20p'
|
||||
TYPE_NEMA_L630P = 'nema-l6-30p'
|
||||
TYPE_NEMA_L650P = 'nema-l6-50p'
|
||||
|
||||
CHOICES = (
|
||||
('IEC 60320', (
|
||||
(TYPE_IEC_C6, 'C6'),
|
||||
(TYPE_IEC_C8, 'C8'),
|
||||
(TYPE_IEC_C14, 'C14'),
|
||||
(TYPE_IEC_C16, 'C16'),
|
||||
(TYPE_IEC_C20, 'C20'),
|
||||
)),
|
||||
('IEC 60309', (
|
||||
(TYPE_IEC_PNE4H, 'P+N+E 4H'),
|
||||
(TYPE_IEC_PNE6H, 'P+N+E 6H'),
|
||||
(TYPE_IEC_PNE9H, 'P+N+E 9H'),
|
||||
(TYPE_IEC_2PE4H, '2P+E 4H'),
|
||||
(TYPE_IEC_2PE6H, '2P+E 6H'),
|
||||
(TYPE_IEC_2PE9H, '2P+E 9H'),
|
||||
(TYPE_IEC_3PE4H, '3P+E 4H'),
|
||||
(TYPE_IEC_3PE6H, '3P+E 6H'),
|
||||
(TYPE_IEC_3PE9H, '3P+E 9H'),
|
||||
(TYPE_IEC_3PNE4H, '3P+N+E 4H'),
|
||||
(TYPE_IEC_3PNE6H, '3P+N+E 6H'),
|
||||
(TYPE_IEC_3PNE9H, '3P+N+E 9H'),
|
||||
)),
|
||||
('NEMA (Non-locking)', (
|
||||
(TYPE_NEMA_515P, 'NEMA 5-15P'),
|
||||
(TYPE_NEMA_520P, 'NEMA 5-20P'),
|
||||
(TYPE_NEMA_530P, 'NEMA 5-30P'),
|
||||
(TYPE_NEMA_550P, 'NEMA 5-50P'),
|
||||
(TYPE_NEMA_615P, 'NEMA 6-15P'),
|
||||
(TYPE_NEMA_620P, 'NEMA 6-20P'),
|
||||
(TYPE_NEMA_630P, 'NEMA 6-30P'),
|
||||
(TYPE_NEMA_650P, 'NEMA 6-50P'),
|
||||
)),
|
||||
('NEMA (Locking)', (
|
||||
(TYPE_NEMA_L515P, 'NEMA L5-15P'),
|
||||
(TYPE_NEMA_L520P, 'NEMA L5-20P'),
|
||||
(TYPE_NEMA_L530P, 'NEMA L5-30P'),
|
||||
(TYPE_NEMA_L615P, 'NEMA L6-15P'),
|
||||
(TYPE_NEMA_L620P, 'NEMA L6-20P'),
|
||||
(TYPE_NEMA_L630P, 'NEMA L6-30P'),
|
||||
(TYPE_NEMA_L650P, 'NEMA L6-50P'),
|
||||
)),
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Power outlet types
|
||||
#
|
||||
|
||||
class PowerOutletTypes:
|
||||
# TODO: Add more power outlet types
|
||||
# IEC 60320
|
||||
TYPE_IEC_C5 = 'iec-60320-c5'
|
||||
TYPE_IEC_C7 = 'iec-60320-c7'
|
||||
TYPE_IEC_C13 = 'iec-60320-c13'
|
||||
TYPE_IEC_C15 = 'iec-60320-c15'
|
||||
TYPE_IEC_C19 = 'iec-60320-c19'
|
||||
# IEC 60309
|
||||
TYPE_IEC_PNE4H = 'iec-60309-p-n-e-4h'
|
||||
TYPE_IEC_PNE6H = 'iec-60309-p-n-e-6h'
|
||||
TYPE_IEC_PNE9H = 'iec-60309-p-n-e-9h'
|
||||
TYPE_IEC_2PE4H = 'iec-60309-2p-e-4h'
|
||||
TYPE_IEC_2PE6H = 'iec-60309-2p-e-6h'
|
||||
TYPE_IEC_2PE9H = 'iec-60309-2p-e-9h'
|
||||
TYPE_IEC_3PE4H = 'iec-60309-3p-e-4h'
|
||||
TYPE_IEC_3PE6H = 'iec-60309-3p-e-6h'
|
||||
TYPE_IEC_3PE9H = 'iec-60309-3p-e-9h'
|
||||
TYPE_IEC_3PNE4H = 'iec-60309-3p-n-e-4h'
|
||||
TYPE_IEC_3PNE6H = 'iec-60309-3p-n-e-6h'
|
||||
TYPE_IEC_3PNE9H = 'iec-60309-3p-n-e-9h'
|
||||
# NEMA non-locking
|
||||
TYPE_NEMA_515R = 'nema-5-15r'
|
||||
TYPE_NEMA_520R = 'nema-5-20r'
|
||||
TYPE_NEMA_530R = 'nema-5-30r'
|
||||
TYPE_NEMA_550R = 'nema-5-50r'
|
||||
TYPE_NEMA_615R = 'nema-6-15r'
|
||||
TYPE_NEMA_620R = 'nema-6-20r'
|
||||
TYPE_NEMA_630R = 'nema-6-30r'
|
||||
TYPE_NEMA_650R = 'nema-6-50r'
|
||||
# NEMA locking
|
||||
TYPE_NEMA_L515R = 'nema-l5-15r'
|
||||
TYPE_NEMA_L520R = 'nema-l5-20r'
|
||||
TYPE_NEMA_L530R = 'nema-l5-30r'
|
||||
TYPE_NEMA_L615R = 'nema-l5-50r'
|
||||
TYPE_NEMA_L620R = 'nema-l6-20r'
|
||||
TYPE_NEMA_L630R = 'nema-l6-30r'
|
||||
TYPE_NEMA_L650R = 'nema-l6-50r'
|
||||
|
||||
CHOICES = (
|
||||
('IEC 60320', (
|
||||
(TYPE_IEC_C5, 'C5'),
|
||||
(TYPE_IEC_C7, 'C7'),
|
||||
(TYPE_IEC_C13, 'C13'),
|
||||
(TYPE_IEC_C15, 'C15'),
|
||||
(TYPE_IEC_C19, 'C19'),
|
||||
)),
|
||||
('IEC 60309', (
|
||||
(TYPE_IEC_PNE4H, 'P+N+E 4H'),
|
||||
(TYPE_IEC_PNE6H, 'P+N+E 6H'),
|
||||
(TYPE_IEC_PNE9H, 'P+N+E 9H'),
|
||||
(TYPE_IEC_2PE4H, '2P+E 4H'),
|
||||
(TYPE_IEC_2PE6H, '2P+E 6H'),
|
||||
(TYPE_IEC_2PE9H, '2P+E 9H'),
|
||||
(TYPE_IEC_3PE4H, '3P+E 4H'),
|
||||
(TYPE_IEC_3PE6H, '3P+E 6H'),
|
||||
(TYPE_IEC_3PE9H, '3P+E 9H'),
|
||||
(TYPE_IEC_3PNE4H, '3P+N+E 4H'),
|
||||
(TYPE_IEC_3PNE6H, '3P+N+E 6H'),
|
||||
(TYPE_IEC_3PNE9H, '3P+N+E 9H'),
|
||||
)),
|
||||
('NEMA (Non-locking)', (
|
||||
(TYPE_NEMA_515R, 'NEMA 5-15R'),
|
||||
(TYPE_NEMA_520R, 'NEMA 5-20R'),
|
||||
(TYPE_NEMA_530R, 'NEMA 5-30R'),
|
||||
(TYPE_NEMA_550R, 'NEMA 5-50R'),
|
||||
(TYPE_NEMA_615R, 'NEMA 6-15R'),
|
||||
(TYPE_NEMA_620R, 'NEMA 6-20R'),
|
||||
(TYPE_NEMA_630R, 'NEMA 6-30R'),
|
||||
(TYPE_NEMA_650R, 'NEMA 6-50R'),
|
||||
)),
|
||||
('NEMA (Locking)', (
|
||||
(TYPE_NEMA_L515R, 'NEMA L5-15R'),
|
||||
(TYPE_NEMA_L520R, 'NEMA L5-20R'),
|
||||
(TYPE_NEMA_L530R, 'NEMA L5-30R'),
|
||||
(TYPE_NEMA_L615R, 'NEMA L6-15R'),
|
||||
(TYPE_NEMA_L620R, 'NEMA L6-20R'),
|
||||
(TYPE_NEMA_L630R, 'NEMA L6-30R'),
|
||||
(TYPE_NEMA_L650R, 'NEMA L6-50R'),
|
||||
)),
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Interface type values
|
||||
#
|
||||
|
||||
class InterfaceTypes:
|
||||
"""
|
||||
Interface.type slugs
|
||||
"""
|
||||
# Virtual
|
||||
TYPE_VIRTUAL = 'virtual'
|
||||
TYPE_LAG = 'lag'
|
||||
|
||||
# Ethernet
|
||||
TYPE_100ME_FIXED = '100base-tx'
|
||||
TYPE_1GE_FIXED = '1000base-t'
|
||||
TYPE_1GE_GBIC = '1000base-x-gbic'
|
||||
TYPE_1GE_SFP = '1000base-x-sfp'
|
||||
TYPE_2GE_FIXED = '2.5gbase-t'
|
||||
TYPE_5GE_FIXED = '5gbase-t'
|
||||
TYPE_10GE_FIXED = '10gbase-t'
|
||||
TYPE_10GE_CX4 = '10gbase-cx4'
|
||||
TYPE_10GE_SFP_PLUS = '10gbase-x-sfpp'
|
||||
TYPE_10GE_XFP = '10gbase-x-xfp'
|
||||
TYPE_10GE_XENPAK = '10gbase-x-xenpak'
|
||||
TYPE_10GE_X2 = '10gbase-x-x2'
|
||||
TYPE_25GE_SFP28 = '25gbase-x-sfp28'
|
||||
TYPE_40GE_QSFP_PLUS = '40gbase-x-qsfpp'
|
||||
TYPE_50GE_QSFP28 = '50gbase-x-sfp28'
|
||||
TYPE_100GE_CFP = '100gbase-x-cfp'
|
||||
TYPE_100GE_CFP2 = '100gbase-x-cfp2'
|
||||
TYPE_100GE_CFP4 = '100gbase-x-cfp4'
|
||||
TYPE_100GE_CPAK = '100gbase-x-cpak'
|
||||
TYPE_100GE_QSFP28 = '100gbase-x-qsfp28'
|
||||
TYPE_200GE_CFP2 = '200gbase-x-cfp2'
|
||||
TYPE_200GE_QSFP56 = '200gbase-x-qsfp56'
|
||||
TYPE_400GE_QSFP_DD = '400gbase-x-qsfpdd'
|
||||
TYPE_400GE_OSFP = '400gbase-x-osfp'
|
||||
|
||||
# Wireless
|
||||
TYPE_80211A = 'ieee802.11a'
|
||||
TYPE_80211G = 'ieee802.11g'
|
||||
TYPE_80211N = 'ieee802.11n'
|
||||
TYPE_80211AC = 'ieee802.11ac'
|
||||
TYPE_80211AD = 'ieee802.11ad'
|
||||
|
||||
# Cellular
|
||||
TYPE_GSM = 'gsm'
|
||||
TYPE_CDMA = 'cdma'
|
||||
TYPE_LTE = 'lte'
|
||||
|
||||
# SONET
|
||||
TYPE_SONET_OC3 = 'sonet-oc3'
|
||||
TYPE_SONET_OC12 = 'sonet-oc12'
|
||||
TYPE_SONET_OC48 = 'sonet-oc48'
|
||||
TYPE_SONET_OC192 = 'sonet-oc192'
|
||||
TYPE_SONET_OC768 = 'sonet-oc768'
|
||||
TYPE_SONET_OC1920 = 'sonet-oc1920'
|
||||
TYPE_SONET_OC3840 = 'sonet-oc3840'
|
||||
|
||||
# Fibrechannel
|
||||
TYPE_1GFC_SFP = '1gfc-sfp'
|
||||
TYPE_2GFC_SFP = '2gfc-sfp'
|
||||
TYPE_4GFC_SFP = '4gfc-sfp'
|
||||
TYPE_8GFC_SFP_PLUS = '8gfc-sfpp'
|
||||
TYPE_16GFC_SFP_PLUS = '16gfc-sfpp'
|
||||
TYPE_32GFC_SFP28 = '32gfc-sfp28'
|
||||
TYPE_128GFC_QSFP28 = '128gfc-sfp28'
|
||||
|
||||
# InfiniBand
|
||||
TYPE_INFINIBAND_SDR = 'inifiband-sdr'
|
||||
TYPE_INFINIBAND_DDR = 'inifiband-ddr'
|
||||
TYPE_INFINIBAND_QDR = 'inifiband-qdr'
|
||||
TYPE_INFINIBAND_FDR10 = 'inifiband-fdr10'
|
||||
TYPE_INFINIBAND_FDR = 'inifiband-fdr'
|
||||
TYPE_INFINIBAND_EDR = 'inifiband-edr'
|
||||
TYPE_INFINIBAND_HDR = 'inifiband-hdr'
|
||||
TYPE_INFINIBAND_NDR = 'inifiband-ndr'
|
||||
TYPE_INFINIBAND_XDR = 'inifiband-xdr'
|
||||
|
||||
# Serial
|
||||
TYPE_T1 = 't1'
|
||||
TYPE_E1 = 'e1'
|
||||
TYPE_T3 = 't3'
|
||||
TYPE_E3 = 'e3'
|
||||
|
||||
# Stacking
|
||||
TYPE_STACKWISE = 'cisco-stackwise'
|
||||
TYPE_STACKWISE_PLUS = 'cisco-stackwise-plus'
|
||||
TYPE_FLEXSTACK = 'cisco-flexstack'
|
||||
TYPE_FLEXSTACK_PLUS = 'cisco-flexstack-plus'
|
||||
TYPE_JUNIPER_VCP = 'juniper-vcp'
|
||||
TYPE_SUMMITSTACK = 'extreme-summitstack'
|
||||
TYPE_SUMMITSTACK128 = 'extreme-summitstack-128'
|
||||
TYPE_SUMMITSTACK256 = 'extreme-summitstack-256'
|
||||
TYPE_SUMMITSTACK512 = 'extreme-summitstack-512'
|
||||
|
||||
# Other
|
||||
TYPE_OTHER = 'other'
|
||||
|
||||
TYPE_CHOICES = (
|
||||
(
|
||||
'Virtual interfaces',
|
||||
(
|
||||
(TYPE_VIRTUAL, 'Virtual'),
|
||||
(TYPE_LAG, 'Link Aggregation Group (LAG)'),
|
||||
),
|
||||
),
|
||||
(
|
||||
'Ethernet (fixed)',
|
||||
(
|
||||
(TYPE_100ME_FIXED, '100BASE-TX (10/100ME)'),
|
||||
(TYPE_1GE_FIXED, '1000BASE-T (1GE)'),
|
||||
(TYPE_2GE_FIXED, '2.5GBASE-T (2.5GE)'),
|
||||
(TYPE_5GE_FIXED, '5GBASE-T (5GE)'),
|
||||
(TYPE_10GE_FIXED, '10GBASE-T (10GE)'),
|
||||
(TYPE_10GE_CX4, '10GBASE-CX4 (10GE)'),
|
||||
)
|
||||
),
|
||||
(
|
||||
'Ethernet (modular)',
|
||||
(
|
||||
(TYPE_1GE_GBIC, 'GBIC (1GE)'),
|
||||
(TYPE_1GE_SFP, 'SFP (1GE)'),
|
||||
(TYPE_10GE_SFP_PLUS, 'SFP+ (10GE)'),
|
||||
(TYPE_10GE_XFP, 'XFP (10GE)'),
|
||||
(TYPE_10GE_XENPAK, 'XENPAK (10GE)'),
|
||||
(TYPE_10GE_X2, 'X2 (10GE)'),
|
||||
(TYPE_25GE_SFP28, 'SFP28 (25GE)'),
|
||||
(TYPE_40GE_QSFP_PLUS, 'QSFP+ (40GE)'),
|
||||
(TYPE_50GE_QSFP28, 'QSFP28 (50GE)'),
|
||||
(TYPE_100GE_CFP, 'CFP (100GE)'),
|
||||
(TYPE_100GE_CFP2, 'CFP2 (100GE)'),
|
||||
(TYPE_200GE_CFP2, 'CFP2 (200GE)'),
|
||||
(TYPE_100GE_CFP4, 'CFP4 (100GE)'),
|
||||
(TYPE_100GE_CPAK, 'Cisco CPAK (100GE)'),
|
||||
(TYPE_100GE_QSFP28, 'QSFP28 (100GE)'),
|
||||
(TYPE_200GE_QSFP56, 'QSFP56 (200GE)'),
|
||||
(TYPE_400GE_QSFP_DD, 'QSFP-DD (400GE)'),
|
||||
(TYPE_400GE_OSFP, 'OSFP (400GE)'),
|
||||
)
|
||||
),
|
||||
(
|
||||
'Wireless',
|
||||
(
|
||||
(TYPE_80211A, 'IEEE 802.11a'),
|
||||
(TYPE_80211G, 'IEEE 802.11b/g'),
|
||||
(TYPE_80211N, 'IEEE 802.11n'),
|
||||
(TYPE_80211AC, 'IEEE 802.11ac'),
|
||||
(TYPE_80211AD, 'IEEE 802.11ad'),
|
||||
)
|
||||
),
|
||||
(
|
||||
'Cellular',
|
||||
(
|
||||
(TYPE_GSM, 'GSM'),
|
||||
(TYPE_CDMA, 'CDMA'),
|
||||
(TYPE_LTE, 'LTE'),
|
||||
)
|
||||
),
|
||||
(
|
||||
'SONET',
|
||||
(
|
||||
(TYPE_SONET_OC3, 'OC-3/STM-1'),
|
||||
(TYPE_SONET_OC12, 'OC-12/STM-4'),
|
||||
(TYPE_SONET_OC48, 'OC-48/STM-16'),
|
||||
(TYPE_SONET_OC192, 'OC-192/STM-64'),
|
||||
(TYPE_SONET_OC768, 'OC-768/STM-256'),
|
||||
(TYPE_SONET_OC1920, 'OC-1920/STM-640'),
|
||||
(TYPE_SONET_OC3840, 'OC-3840/STM-1234'),
|
||||
)
|
||||
),
|
||||
(
|
||||
'FibreChannel',
|
||||
(
|
||||
(TYPE_1GFC_SFP, 'SFP (1GFC)'),
|
||||
(TYPE_2GFC_SFP, 'SFP (2GFC)'),
|
||||
(TYPE_4GFC_SFP, 'SFP (4GFC)'),
|
||||
(TYPE_8GFC_SFP_PLUS, 'SFP+ (8GFC)'),
|
||||
(TYPE_16GFC_SFP_PLUS, 'SFP+ (16GFC)'),
|
||||
(TYPE_32GFC_SFP28, 'SFP28 (32GFC)'),
|
||||
(TYPE_128GFC_QSFP28, 'QSFP28 (128GFC)'),
|
||||
)
|
||||
),
|
||||
(
|
||||
'InfiniBand',
|
||||
(
|
||||
(TYPE_INFINIBAND_SDR, 'SDR (2 Gbps)'),
|
||||
(TYPE_INFINIBAND_DDR, 'DDR (4 Gbps)'),
|
||||
(TYPE_INFINIBAND_QDR, 'QDR (8 Gbps)'),
|
||||
(TYPE_INFINIBAND_FDR10, 'FDR10 (10 Gbps)'),
|
||||
(TYPE_INFINIBAND_FDR, 'FDR (13.5 Gbps)'),
|
||||
(TYPE_INFINIBAND_EDR, 'EDR (25 Gbps)'),
|
||||
(TYPE_INFINIBAND_HDR, 'HDR (50 Gbps)'),
|
||||
(TYPE_INFINIBAND_NDR, 'NDR (100 Gbps)'),
|
||||
(TYPE_INFINIBAND_XDR, 'XDR (250 Gbps)'),
|
||||
)
|
||||
),
|
||||
(
|
||||
'Serial',
|
||||
(
|
||||
(TYPE_T1, 'T1 (1.544 Mbps)'),
|
||||
(TYPE_E1, 'E1 (2.048 Mbps)'),
|
||||
(TYPE_T3, 'T3 (45 Mbps)'),
|
||||
(TYPE_E3, 'E3 (34 Mbps)'),
|
||||
)
|
||||
),
|
||||
(
|
||||
'Stacking',
|
||||
(
|
||||
(TYPE_STACKWISE, 'Cisco StackWise'),
|
||||
(TYPE_STACKWISE_PLUS, 'Cisco StackWise Plus'),
|
||||
(TYPE_FLEXSTACK, 'Cisco FlexStack'),
|
||||
(TYPE_FLEXSTACK_PLUS, 'Cisco FlexStack Plus'),
|
||||
(TYPE_JUNIPER_VCP, 'Juniper VCP'),
|
||||
(TYPE_SUMMITSTACK, 'Extreme SummitStack'),
|
||||
(TYPE_SUMMITSTACK128, 'Extreme SummitStack-128'),
|
||||
(TYPE_SUMMITSTACK256, 'Extreme SummitStack-256'),
|
||||
(TYPE_SUMMITSTACK512, 'Extreme SummitStack-512'),
|
||||
)
|
||||
),
|
||||
(
|
||||
'Other',
|
||||
(
|
||||
(TYPE_OTHER, 'Other'),
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def slug_to_integer(cls, slug):
|
||||
"""
|
||||
Provide backward-compatible mapping of the type slug to integer.
|
||||
"""
|
||||
return {
|
||||
# Slug: integer
|
||||
cls.TYPE_VIRTUAL: IFACE_TYPE_VIRTUAL,
|
||||
cls.TYPE_LAG: IFACE_TYPE_LAG,
|
||||
cls.TYPE_100ME_FIXED: IFACE_TYPE_100ME_FIXED,
|
||||
cls.TYPE_1GE_FIXED: IFACE_TYPE_1GE_FIXED,
|
||||
cls.TYPE_1GE_GBIC: IFACE_TYPE_1GE_GBIC,
|
||||
cls.TYPE_1GE_SFP: IFACE_TYPE_1GE_SFP,
|
||||
cls.TYPE_2GE_FIXED: IFACE_TYPE_2GE_FIXED,
|
||||
cls.TYPE_5GE_FIXED: IFACE_TYPE_5GE_FIXED,
|
||||
cls.TYPE_10GE_FIXED: IFACE_TYPE_10GE_FIXED,
|
||||
cls.TYPE_10GE_CX4: IFACE_TYPE_10GE_CX4,
|
||||
cls.TYPE_10GE_SFP_PLUS: IFACE_TYPE_10GE_SFP_PLUS,
|
||||
cls.TYPE_10GE_XFP: IFACE_TYPE_10GE_XFP,
|
||||
cls.TYPE_10GE_XENPAK: IFACE_TYPE_10GE_XENPAK,
|
||||
cls.TYPE_10GE_X2: IFACE_TYPE_10GE_X2,
|
||||
cls.TYPE_25GE_SFP28: IFACE_TYPE_25GE_SFP28,
|
||||
cls.TYPE_40GE_QSFP_PLUS: IFACE_TYPE_40GE_QSFP_PLUS,
|
||||
cls.TYPE_50GE_QSFP28: IFACE_TYPE_50GE_QSFP28,
|
||||
cls.TYPE_100GE_CFP: IFACE_TYPE_100GE_CFP,
|
||||
cls.TYPE_100GE_CFP2: IFACE_TYPE_100GE_CFP2,
|
||||
cls.TYPE_100GE_CFP4: IFACE_TYPE_100GE_CFP4,
|
||||
cls.TYPE_100GE_CPAK: IFACE_TYPE_100GE_CPAK,
|
||||
cls.TYPE_100GE_QSFP28: IFACE_TYPE_100GE_QSFP28,
|
||||
cls.TYPE_200GE_CFP2: IFACE_TYPE_200GE_CFP2,
|
||||
cls.TYPE_200GE_QSFP56: IFACE_TYPE_200GE_QSFP56,
|
||||
cls.TYPE_400GE_QSFP_DD: IFACE_TYPE_400GE_QSFP_DD,
|
||||
cls.TYPE_80211A: IFACE_TYPE_80211A,
|
||||
cls.TYPE_80211G: IFACE_TYPE_80211G,
|
||||
cls.TYPE_80211N: IFACE_TYPE_80211N,
|
||||
cls.TYPE_80211AC: IFACE_TYPE_80211AC,
|
||||
cls.TYPE_80211AD: IFACE_TYPE_80211AD,
|
||||
cls.TYPE_GSM: IFACE_TYPE_GSM,
|
||||
cls.TYPE_CDMA: IFACE_TYPE_CDMA,
|
||||
cls.TYPE_LTE: IFACE_TYPE_LTE,
|
||||
cls.TYPE_SONET_OC3: IFACE_TYPE_SONET_OC3,
|
||||
cls.TYPE_SONET_OC12: IFACE_TYPE_SONET_OC12,
|
||||
cls.TYPE_SONET_OC48: IFACE_TYPE_SONET_OC48,
|
||||
cls.TYPE_SONET_OC192: IFACE_TYPE_SONET_OC192,
|
||||
cls.TYPE_SONET_OC768: IFACE_TYPE_SONET_OC768,
|
||||
cls.TYPE_SONET_OC1920: IFACE_TYPE_SONET_OC1920,
|
||||
cls.TYPE_SONET_OC3840: IFACE_TYPE_SONET_OC3840,
|
||||
cls.TYPE_1GFC_SFP: IFACE_TYPE_1GFC_SFP,
|
||||
cls.TYPE_2GFC_SFP: IFACE_TYPE_2GFC_SFP,
|
||||
cls.TYPE_4GFC_SFP: IFACE_TYPE_4GFC_SFP,
|
||||
cls.TYPE_8GFC_SFP_PLUS: IFACE_TYPE_8GFC_SFP_PLUS,
|
||||
cls.TYPE_16GFC_SFP_PLUS: IFACE_TYPE_16GFC_SFP_PLUS,
|
||||
cls.TYPE_32GFC_SFP28: IFACE_TYPE_32GFC_SFP28,
|
||||
cls.TYPE_128GFC_QSFP28: IFACE_TYPE_128GFC_QSFP28,
|
||||
cls.TYPE_INFINIBAND_SDR: IFACE_TYPE_INFINIBAND_SDR,
|
||||
cls.TYPE_INFINIBAND_DDR: IFACE_TYPE_INFINIBAND_DDR,
|
||||
cls.TYPE_INFINIBAND_QDR: IFACE_TYPE_INFINIBAND_QDR,
|
||||
cls.TYPE_INFINIBAND_FDR10: IFACE_TYPE_INFINIBAND_FDR10,
|
||||
cls.TYPE_INFINIBAND_FDR: IFACE_TYPE_INFINIBAND_FDR,
|
||||
cls.TYPE_INFINIBAND_EDR: IFACE_TYPE_INFINIBAND_EDR,
|
||||
cls.TYPE_INFINIBAND_HDR: IFACE_TYPE_INFINIBAND_HDR,
|
||||
cls.TYPE_INFINIBAND_NDR: IFACE_TYPE_INFINIBAND_NDR,
|
||||
cls.TYPE_INFINIBAND_XDR: IFACE_TYPE_INFINIBAND_XDR,
|
||||
cls.TYPE_T1: IFACE_TYPE_T1,
|
||||
cls.TYPE_E1: IFACE_TYPE_E1,
|
||||
cls.TYPE_T3: IFACE_TYPE_T3,
|
||||
cls.TYPE_E3: IFACE_TYPE_E3,
|
||||
cls.TYPE_STACKWISE: IFACE_TYPE_STACKWISE,
|
||||
cls.TYPE_STACKWISE_PLUS: IFACE_TYPE_STACKWISE_PLUS,
|
||||
cls.TYPE_FLEXSTACK: IFACE_TYPE_FLEXSTACK,
|
||||
cls.TYPE_FLEXSTACK_PLUS: IFACE_TYPE_FLEXSTACK_PLUS,
|
||||
cls.TYPE_JUNIPER_VCP: IFACE_TYPE_JUNIPER_VCP,
|
||||
cls.TYPE_SUMMITSTACK: IFACE_TYPE_SUMMITSTACK,
|
||||
cls.TYPE_SUMMITSTACK128: IFACE_TYPE_SUMMITSTACK128,
|
||||
cls.TYPE_SUMMITSTACK256: IFACE_TYPE_SUMMITSTACK256,
|
||||
cls.TYPE_SUMMITSTACK512: IFACE_TYPE_SUMMITSTACK512,
|
||||
}.get(slug)
|
||||
|
||||
|
||||
#
|
||||
# Port type values
|
||||
#
|
||||
|
||||
class PortTypes:
|
||||
"""
|
||||
FrontPort/RearPort.type slugs
|
||||
"""
|
||||
TYPE_8P8C = '8p8c'
|
||||
TYPE_110_PUNCH = '110-punch'
|
||||
TYPE_BNC = 'bnc'
|
||||
TYPE_ST = 'st'
|
||||
TYPE_SC = 'sc'
|
||||
TYPE_SC_APC = 'sc-apc'
|
||||
TYPE_FC = 'fc'
|
||||
TYPE_LC = 'lc'
|
||||
TYPE_LC_APC = 'lc-apc'
|
||||
TYPE_MTRJ = 'mtrj'
|
||||
TYPE_MPO = 'mpo'
|
||||
TYPE_LSH = 'lsh'
|
||||
TYPE_LSH_APC = 'lsh-apc'
|
||||
|
||||
TYPE_CHOICES = (
|
||||
(
|
||||
'Copper',
|
||||
(
|
||||
(TYPE_8P8C, '8P8C'),
|
||||
(TYPE_110_PUNCH, '110 Punch'),
|
||||
(TYPE_BNC, 'BNC'),
|
||||
),
|
||||
),
|
||||
(
|
||||
'Fiber Optic',
|
||||
(
|
||||
(TYPE_FC, 'FC'),
|
||||
(TYPE_LC, 'LC'),
|
||||
(TYPE_LC_APC, 'LC/APC'),
|
||||
(TYPE_LSH, 'LSH'),
|
||||
(TYPE_LSH_APC, 'LSH/APC'),
|
||||
(TYPE_MPO, 'MPO'),
|
||||
(TYPE_MTRJ, 'MTRJ'),
|
||||
(TYPE_SC, 'SC'),
|
||||
(TYPE_SC_APC, 'SC/APC'),
|
||||
(TYPE_ST, 'ST'),
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def slug_to_integer(cls, slug):
|
||||
"""
|
||||
Provide backward-compatible mapping of the type slug to integer.
|
||||
"""
|
||||
return {
|
||||
# Slug: integer
|
||||
cls.TYPE_8P8C: PORT_TYPE_8P8C,
|
||||
cls.TYPE_110_PUNCH: PORT_TYPE_8P8C,
|
||||
cls.TYPE_BNC: PORT_TYPE_BNC,
|
||||
cls.TYPE_ST: PORT_TYPE_ST,
|
||||
cls.TYPE_SC: PORT_TYPE_SC,
|
||||
cls.TYPE_SC_APC: PORT_TYPE_SC_APC,
|
||||
cls.TYPE_FC: PORT_TYPE_FC,
|
||||
cls.TYPE_LC: PORT_TYPE_LC,
|
||||
cls.TYPE_LC_APC: PORT_TYPE_LC_APC,
|
||||
cls.TYPE_MTRJ: PORT_TYPE_MTRJ,
|
||||
cls.TYPE_MPO: PORT_TYPE_MPO,
|
||||
cls.TYPE_LSH: PORT_TYPE_LSH,
|
||||
cls.TYPE_LSH_APC: PORT_TYPE_LSH_APC,
|
||||
}.get(slug)
|
@ -1,4 +1,3 @@
|
||||
|
||||
# Rack types
|
||||
RACK_TYPE_2POST = 100
|
||||
RACK_TYPE_4POST = 200
|
||||
@ -58,7 +57,10 @@ SUBDEVICE_ROLE_CHOICES = (
|
||||
(SUBDEVICE_ROLE_CHILD, 'Child'),
|
||||
)
|
||||
|
||||
# Interface types
|
||||
#
|
||||
# Numeric interface types
|
||||
#
|
||||
|
||||
# Virtual
|
||||
IFACE_TYPE_VIRTUAL = 0
|
||||
IFACE_TYPE_LAG = 200
|
||||
@ -114,15 +116,15 @@ IFACE_TYPE_16GFC_SFP_PLUS = 3160
|
||||
IFACE_TYPE_32GFC_SFP28 = 3320
|
||||
IFACE_TYPE_128GFC_QSFP28 = 3400
|
||||
# InfiniBand
|
||||
IFACE_FF_INFINIBAND_SDR = 7010
|
||||
IFACE_FF_INFINIBAND_DDR = 7020
|
||||
IFACE_FF_INFINIBAND_QDR = 7030
|
||||
IFACE_FF_INFINIBAND_FDR10 = 7040
|
||||
IFACE_FF_INFINIBAND_FDR = 7050
|
||||
IFACE_FF_INFINIBAND_EDR = 7060
|
||||
IFACE_FF_INFINIBAND_HDR = 7070
|
||||
IFACE_FF_INFINIBAND_NDR = 7080
|
||||
IFACE_FF_INFINIBAND_XDR = 7090
|
||||
IFACE_TYPE_INFINIBAND_SDR = 7010
|
||||
IFACE_TYPE_INFINIBAND_DDR = 7020
|
||||
IFACE_TYPE_INFINIBAND_QDR = 7030
|
||||
IFACE_TYPE_INFINIBAND_FDR10 = 7040
|
||||
IFACE_TYPE_INFINIBAND_FDR = 7050
|
||||
IFACE_TYPE_INFINIBAND_EDR = 7060
|
||||
IFACE_TYPE_INFINIBAND_HDR = 7070
|
||||
IFACE_TYPE_INFINIBAND_NDR = 7080
|
||||
IFACE_TYPE_INFINIBAND_XDR = 7090
|
||||
# Serial
|
||||
IFACE_TYPE_T1 = 4000
|
||||
IFACE_TYPE_E1 = 4010
|
||||
@ -229,15 +231,15 @@ IFACE_TYPE_CHOICES = [
|
||||
[
|
||||
'InfiniBand',
|
||||
[
|
||||
[IFACE_FF_INFINIBAND_SDR, 'SDR (2 Gbps)'],
|
||||
[IFACE_FF_INFINIBAND_DDR, 'DDR (4 Gbps)'],
|
||||
[IFACE_FF_INFINIBAND_QDR, 'QDR (8 Gbps)'],
|
||||
[IFACE_FF_INFINIBAND_FDR10, 'FDR10 (10 Gbps)'],
|
||||
[IFACE_FF_INFINIBAND_FDR, 'FDR (13.5 Gbps)'],
|
||||
[IFACE_FF_INFINIBAND_EDR, 'EDR (25 Gbps)'],
|
||||
[IFACE_FF_INFINIBAND_HDR, 'HDR (50 Gbps)'],
|
||||
[IFACE_FF_INFINIBAND_NDR, 'NDR (100 Gbps)'],
|
||||
[IFACE_FF_INFINIBAND_XDR, 'XDR (250 Gbps)'],
|
||||
[IFACE_TYPE_INFINIBAND_SDR, 'SDR (2 Gbps)'],
|
||||
[IFACE_TYPE_INFINIBAND_DDR, 'DDR (4 Gbps)'],
|
||||
[IFACE_TYPE_INFINIBAND_QDR, 'QDR (8 Gbps)'],
|
||||
[IFACE_TYPE_INFINIBAND_FDR10, 'FDR10 (10 Gbps)'],
|
||||
[IFACE_TYPE_INFINIBAND_FDR, 'FDR (13.5 Gbps)'],
|
||||
[IFACE_TYPE_INFINIBAND_EDR, 'EDR (25 Gbps)'],
|
||||
[IFACE_TYPE_INFINIBAND_HDR, 'HDR (50 Gbps)'],
|
||||
[IFACE_TYPE_INFINIBAND_NDR, 'NDR (100 Gbps)'],
|
||||
[IFACE_TYPE_INFINIBAND_XDR, 'XDR (250 Gbps)'],
|
||||
]
|
||||
],
|
||||
[
|
||||
@ -384,7 +386,8 @@ CONNECTION_STATUS_CHOICES = [
|
||||
|
||||
# Cable endpoint types
|
||||
CABLE_TERMINATION_TYPES = [
|
||||
'consoleport', 'consoleserverport', 'interface', 'poweroutlet', 'powerport', 'frontport', 'rearport', 'circuittermination',
|
||||
'consoleport', 'consoleserverport', 'interface', 'poweroutlet', 'powerport', 'frontport', 'rearport',
|
||||
'circuittermination',
|
||||
]
|
||||
|
||||
# Cable types
|
||||
|
@ -11,6 +11,7 @@ from utilities.filters import (
|
||||
TreeNodeMultipleChoiceFilter,
|
||||
)
|
||||
from virtualization.models import Cluster
|
||||
from .choices import *
|
||||
from .constants import *
|
||||
from .models import (
|
||||
Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||
@ -346,28 +347,28 @@ class ConsolePortTemplateFilter(DeviceTypeComponentFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = ConsolePortTemplate
|
||||
fields = ['id', 'name']
|
||||
fields = ['id', 'name', 'type']
|
||||
|
||||
|
||||
class ConsoleServerPortTemplateFilter(DeviceTypeComponentFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = ConsoleServerPortTemplate
|
||||
fields = ['id', 'name']
|
||||
fields = ['id', 'name', 'type']
|
||||
|
||||
|
||||
class PowerPortTemplateFilter(DeviceTypeComponentFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = PowerPortTemplate
|
||||
fields = ['id', 'name', 'maximum_draw', 'allocated_draw']
|
||||
fields = ['id', 'name', 'type', 'maximum_draw', 'allocated_draw']
|
||||
|
||||
|
||||
class PowerOutletTemplateFilter(DeviceTypeComponentFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = PowerOutletTemplate
|
||||
fields = ['id', 'name', 'feed_leg']
|
||||
fields = ['id', 'name', 'type', 'feed_leg']
|
||||
|
||||
|
||||
class InterfaceTemplateFilter(DeviceTypeComponentFilterSet):
|
||||
@ -641,6 +642,10 @@ class DeviceComponentFilterSet(django_filters.FilterSet):
|
||||
|
||||
|
||||
class ConsolePortFilter(DeviceComponentFilterSet):
|
||||
type = django_filters.MultipleChoiceFilter(
|
||||
choices=ConsolePortTypes.CHOICES,
|
||||
null_value=None
|
||||
)
|
||||
cabled = django_filters.BooleanFilter(
|
||||
field_name='cable',
|
||||
lookup_expr='isnull',
|
||||
@ -653,6 +658,10 @@ class ConsolePortFilter(DeviceComponentFilterSet):
|
||||
|
||||
|
||||
class ConsoleServerPortFilter(DeviceComponentFilterSet):
|
||||
type = django_filters.MultipleChoiceFilter(
|
||||
choices=ConsolePortTypes.CHOICES,
|
||||
null_value=None
|
||||
)
|
||||
cabled = django_filters.BooleanFilter(
|
||||
field_name='cable',
|
||||
lookup_expr='isnull',
|
||||
@ -665,6 +674,10 @@ class ConsoleServerPortFilter(DeviceComponentFilterSet):
|
||||
|
||||
|
||||
class PowerPortFilter(DeviceComponentFilterSet):
|
||||
type = django_filters.MultipleChoiceFilter(
|
||||
choices=PowerPortTypes.CHOICES,
|
||||
null_value=None
|
||||
)
|
||||
cabled = django_filters.BooleanFilter(
|
||||
field_name='cable',
|
||||
lookup_expr='isnull',
|
||||
@ -677,6 +690,10 @@ class PowerPortFilter(DeviceComponentFilterSet):
|
||||
|
||||
|
||||
class PowerOutletFilter(DeviceComponentFilterSet):
|
||||
type = django_filters.MultipleChoiceFilter(
|
||||
choices=PowerOutletTypes.CHOICES,
|
||||
null_value=None
|
||||
)
|
||||
cabled = django_filters.BooleanFilter(
|
||||
field_name='cable',
|
||||
lookup_expr='isnull',
|
||||
|
@ -23,9 +23,10 @@ from utilities.forms import (
|
||||
APISelect, APISelectMultiple, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm,
|
||||
BulkEditNullBooleanSelect, ChainedFieldsMixin, ChainedModelChoiceField, ColorSelect, CommentField, ComponentForm,
|
||||
ConfirmationForm, CSVChoiceField, ExpandableNameField, FilterChoiceField, FlexibleModelChoiceField, JSONField,
|
||||
SelectWithPK, SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple, BOOLEAN_WITH_BLANK_CHOICES
|
||||
SelectWithPK, SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple, BOOLEAN_WITH_BLANK_CHOICES,
|
||||
)
|
||||
from virtualization.models import Cluster, ClusterGroup, VirtualMachine
|
||||
from .choices import *
|
||||
from .constants import *
|
||||
from .models import (
|
||||
Cable, DeviceBay, DeviceBayTemplate, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate,
|
||||
@ -828,29 +829,17 @@ class DeviceTypeForm(BootstrapMixin, CustomFieldForm):
|
||||
}
|
||||
|
||||
|
||||
class DeviceTypeCSVForm(forms.ModelForm):
|
||||
class DeviceTypeImportForm(BootstrapMixin, forms.ModelForm):
|
||||
manufacturer = forms.ModelChoiceField(
|
||||
queryset=Manufacturer.objects.all(),
|
||||
required=True,
|
||||
to_field_name='name',
|
||||
help_text='Manufacturer name',
|
||||
error_messages={
|
||||
'invalid_choice': 'Manufacturer not found.',
|
||||
}
|
||||
)
|
||||
subdevice_role = CSVChoiceField(
|
||||
choices=SUBDEVICE_ROLE_CHOICES,
|
||||
required=False,
|
||||
help_text='Parent/child status'
|
||||
to_field_name='name'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = DeviceType
|
||||
fields = DeviceType.csv_headers
|
||||
help_texts = {
|
||||
'model': 'Model name',
|
||||
'slug': 'URL-friendly slug',
|
||||
}
|
||||
fields = [
|
||||
'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role',
|
||||
]
|
||||
|
||||
|
||||
class DeviceTypeBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
|
||||
@ -953,7 +942,7 @@ class ConsolePortTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||
class Meta:
|
||||
model = ConsolePortTemplate
|
||||
fields = [
|
||||
'device_type', 'name',
|
||||
'device_type', 'name', 'type',
|
||||
]
|
||||
widgets = {
|
||||
'device_type': forms.HiddenInput(),
|
||||
@ -964,6 +953,10 @@ class ConsolePortTemplateCreateForm(ComponentForm):
|
||||
name_pattern = ExpandableNameField(
|
||||
label='Name'
|
||||
)
|
||||
type = forms.ChoiceField(
|
||||
choices=ConsolePortTypes.CHOICES,
|
||||
widget=StaticSelect2()
|
||||
)
|
||||
|
||||
|
||||
class ConsoleServerPortTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||
@ -971,7 +964,7 @@ class ConsoleServerPortTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||
class Meta:
|
||||
model = ConsoleServerPortTemplate
|
||||
fields = [
|
||||
'device_type', 'name',
|
||||
'device_type', 'name', 'type',
|
||||
]
|
||||
widgets = {
|
||||
'device_type': forms.HiddenInput(),
|
||||
@ -982,6 +975,10 @@ class ConsoleServerPortTemplateCreateForm(ComponentForm):
|
||||
name_pattern = ExpandableNameField(
|
||||
label='Name'
|
||||
)
|
||||
type = forms.ChoiceField(
|
||||
choices=add_blank_choice(ConsolePortTypes.CHOICES),
|
||||
widget=StaticSelect2()
|
||||
)
|
||||
|
||||
|
||||
class PowerPortTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||
@ -989,7 +986,7 @@ class PowerPortTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||
class Meta:
|
||||
model = PowerPortTemplate
|
||||
fields = [
|
||||
'device_type', 'name', 'maximum_draw', 'allocated_draw',
|
||||
'device_type', 'name', 'type', 'maximum_draw', 'allocated_draw',
|
||||
]
|
||||
widgets = {
|
||||
'device_type': forms.HiddenInput(),
|
||||
@ -1000,6 +997,10 @@ class PowerPortTemplateCreateForm(ComponentForm):
|
||||
name_pattern = ExpandableNameField(
|
||||
label='Name'
|
||||
)
|
||||
type = forms.ChoiceField(
|
||||
choices=add_blank_choice(PowerPortTypes.CHOICES),
|
||||
required=False
|
||||
)
|
||||
maximum_draw = forms.IntegerField(
|
||||
min_value=1,
|
||||
required=False,
|
||||
@ -1017,7 +1018,7 @@ class PowerOutletTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||
class Meta:
|
||||
model = PowerOutletTemplate
|
||||
fields = [
|
||||
'device_type', 'name', 'power_port', 'feed_leg',
|
||||
'device_type', 'name', 'type', 'power_port', 'feed_leg',
|
||||
]
|
||||
widgets = {
|
||||
'device_type': forms.HiddenInput(),
|
||||
@ -1038,6 +1039,10 @@ class PowerOutletTemplateCreateForm(ComponentForm):
|
||||
name_pattern = ExpandableNameField(
|
||||
label='Name'
|
||||
)
|
||||
type = forms.ChoiceField(
|
||||
choices=add_blank_choice(PowerOutletTypes.CHOICES),
|
||||
required=False
|
||||
)
|
||||
power_port = forms.ModelChoiceField(
|
||||
queryset=PowerPortTemplate.objects.all(),
|
||||
required=False
|
||||
@ -1232,6 +1237,139 @@ class DeviceBayTemplateCreateForm(ComponentForm):
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Component template import forms
|
||||
#
|
||||
|
||||
class ComponentTemplateImportForm(BootstrapMixin, forms.ModelForm):
|
||||
|
||||
def __init__(self, device_type, data=None, *args, **kwargs):
|
||||
|
||||
# Must pass the parent DeviceType on form initialization
|
||||
data.update({
|
||||
'device_type': device_type.pk,
|
||||
})
|
||||
|
||||
super().__init__(data, *args, **kwargs)
|
||||
|
||||
def clean_device_type(self):
|
||||
|
||||
data = self.cleaned_data['device_type']
|
||||
|
||||
# Limit fields referencing other components to the parent DeviceType
|
||||
for field_name, field in self.fields.items():
|
||||
if isinstance(field, forms.ModelChoiceField) and field_name != 'device_type':
|
||||
field.queryset = field.queryset.filter(device_type=data)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
class ConsolePortTemplateImportForm(ComponentTemplateImportForm):
|
||||
|
||||
class Meta:
|
||||
model = ConsolePortTemplate
|
||||
fields = [
|
||||
'device_type', 'name', 'type',
|
||||
]
|
||||
|
||||
|
||||
class ConsoleServerPortTemplateImportForm(ComponentTemplateImportForm):
|
||||
|
||||
class Meta:
|
||||
model = ConsoleServerPortTemplate
|
||||
fields = [
|
||||
'device_type', 'name', 'type',
|
||||
]
|
||||
|
||||
|
||||
class PowerPortTemplateImportForm(ComponentTemplateImportForm):
|
||||
|
||||
class Meta:
|
||||
model = PowerPortTemplate
|
||||
fields = [
|
||||
'device_type', 'name', 'type', 'maximum_draw', 'allocated_draw',
|
||||
]
|
||||
|
||||
|
||||
class PowerOutletTemplateImportForm(ComponentTemplateImportForm):
|
||||
power_port = forms.ModelChoiceField(
|
||||
queryset=PowerPortTemplate.objects.all(),
|
||||
to_field_name='name',
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = PowerOutletTemplate
|
||||
fields = [
|
||||
'device_type', 'name', 'type', 'power_port', 'feed_leg',
|
||||
]
|
||||
|
||||
|
||||
class InterfaceTemplateImportForm(ComponentTemplateImportForm):
|
||||
type = forms.ChoiceField(
|
||||
choices=InterfaceTypes.TYPE_CHOICES
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = InterfaceTemplate
|
||||
fields = [
|
||||
'device_type', 'name', 'type', 'mgmt_only',
|
||||
]
|
||||
|
||||
def clean_type(self):
|
||||
# Convert slug value to field integer value
|
||||
slug = self.cleaned_data['type']
|
||||
return InterfaceTypes.slug_to_integer(slug)
|
||||
|
||||
|
||||
class FrontPortTemplateImportForm(ComponentTemplateImportForm):
|
||||
type = forms.ChoiceField(
|
||||
choices=PortTypes.TYPE_CHOICES
|
||||
)
|
||||
rear_port = forms.ModelChoiceField(
|
||||
queryset=RearPortTemplate.objects.all(),
|
||||
to_field_name='name',
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = FrontPortTemplate
|
||||
fields = [
|
||||
'device_type', 'name', 'type', 'rear_port', 'rear_port_position',
|
||||
]
|
||||
|
||||
def clean_type(self):
|
||||
# Convert slug value to field integer value
|
||||
slug = self.cleaned_data['type']
|
||||
return PortTypes.slug_to_integer(slug)
|
||||
|
||||
|
||||
class RearPortTemplateImportForm(ComponentTemplateImportForm):
|
||||
type = forms.ChoiceField(
|
||||
choices=PortTypes.TYPE_CHOICES
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = RearPortTemplate
|
||||
fields = [
|
||||
'device_type', 'name', 'type', 'positions',
|
||||
]
|
||||
|
||||
def clean_type(self):
|
||||
# Convert slug value to field integer value
|
||||
slug = self.cleaned_data['type']
|
||||
return PortTypes.slug_to_integer(slug)
|
||||
|
||||
|
||||
class DeviceBayTemplateImportForm(ComponentTemplateImportForm):
|
||||
|
||||
class Meta:
|
||||
model = DeviceBayTemplate
|
||||
fields = [
|
||||
'device_type', 'name',
|
||||
]
|
||||
|
||||
|
||||
#
|
||||
# Device roles
|
||||
#
|
||||
@ -1933,7 +2071,7 @@ class ConsolePortForm(BootstrapMixin, forms.ModelForm):
|
||||
class Meta:
|
||||
model = ConsolePort
|
||||
fields = [
|
||||
'device', 'name', 'description', 'tags',
|
||||
'device', 'name', 'type', 'description', 'tags',
|
||||
]
|
||||
widgets = {
|
||||
'device': forms.HiddenInput(),
|
||||
@ -1944,6 +2082,11 @@ class ConsolePortCreateForm(ComponentForm):
|
||||
name_pattern = ExpandableNameField(
|
||||
label='Name'
|
||||
)
|
||||
type = forms.ChoiceField(
|
||||
choices=add_blank_choice(ConsolePortTypes.CHOICES),
|
||||
required=False,
|
||||
widget=StaticSelect2()
|
||||
)
|
||||
description = forms.CharField(
|
||||
max_length=100,
|
||||
required=False
|
||||
@ -1980,7 +2123,7 @@ class ConsoleServerPortForm(BootstrapMixin, forms.ModelForm):
|
||||
class Meta:
|
||||
model = ConsoleServerPort
|
||||
fields = [
|
||||
'device', 'name', 'description', 'tags',
|
||||
'device', 'name', 'type', 'description', 'tags',
|
||||
]
|
||||
widgets = {
|
||||
'device': forms.HiddenInput(),
|
||||
@ -1991,6 +2134,11 @@ class ConsoleServerPortCreateForm(ComponentForm):
|
||||
name_pattern = ExpandableNameField(
|
||||
label='Name'
|
||||
)
|
||||
type = forms.ChoiceField(
|
||||
choices=add_blank_choice(ConsolePortTypes.CHOICES),
|
||||
required=False,
|
||||
widget=StaticSelect2()
|
||||
)
|
||||
description = forms.CharField(
|
||||
max_length=100,
|
||||
required=False
|
||||
@ -2005,6 +2153,11 @@ class ConsoleServerPortBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditF
|
||||
queryset=ConsoleServerPort.objects.all(),
|
||||
widget=forms.MultipleHiddenInput()
|
||||
)
|
||||
type = forms.ChoiceField(
|
||||
choices=add_blank_choice(ConsolePortTypes.CHOICES),
|
||||
required=False,
|
||||
widget=StaticSelect2()
|
||||
)
|
||||
description = forms.CharField(
|
||||
max_length=100,
|
||||
required=False
|
||||
@ -2057,7 +2210,7 @@ class PowerPortForm(BootstrapMixin, forms.ModelForm):
|
||||
class Meta:
|
||||
model = PowerPort
|
||||
fields = [
|
||||
'device', 'name', 'maximum_draw', 'allocated_draw', 'description', 'tags',
|
||||
'device', 'name', 'type', 'maximum_draw', 'allocated_draw', 'description', 'tags',
|
||||
]
|
||||
widgets = {
|
||||
'device': forms.HiddenInput(),
|
||||
@ -2068,6 +2221,11 @@ class PowerPortCreateForm(ComponentForm):
|
||||
name_pattern = ExpandableNameField(
|
||||
label='Name'
|
||||
)
|
||||
type = forms.ChoiceField(
|
||||
choices=add_blank_choice(PowerPortTypes.CHOICES),
|
||||
required=False,
|
||||
widget=StaticSelect2()
|
||||
)
|
||||
maximum_draw = forms.IntegerField(
|
||||
min_value=1,
|
||||
required=False,
|
||||
@ -2118,7 +2276,7 @@ class PowerOutletForm(BootstrapMixin, forms.ModelForm):
|
||||
class Meta:
|
||||
model = PowerOutlet
|
||||
fields = [
|
||||
'device', 'name', 'power_port', 'feed_leg', 'description', 'tags',
|
||||
'device', 'name', 'type', 'power_port', 'feed_leg', 'description', 'tags',
|
||||
]
|
||||
widgets = {
|
||||
'device': forms.HiddenInput(),
|
||||
@ -2138,6 +2296,11 @@ class PowerOutletCreateForm(ComponentForm):
|
||||
name_pattern = ExpandableNameField(
|
||||
label='Name'
|
||||
)
|
||||
type = forms.ChoiceField(
|
||||
choices=add_blank_choice(PowerOutletTypes.CHOICES),
|
||||
required=False,
|
||||
widget=StaticSelect2()
|
||||
)
|
||||
power_port = forms.ModelChoiceField(
|
||||
queryset=PowerPort.objects.all(),
|
||||
required=False
|
||||
@ -2217,6 +2380,10 @@ class PowerOutletBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
|
||||
queryset=PowerOutlet.objects.all(),
|
||||
widget=forms.MultipleHiddenInput()
|
||||
)
|
||||
type = forms.ChoiceField(
|
||||
choices=PowerOutletTypes.CHOICES,
|
||||
required=False
|
||||
)
|
||||
feed_leg = forms.ChoiceField(
|
||||
choices=add_blank_choice(POWERFEED_LEG_CHOICES),
|
||||
required=False,
|
||||
@ -2232,7 +2399,7 @@ class PowerOutletBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
|
||||
|
||||
class Meta:
|
||||
nullable_fields = [
|
||||
'feed_leg', 'power_port', 'description',
|
||||
'type', 'feed_leg', 'power_port', 'description',
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
33
netbox/dcim/migrations/0076_console_port_types.py
Normal file
33
netbox/dcim/migrations/0076_console_port_types.py
Normal file
@ -0,0 +1,33 @@
|
||||
# Generated by Django 2.2.6 on 2019-10-30 17:41
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0075_cable_devices'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='consoleport',
|
||||
name='type',
|
||||
field=models.CharField(blank=True, max_length=50),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='consoleporttemplate',
|
||||
name='type',
|
||||
field=models.CharField(blank=True, max_length=50),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='consoleserverport',
|
||||
name='type',
|
||||
field=models.CharField(blank=True, max_length=50),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='consoleserverporttemplate',
|
||||
name='type',
|
||||
field=models.CharField(blank=True, max_length=50),
|
||||
),
|
||||
]
|
33
netbox/dcim/migrations/0077_power_types.py
Normal file
33
netbox/dcim/migrations/0077_power_types.py
Normal file
@ -0,0 +1,33 @@
|
||||
# Generated by Django 2.2.6 on 2019-11-06 19:48
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0076_console_port_types'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='poweroutlet',
|
||||
name='type',
|
||||
field=models.CharField(blank=True, max_length=50),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='poweroutlettemplate',
|
||||
name='type',
|
||||
field=models.CharField(blank=True, max_length=50),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='powerport',
|
||||
name='type',
|
||||
field=models.CharField(blank=True, max_length=50),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='powerporttemplate',
|
||||
name='type',
|
||||
field=models.CharField(blank=True, max_length=50),
|
||||
),
|
||||
]
|
@ -20,6 +20,7 @@ from utilities.fields import ColorField
|
||||
from utilities.managers import NaturalOrderingManager
|
||||
from utilities.models import ChangeLoggedModel
|
||||
from utilities.utils import serialize_object, to_meters
|
||||
from .choices import *
|
||||
from .constants import *
|
||||
from .exceptions import LoopDetected
|
||||
from .fields import ASNField, MACAddressField
|
||||
@ -1014,6 +1015,11 @@ class ConsolePortTemplate(ComponentTemplateModel):
|
||||
name = models.CharField(
|
||||
max_length=50
|
||||
)
|
||||
type = models.CharField(
|
||||
max_length=50,
|
||||
choices=ConsolePortTypes.CHOICES,
|
||||
blank=True
|
||||
)
|
||||
|
||||
objects = NaturalOrderingManager()
|
||||
|
||||
@ -1027,7 +1033,8 @@ class ConsolePortTemplate(ComponentTemplateModel):
|
||||
def instantiate(self, device):
|
||||
return ConsolePort(
|
||||
device=device,
|
||||
name=self.name
|
||||
name=self.name,
|
||||
type=self.type
|
||||
)
|
||||
|
||||
|
||||
@ -1043,6 +1050,11 @@ class ConsoleServerPortTemplate(ComponentTemplateModel):
|
||||
name = models.CharField(
|
||||
max_length=50
|
||||
)
|
||||
type = models.CharField(
|
||||
max_length=50,
|
||||
choices=ConsolePortTypes.CHOICES,
|
||||
blank=True
|
||||
)
|
||||
|
||||
objects = NaturalOrderingManager()
|
||||
|
||||
@ -1056,7 +1068,8 @@ class ConsoleServerPortTemplate(ComponentTemplateModel):
|
||||
def instantiate(self, device):
|
||||
return ConsoleServerPort(
|
||||
device=device,
|
||||
name=self.name
|
||||
name=self.name,
|
||||
type=self.type
|
||||
)
|
||||
|
||||
|
||||
@ -1072,6 +1085,11 @@ class PowerPortTemplate(ComponentTemplateModel):
|
||||
name = models.CharField(
|
||||
max_length=50
|
||||
)
|
||||
type = models.CharField(
|
||||
max_length=50,
|
||||
choices=PowerPortTypes.CHOICES,
|
||||
blank=True
|
||||
)
|
||||
maximum_draw = models.PositiveSmallIntegerField(
|
||||
blank=True,
|
||||
null=True,
|
||||
@ -1115,6 +1133,11 @@ class PowerOutletTemplate(ComponentTemplateModel):
|
||||
name = models.CharField(
|
||||
max_length=50
|
||||
)
|
||||
type = models.CharField(
|
||||
max_length=50,
|
||||
choices=PowerOutletTypes.CHOICES,
|
||||
blank=True
|
||||
)
|
||||
power_port = models.ForeignKey(
|
||||
to='dcim.PowerPortTemplate',
|
||||
on_delete=models.SET_NULL,
|
||||
@ -1189,22 +1212,6 @@ class InterfaceTemplate(ComponentTemplateModel):
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
# TODO: Remove in v2.7
|
||||
@property
|
||||
def form_factor(self):
|
||||
"""
|
||||
Backward-compatibility for form_factor
|
||||
"""
|
||||
return self.type
|
||||
|
||||
# TODO: Remove in v2.7
|
||||
@form_factor.setter
|
||||
def form_factor(self, value):
|
||||
"""
|
||||
Backward-compatibility for form_factor
|
||||
"""
|
||||
self.type = value
|
||||
|
||||
def instantiate(self, device):
|
||||
return Interface(
|
||||
device=device,
|
||||
@ -1862,6 +1869,11 @@ class ConsolePort(CableTermination, ComponentModel):
|
||||
name = models.CharField(
|
||||
max_length=50
|
||||
)
|
||||
type = models.CharField(
|
||||
max_length=50,
|
||||
choices=ConsolePortTypes.CHOICES,
|
||||
blank=True
|
||||
)
|
||||
connected_endpoint = models.OneToOneField(
|
||||
to='dcim.ConsoleServerPort',
|
||||
on_delete=models.SET_NULL,
|
||||
@ -1877,7 +1889,7 @@ class ConsolePort(CableTermination, ComponentModel):
|
||||
objects = NaturalOrderingManager()
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
csv_headers = ['device', 'name', 'description']
|
||||
csv_headers = ['device', 'name', 'type', 'description']
|
||||
|
||||
class Meta:
|
||||
ordering = ['device', 'name']
|
||||
@ -1893,6 +1905,7 @@ class ConsolePort(CableTermination, ComponentModel):
|
||||
return (
|
||||
self.device.identifier,
|
||||
self.name,
|
||||
self.type,
|
||||
self.description,
|
||||
)
|
||||
|
||||
@ -1913,6 +1926,11 @@ class ConsoleServerPort(CableTermination, ComponentModel):
|
||||
name = models.CharField(
|
||||
max_length=50
|
||||
)
|
||||
type = models.CharField(
|
||||
max_length=50,
|
||||
choices=ConsolePortTypes.CHOICES,
|
||||
blank=True
|
||||
)
|
||||
connection_status = models.NullBooleanField(
|
||||
choices=CONNECTION_STATUS_CHOICES,
|
||||
blank=True
|
||||
@ -1921,7 +1939,7 @@ class ConsoleServerPort(CableTermination, ComponentModel):
|
||||
objects = NaturalOrderingManager()
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
csv_headers = ['device', 'name', 'description']
|
||||
csv_headers = ['device', 'name', 'type', 'description']
|
||||
|
||||
class Meta:
|
||||
unique_together = ['device', 'name']
|
||||
@ -1936,6 +1954,7 @@ class ConsoleServerPort(CableTermination, ComponentModel):
|
||||
return (
|
||||
self.device.identifier,
|
||||
self.name,
|
||||
self.type,
|
||||
self.description,
|
||||
)
|
||||
|
||||
@ -1956,6 +1975,11 @@ class PowerPort(CableTermination, ComponentModel):
|
||||
name = models.CharField(
|
||||
max_length=50
|
||||
)
|
||||
type = models.CharField(
|
||||
max_length=50,
|
||||
choices=PowerPortTypes.CHOICES,
|
||||
blank=True
|
||||
)
|
||||
maximum_draw = models.PositiveSmallIntegerField(
|
||||
blank=True,
|
||||
null=True,
|
||||
@ -1990,7 +2014,7 @@ class PowerPort(CableTermination, ComponentModel):
|
||||
objects = NaturalOrderingManager()
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
csv_headers = ['device', 'name', 'maximum_draw', 'allocated_draw', 'description']
|
||||
csv_headers = ['device', 'name', 'type', 'maximum_draw', 'allocated_draw', 'description']
|
||||
|
||||
class Meta:
|
||||
ordering = ['device', 'name']
|
||||
@ -2006,6 +2030,7 @@ class PowerPort(CableTermination, ComponentModel):
|
||||
return (
|
||||
self.device.identifier,
|
||||
self.name,
|
||||
self.get_type_display(),
|
||||
self.maximum_draw,
|
||||
self.allocated_draw,
|
||||
self.description,
|
||||
@ -2093,6 +2118,11 @@ class PowerOutlet(CableTermination, ComponentModel):
|
||||
name = models.CharField(
|
||||
max_length=50
|
||||
)
|
||||
type = models.CharField(
|
||||
max_length=50,
|
||||
choices=PowerOutletTypes.CHOICES,
|
||||
blank=True
|
||||
)
|
||||
power_port = models.ForeignKey(
|
||||
to='dcim.PowerPort',
|
||||
on_delete=models.SET_NULL,
|
||||
@ -2114,7 +2144,7 @@ class PowerOutlet(CableTermination, ComponentModel):
|
||||
objects = NaturalOrderingManager()
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
csv_headers = ['device', 'name', 'power_port', 'feed_leg', 'description']
|
||||
csv_headers = ['device', 'name', 'type', 'power_port', 'feed_leg', 'description']
|
||||
|
||||
class Meta:
|
||||
unique_together = ['device', 'name']
|
||||
@ -2129,6 +2159,7 @@ class PowerOutlet(CableTermination, ComponentModel):
|
||||
return (
|
||||
self.device.identifier,
|
||||
self.name,
|
||||
self.get_type_display(),
|
||||
self.power_port.name if self.power_port else None,
|
||||
self.get_feed_leg_display(),
|
||||
self.description,
|
||||
@ -2350,22 +2381,6 @@ class Interface(CableTermination, ComponentModel):
|
||||
object_data=serialize_object(self)
|
||||
)
|
||||
|
||||
# TODO: Remove in v2.7
|
||||
@property
|
||||
def form_factor(self):
|
||||
"""
|
||||
Backward-compatibility for form_factor
|
||||
"""
|
||||
return self.type
|
||||
|
||||
# TODO: Remove in v2.7
|
||||
@form_factor.setter
|
||||
def form_factor(self, value):
|
||||
"""
|
||||
Backward-compatibility for form_factor
|
||||
"""
|
||||
self.type = value
|
||||
|
||||
@property
|
||||
def connected_endpoint(self):
|
||||
if self._connected_interface:
|
||||
|
@ -422,7 +422,7 @@ class ConsolePortTemplateTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = ConsolePortTemplate
|
||||
fields = ('pk', 'name', 'actions')
|
||||
fields = ('pk', 'name', 'type', 'actions')
|
||||
empty_text = "None"
|
||||
|
||||
|
||||
@ -468,7 +468,7 @@ class PowerPortTemplateTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = PowerPortTemplate
|
||||
fields = ('pk', 'name', 'maximum_draw', 'allocated_draw', 'actions')
|
||||
fields = ('pk', 'name', 'type', 'maximum_draw', 'allocated_draw', 'actions')
|
||||
empty_text = "None"
|
||||
|
||||
|
||||
@ -491,7 +491,7 @@ class PowerOutletTemplateTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = PowerOutletTemplate
|
||||
fields = ('pk', 'name', 'power_port', 'feed_leg', 'actions')
|
||||
fields = ('pk', 'name', 'type', 'power_port', 'feed_leg', 'actions')
|
||||
empty_text = "None"
|
||||
|
||||
|
||||
@ -711,7 +711,7 @@ class ConsolePortTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = ConsolePort
|
||||
fields = ('name',)
|
||||
fields = ('name', 'type')
|
||||
|
||||
|
||||
class ConsoleServerPortTable(BaseTable):
|
||||
@ -725,14 +725,14 @@ class PowerPortTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = PowerPort
|
||||
fields = ('name',)
|
||||
fields = ('name', 'type')
|
||||
|
||||
|
||||
class PowerOutletTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = PowerOutlet
|
||||
fields = ('name', 'description')
|
||||
fields = ('name', 'type', 'description')
|
||||
|
||||
|
||||
class InterfaceTable(BaseTable):
|
||||
|
@ -3,10 +3,12 @@ import urllib.parse
|
||||
from django.test import Client, TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from dcim.constants import CABLE_TYPE_CAT6, IFACE_TYPE_1GE_FIXED
|
||||
from dcim.choices import *
|
||||
from dcim.constants import *
|
||||
from dcim.models import (
|
||||
Cable, Device, DeviceRole, DeviceType, Interface, InventoryItem, Manufacturer, Platform, Rack, RackGroup,
|
||||
RackReservation, RackRole, Site, Region, VirtualChassis,
|
||||
Cable, ConsolePortTemplate, ConsoleServerPortTemplate, Device, DeviceBayTemplate, DeviceRole, DeviceType,
|
||||
FrontPortTemplate, Interface, InterfaceTemplate, InventoryItem, Manufacturer, Platform, PowerPortTemplate,
|
||||
PowerOutletTemplate, Rack, RackGroup, RackReservation, RackRole, RearPortTemplate, Site, Region, VirtualChassis,
|
||||
)
|
||||
from utilities.testing import create_test_user
|
||||
|
||||
@ -221,6 +223,148 @@ class DeviceTypeTestCase(TestCase):
|
||||
response = self.client.get(devicetype.get_absolute_url())
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_devicetype_import(self):
|
||||
|
||||
IMPORT_DATA = """
|
||||
manufacturer: Generic
|
||||
model: TEST-1000
|
||||
slug: test-1000
|
||||
u_height: 2
|
||||
console-ports:
|
||||
- name: Console Port 1
|
||||
type: de-9
|
||||
- name: Console Port 2
|
||||
type: de-9
|
||||
- name: Console Port 3
|
||||
type: de-9
|
||||
console-server-ports:
|
||||
- name: Console Server Port 1
|
||||
type: rj-45
|
||||
- name: Console Server Port 2
|
||||
type: rj-45
|
||||
- name: Console Server Port 3
|
||||
type: rj-45
|
||||
power-ports:
|
||||
- name: Power Port 1
|
||||
type: iec-60320-c14
|
||||
- name: Power Port 2
|
||||
type: iec-60320-c14
|
||||
- name: Power Port 3
|
||||
type: iec-60320-c14
|
||||
power-outlets:
|
||||
- name: Power Outlet 1
|
||||
type: iec-60320-c13
|
||||
power_port: Power Port 1
|
||||
feed_leg: 1
|
||||
- name: Power Outlet 2
|
||||
type: iec-60320-c13
|
||||
power_port: Power Port 1
|
||||
feed_leg: 1
|
||||
- name: Power Outlet 3
|
||||
type: iec-60320-c13
|
||||
power_port: Power Port 1
|
||||
feed_leg: 1
|
||||
interfaces:
|
||||
- name: Interface 1
|
||||
type: 1000base-t
|
||||
mgmt_only: true
|
||||
- name: Interface 2
|
||||
type: 1000base-t
|
||||
- name: Interface 3
|
||||
type: 1000base-t
|
||||
rear-ports:
|
||||
- name: Rear Port 1
|
||||
type: 8p8c
|
||||
- name: Rear Port 2
|
||||
type: 8p8c
|
||||
- name: Rear Port 3
|
||||
type: 8p8c
|
||||
front-ports:
|
||||
- name: Front Port 1
|
||||
type: 8p8c
|
||||
rear_port: Rear Port 1
|
||||
- name: Front Port 2
|
||||
type: 8p8c
|
||||
rear_port: Rear Port 2
|
||||
- name: Front Port 3
|
||||
type: 8p8c
|
||||
rear_port: Rear Port 3
|
||||
device-bays:
|
||||
- name: Device Bay 1
|
||||
- name: Device Bay 2
|
||||
- name: Device Bay 3
|
||||
"""
|
||||
|
||||
# Create the manufacturer
|
||||
Manufacturer(name='Generic', slug='generic').save()
|
||||
|
||||
# Authenticate as user with necessary permissions
|
||||
user = create_test_user(username='testuser2', permissions=[
|
||||
'dcim.view_devicetype',
|
||||
'dcim.add_devicetype',
|
||||
'dcim.add_consoleporttemplate',
|
||||
'dcim.add_consoleserverporttemplate',
|
||||
'dcim.add_powerporttemplate',
|
||||
'dcim.add_poweroutlettemplate',
|
||||
'dcim.add_interfacetemplate',
|
||||
'dcim.add_frontporttemplate',
|
||||
'dcim.add_rearporttemplate',
|
||||
'dcim.add_devicebaytemplate',
|
||||
])
|
||||
self.client.force_login(user)
|
||||
|
||||
form_data = {
|
||||
'data': IMPORT_DATA,
|
||||
'format': 'yaml'
|
||||
}
|
||||
response = self.client.post(reverse('dcim:devicetype_import'), data=form_data, follow=True)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
dt = DeviceType.objects.get(model='TEST-1000')
|
||||
|
||||
# Verify all of the components were created
|
||||
self.assertEqual(dt.consoleport_templates.count(), 3)
|
||||
cp1 = ConsolePortTemplate.objects.first()
|
||||
self.assertEqual(cp1.name, 'Console Port 1')
|
||||
self.assertEqual(cp1.type, ConsolePortTypes.TYPE_DE9)
|
||||
|
||||
self.assertEqual(dt.consoleserverport_templates.count(), 3)
|
||||
csp1 = ConsoleServerPortTemplate.objects.first()
|
||||
self.assertEqual(csp1.name, 'Console Server Port 1')
|
||||
self.assertEqual(csp1.type, ConsolePortTypes.TYPE_RJ45)
|
||||
|
||||
self.assertEqual(dt.powerport_templates.count(), 3)
|
||||
pp1 = PowerPortTemplate.objects.first()
|
||||
self.assertEqual(pp1.name, 'Power Port 1')
|
||||
self.assertEqual(pp1.type, PowerPortTypes.TYPE_IEC_C14)
|
||||
|
||||
self.assertEqual(dt.poweroutlet_templates.count(), 3)
|
||||
po1 = PowerOutletTemplate.objects.first()
|
||||
self.assertEqual(po1.name, 'Power Outlet 1')
|
||||
self.assertEqual(po1.type, PowerOutletTypes.TYPE_IEC_C13)
|
||||
self.assertEqual(po1.power_port, pp1)
|
||||
self.assertEqual(po1.feed_leg, POWERFEED_LEG_A)
|
||||
|
||||
self.assertEqual(dt.interface_templates.count(), 3)
|
||||
iface1 = InterfaceTemplate.objects.first()
|
||||
self.assertEqual(iface1.name, 'Interface 1')
|
||||
self.assertEqual(iface1.type, IFACE_TYPE_1GE_FIXED)
|
||||
self.assertTrue(iface1.mgmt_only)
|
||||
|
||||
self.assertEqual(dt.rearport_templates.count(), 3)
|
||||
rp1 = RearPortTemplate.objects.first()
|
||||
self.assertEqual(rp1.name, 'Rear Port 1')
|
||||
|
||||
self.assertEqual(dt.frontport_templates.count(), 3)
|
||||
fp1 = FrontPortTemplate.objects.first()
|
||||
self.assertEqual(fp1.name, 'Front Port 1')
|
||||
self.assertEqual(fp1.rear_port, rp1)
|
||||
self.assertEqual(fp1.rear_port_position, 1)
|
||||
|
||||
self.assertEqual(dt.device_bay_templates.count(), 3)
|
||||
db1 = DeviceBayTemplate.objects.first()
|
||||
self.assertEqual(db1.name, 'Device Bay 1')
|
||||
|
||||
|
||||
class DeviceRoleTestCase(TestCase):
|
||||
|
||||
|
@ -82,7 +82,7 @@ urlpatterns = [
|
||||
# Device types
|
||||
path(r'device-types/', views.DeviceTypeListView.as_view(), name='devicetype_list'),
|
||||
path(r'device-types/add/', views.DeviceTypeCreateView.as_view(), name='devicetype_add'),
|
||||
path(r'device-types/import/', views.DeviceTypeBulkImportView.as_view(), name='devicetype_import'),
|
||||
path(r'device-types/import/', views.DeviceTypeImportView.as_view(), name='devicetype_import'),
|
||||
path(r'device-types/edit/', views.DeviceTypeBulkEditView.as_view(), name='devicetype_bulk_edit'),
|
||||
path(r'device-types/delete/', views.DeviceTypeBulkDeleteView.as_view(), name='devicetype_bulk_delete'),
|
||||
path(r'device-types/<int:pk>/', views.DeviceTypeView.as_view(), name='devicetype'),
|
||||
|
@ -1,3 +1,4 @@
|
||||
from collections import OrderedDict
|
||||
import re
|
||||
|
||||
from django.conf import settings
|
||||
@ -17,7 +18,7 @@ from django.views.generic import View
|
||||
|
||||
from circuits.models import Circuit
|
||||
from extras.constants import GRAPH_TYPE_DEVICE, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE
|
||||
from extras.models import Graph, TopologyMap
|
||||
from extras.models import Graph
|
||||
from extras.views import ObjectConfigContextView
|
||||
from ipam.models import Prefix, VLAN
|
||||
from ipam.tables import InterfaceIPAddressTable, InterfaceVLANTable
|
||||
@ -26,7 +27,7 @@ from utilities.paginator import EnhancedPaginator
|
||||
from utilities.utils import csv_format
|
||||
from utilities.views import (
|
||||
BulkComponentCreateView, BulkDeleteView, BulkEditView, BulkImportView, ComponentCreateView, GetReturnURLMixin,
|
||||
ObjectDeleteView, ObjectEditView, ObjectListView,
|
||||
ObjectImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
|
||||
)
|
||||
from virtualization.models import VirtualMachine
|
||||
from . import filters, forms, tables
|
||||
@ -208,14 +209,12 @@ class SiteView(PermissionRequiredMixin, View):
|
||||
'vm_count': VirtualMachine.objects.filter(cluster__site=site).count(),
|
||||
}
|
||||
rack_groups = RackGroup.objects.filter(site=site).annotate(rack_count=Count('racks'))
|
||||
topology_maps = TopologyMap.objects.filter(site=site)
|
||||
show_graphs = Graph.objects.filter(type=GRAPH_TYPE_SITE).exists()
|
||||
|
||||
return render(request, 'dcim/site.html', {
|
||||
'site': site,
|
||||
'stats': stats,
|
||||
'rack_groups': rack_groups,
|
||||
'topology_maps': topology_maps,
|
||||
'show_graphs': show_graphs,
|
||||
})
|
||||
|
||||
@ -659,11 +658,31 @@ class DeviceTypeDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
default_return_url = 'dcim:devicetype_list'
|
||||
|
||||
|
||||
class DeviceTypeBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
permission_required = 'dcim.add_devicetype'
|
||||
model_form = forms.DeviceTypeCSVForm
|
||||
table = tables.DeviceTypeTable
|
||||
default_return_url = 'dcim:devicetype_list'
|
||||
class DeviceTypeImportView(PermissionRequiredMixin, ObjectImportView):
|
||||
permission_required = [
|
||||
'dcim.add_devicetype',
|
||||
'dcim.add_consoleporttemplate',
|
||||
'dcim.add_consoleserverporttemplate',
|
||||
'dcim.add_powerporttemplate',
|
||||
'dcim.add_poweroutlettemplate',
|
||||
'dcim.add_interfacetemplate',
|
||||
'dcim.add_frontporttemplate',
|
||||
'dcim.add_rearporttemplate',
|
||||
'dcim.add_devicebaytemplate',
|
||||
]
|
||||
model = DeviceType
|
||||
model_form = forms.DeviceTypeImportForm
|
||||
related_object_forms = OrderedDict((
|
||||
('console-ports', forms.ConsolePortTemplateImportForm),
|
||||
('console-server-ports', forms.ConsoleServerPortTemplateImportForm),
|
||||
('power-ports', forms.PowerPortTemplateImportForm),
|
||||
('power-outlets', forms.PowerOutletTemplateImportForm),
|
||||
('interfaces', forms.InterfaceTemplateImportForm),
|
||||
('rear-ports', forms.RearPortTemplateImportForm),
|
||||
('front-ports', forms.FrontPortTemplateImportForm),
|
||||
('device-bays', forms.DeviceBayTemplateImportForm),
|
||||
))
|
||||
default_return_url = 'dcim:devicetype_import'
|
||||
|
||||
|
||||
class DeviceTypeBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
|
@ -3,7 +3,7 @@ from django.contrib import admin
|
||||
|
||||
from netbox.admin import admin_site
|
||||
from utilities.forms import LaxURLField
|
||||
from .models import CustomField, CustomFieldChoice, CustomLink, Graph, ExportTemplate, TopologyMap, Webhook
|
||||
from .models import CustomField, CustomFieldChoice, CustomLink, Graph, ExportTemplate, Webhook
|
||||
|
||||
|
||||
def order_content_types(field):
|
||||
@ -164,15 +164,3 @@ class ExportTemplateAdmin(admin.ModelAdmin):
|
||||
'content_type',
|
||||
]
|
||||
form = ExportTemplateForm
|
||||
|
||||
|
||||
#
|
||||
# Topology maps
|
||||
#
|
||||
|
||||
@admin.register(TopologyMap, site=admin_site)
|
||||
class TopologyMapAdmin(admin.ModelAdmin):
|
||||
list_display = ['name', 'slug', 'site']
|
||||
prepopulated_fields = {
|
||||
'slug': ['name'],
|
||||
}
|
||||
|
@ -10,8 +10,7 @@ from dcim.api.nested_serializers import (
|
||||
from dcim.models import Device, DeviceRole, Platform, Rack, Region, Site
|
||||
from extras.constants import *
|
||||
from extras.models import (
|
||||
ConfigContext, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, TopologyMap,
|
||||
Tag
|
||||
ConfigContext, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, Tag,
|
||||
)
|
||||
from tenancy.api.nested_serializers import NestedTenantSerializer, NestedTenantGroupSerializer
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
@ -69,18 +68,6 @@ class ExportTemplateSerializer(ValidatedModelSerializer):
|
||||
]
|
||||
|
||||
|
||||
#
|
||||
# Topology maps
|
||||
#
|
||||
|
||||
class TopologyMapSerializer(ValidatedModelSerializer):
|
||||
site = NestedSiteSerializer()
|
||||
|
||||
class Meta:
|
||||
model = TopologyMap
|
||||
fields = ['id', 'name', 'slug', 'site', 'device_patterns', 'description']
|
||||
|
||||
|
||||
#
|
||||
# Tags
|
||||
#
|
||||
@ -213,6 +200,52 @@ class ReportDetailSerializer(ReportSerializer):
|
||||
result = ReportResultSerializer()
|
||||
|
||||
|
||||
#
|
||||
# Scripts
|
||||
#
|
||||
|
||||
class ScriptSerializer(serializers.Serializer):
|
||||
id = serializers.SerializerMethodField(read_only=True)
|
||||
name = serializers.SerializerMethodField(read_only=True)
|
||||
description = serializers.SerializerMethodField(read_only=True)
|
||||
vars = serializers.SerializerMethodField(read_only=True)
|
||||
|
||||
def get_id(self, instance):
|
||||
return '{}.{}'.format(instance.__module__, instance.__name__)
|
||||
|
||||
def get_name(self, instance):
|
||||
return getattr(instance.Meta, 'name', instance.__name__)
|
||||
|
||||
def get_description(self, instance):
|
||||
return getattr(instance.Meta, 'description', '')
|
||||
|
||||
def get_vars(self, instance):
|
||||
return {
|
||||
k: v.__class__.__name__ for k, v in instance._get_vars().items()
|
||||
}
|
||||
|
||||
|
||||
class ScriptInputSerializer(serializers.Serializer):
|
||||
data = serializers.JSONField()
|
||||
commit = serializers.BooleanField()
|
||||
|
||||
|
||||
class ScriptLogMessageSerializer(serializers.Serializer):
|
||||
status = serializers.SerializerMethodField(read_only=True)
|
||||
message = serializers.SerializerMethodField(read_only=True)
|
||||
|
||||
def get_status(self, instance):
|
||||
return LOG_LEVEL_CODES.get(instance[0])
|
||||
|
||||
def get_message(self, instance):
|
||||
return instance[1]
|
||||
|
||||
|
||||
class ScriptOutputSerializer(serializers.Serializer):
|
||||
log = ScriptLogMessageSerializer(many=True, read_only=True)
|
||||
output = serializers.CharField(read_only=True)
|
||||
|
||||
|
||||
#
|
||||
# Change logging
|
||||
#
|
||||
|
@ -26,9 +26,6 @@ router.register(r'graphs', views.GraphViewSet)
|
||||
# Export templates
|
||||
router.register(r'export-templates', views.ExportTemplateViewSet)
|
||||
|
||||
# Topology maps
|
||||
router.register(r'topology-maps', views.TopologyMapViewSet)
|
||||
|
||||
# Tags
|
||||
router.register(r'tags', views.TagViewSet)
|
||||
|
||||
@ -41,6 +38,9 @@ router.register(r'config-contexts', views.ConfigContextViewSet)
|
||||
# Reports
|
||||
router.register(r'reports', views.ReportViewSet, basename='report')
|
||||
|
||||
# Scripts
|
||||
router.register(r'scripts', views.ScriptViewSet, basename='script')
|
||||
|
||||
# Change logging
|
||||
router.register(r'object-changes', views.ObjectChangeViewSet)
|
||||
|
||||
|
@ -2,8 +2,8 @@ from collections import OrderedDict
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db.models import Count
|
||||
from django.http import Http404, HttpResponse
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.http import Http404
|
||||
from rest_framework import status
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
from rest_framework.response import Response
|
||||
@ -11,10 +11,10 @@ from rest_framework.viewsets import ReadOnlyModelViewSet, ViewSet
|
||||
|
||||
from extras import filters
|
||||
from extras.models import (
|
||||
ConfigContext, CustomFieldChoice, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, TopologyMap,
|
||||
Tag,
|
||||
ConfigContext, CustomFieldChoice, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, Tag,
|
||||
)
|
||||
from extras.reports import get_report, get_reports
|
||||
from extras.scripts import get_script, get_scripts
|
||||
from utilities.api import FieldChoicesViewSet, IsAuthenticatedOrLoginNotRequired, ModelViewSet
|
||||
from . import serializers
|
||||
|
||||
@ -115,34 +115,6 @@ class ExportTemplateViewSet(ModelViewSet):
|
||||
filterset_class = filters.ExportTemplateFilter
|
||||
|
||||
|
||||
#
|
||||
# Topology maps
|
||||
#
|
||||
|
||||
class TopologyMapViewSet(ModelViewSet):
|
||||
queryset = TopologyMap.objects.prefetch_related('site')
|
||||
serializer_class = serializers.TopologyMapSerializer
|
||||
filterset_class = filters.TopologyMapFilter
|
||||
|
||||
@action(detail=True)
|
||||
def render(self, request, pk):
|
||||
|
||||
tmap = get_object_or_404(TopologyMap, pk=pk)
|
||||
img_format = 'png'
|
||||
|
||||
try:
|
||||
data = tmap.render(img_format=img_format)
|
||||
except Exception as e:
|
||||
return HttpResponse(
|
||||
"There was an error generating the requested graph: %s" % e
|
||||
)
|
||||
|
||||
response = HttpResponse(data, content_type='image/{}'.format(img_format))
|
||||
response['Content-Disposition'] = 'inline; filename="{}.{}"'.format(tmap.slug, img_format)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
#
|
||||
# Tags
|
||||
#
|
||||
@ -252,6 +224,56 @@ class ReportViewSet(ViewSet):
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
#
|
||||
# Scripts
|
||||
#
|
||||
|
||||
class ScriptViewSet(ViewSet):
|
||||
permission_classes = [IsAuthenticatedOrLoginNotRequired]
|
||||
_ignore_model_permissions = True
|
||||
exclude_from_schema = True
|
||||
lookup_value_regex = '[^/]+' # Allow dots
|
||||
|
||||
def _get_script(self, pk):
|
||||
module_name, script_name = pk.split('.')
|
||||
script = get_script(module_name, script_name)
|
||||
if script is None:
|
||||
raise Http404
|
||||
return script
|
||||
|
||||
def list(self, request):
|
||||
|
||||
flat_list = []
|
||||
for script_list in get_scripts().values():
|
||||
flat_list.extend(script_list.values())
|
||||
|
||||
serializer = serializers.ScriptSerializer(flat_list, many=True, context={'request': request})
|
||||
|
||||
return Response(serializer.data)
|
||||
|
||||
def retrieve(self, request, pk):
|
||||
script = self._get_script(pk)
|
||||
serializer = serializers.ScriptSerializer(script, context={'request': request})
|
||||
|
||||
return Response(serializer.data)
|
||||
|
||||
def post(self, request, pk):
|
||||
"""
|
||||
Run a Script identified as "<module>.<script>".
|
||||
"""
|
||||
script = self._get_script(pk)()
|
||||
input_serializer = serializers.ScriptInputSerializer(data=request.data)
|
||||
|
||||
if input_serializer.is_valid():
|
||||
output = script.run(input_serializer.data['data'])
|
||||
script.output = output
|
||||
output_serializer = serializers.ScriptOutputSerializer(script)
|
||||
|
||||
return Response(output_serializer.data)
|
||||
|
||||
return Response(input_serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
#
|
||||
# Change logging
|
||||
#
|
||||
|
@ -21,11 +21,11 @@ class ExtrasConfig(AppConfig):
|
||||
)
|
||||
try:
|
||||
rs = redis.Redis(
|
||||
host=settings.REDIS_HOST,
|
||||
port=settings.REDIS_PORT,
|
||||
db=settings.REDIS_DATABASE,
|
||||
password=settings.REDIS_PASSWORD or None,
|
||||
ssl=settings.REDIS_SSL,
|
||||
host=settings.WEBHOOKS_REDIS_HOST,
|
||||
port=settings.WEBHOOKS_REDIS_PORT,
|
||||
db=settings.WEBHOOKS_REDIS_DATABASE,
|
||||
password=settings.WEBHOOKS_REDIS_PASSWORD or None,
|
||||
ssl=settings.WEBHOOKS_REDIS_SSL,
|
||||
)
|
||||
rs.ping()
|
||||
except redis.exceptions.ConnectionError:
|
||||
|
@ -136,16 +136,6 @@ TEMPLATE_LANGUAGE_CHOICES = (
|
||||
(TEMPLATE_LANGUAGE_JINJA2, 'Jinja2'),
|
||||
)
|
||||
|
||||
# Topology map types
|
||||
TOPOLOGYMAP_TYPE_NETWORK = 1
|
||||
TOPOLOGYMAP_TYPE_CONSOLE = 2
|
||||
TOPOLOGYMAP_TYPE_POWER = 3
|
||||
TOPOLOGYMAP_TYPE_CHOICES = (
|
||||
(TOPOLOGYMAP_TYPE_NETWORK, 'Network'),
|
||||
(TOPOLOGYMAP_TYPE_CONSOLE, 'Console'),
|
||||
(TOPOLOGYMAP_TYPE_POWER, 'Power'),
|
||||
)
|
||||
|
||||
# Change log actions
|
||||
OBJECTCHANGE_ACTION_CREATE = 1
|
||||
OBJECTCHANGE_ACTION_UPDATE = 2
|
||||
|
@ -5,7 +5,7 @@ from django.db.models import Q
|
||||
from dcim.models import DeviceRole, Platform, Region, Site
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from .constants import *
|
||||
from .models import ConfigContext, CustomField, Graph, ExportTemplate, ObjectChange, Tag, TopologyMap
|
||||
from .models import ConfigContext, CustomField, Graph, ExportTemplate, ObjectChange, Tag
|
||||
|
||||
|
||||
class CustomFieldFilter(django_filters.Filter):
|
||||
@ -103,24 +103,6 @@ class TagFilter(django_filters.FilterSet):
|
||||
)
|
||||
|
||||
|
||||
class TopologyMapFilter(django_filters.FilterSet):
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='site',
|
||||
queryset=Site.objects.all(),
|
||||
label='Site',
|
||||
)
|
||||
site = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='site__slug',
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Site (slug)',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = TopologyMap
|
||||
fields = ['name', 'slug']
|
||||
|
||||
|
||||
class ConfigContextFilter(django_filters.FilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
|
16
netbox/extras/migrations/0028_remove_topology_maps.py
Normal file
16
netbox/extras/migrations/0028_remove_topology_maps.py
Normal file
@ -0,0 +1,16 @@
|
||||
# Generated by Django 2.2 on 2019-08-09 01:26
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('extras', '0027_webhook_additional_headers'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.DeleteModel(
|
||||
name='TopologyMap',
|
||||
),
|
||||
]
|
@ -1,23 +1,20 @@
|
||||
from collections import OrderedDict
|
||||
from datetime import date
|
||||
|
||||
import graphviz
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.contrib.postgres.fields import JSONField
|
||||
from django.core.validators import ValidationError
|
||||
from django.db import models
|
||||
from django.db.models import F, Q
|
||||
from django.http import HttpResponse
|
||||
from django.template import Template, Context
|
||||
from django.urls import reverse
|
||||
from jinja2 import Environment
|
||||
from taggit.models import TagBase, GenericTaggedItemBase
|
||||
|
||||
from dcim.constants import CONNECTION_STATUS_CONNECTED
|
||||
from utilities.fields import ColorField
|
||||
from utilities.utils import deepmerge, foreground_color, model_names_to_filter_dict
|
||||
from utilities.utils import deepmerge, model_names_to_filter_dict
|
||||
from .constants import *
|
||||
from .querysets import ConfigContextQuerySet
|
||||
|
||||
@ -531,154 +528,6 @@ class ExportTemplate(models.Model):
|
||||
return response
|
||||
|
||||
|
||||
#
|
||||
# Topology maps
|
||||
#
|
||||
|
||||
class TopologyMap(models.Model):
|
||||
name = models.CharField(
|
||||
max_length=50,
|
||||
unique=True
|
||||
)
|
||||
slug = models.SlugField(
|
||||
unique=True
|
||||
)
|
||||
type = models.PositiveSmallIntegerField(
|
||||
choices=TOPOLOGYMAP_TYPE_CHOICES,
|
||||
default=TOPOLOGYMAP_TYPE_NETWORK
|
||||
)
|
||||
site = models.ForeignKey(
|
||||
to='dcim.Site',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='topology_maps',
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
device_patterns = models.TextField(
|
||||
help_text='Identify devices to include in the diagram using regular '
|
||||
'expressions, one per line. Each line will result in a new '
|
||||
'tier of the drawing. Separate multiple regexes within a '
|
||||
'line using semicolons. Devices will be rendered in the '
|
||||
'order they are defined.'
|
||||
)
|
||||
description = models.CharField(
|
||||
max_length=100,
|
||||
blank=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@property
|
||||
def device_sets(self):
|
||||
if not self.device_patterns:
|
||||
return None
|
||||
return [line.strip() for line in self.device_patterns.split('\n')]
|
||||
|
||||
def render(self, img_format='png'):
|
||||
|
||||
from dcim.models import Device
|
||||
|
||||
# Construct the graph
|
||||
if self.type == TOPOLOGYMAP_TYPE_NETWORK:
|
||||
G = graphviz.Graph
|
||||
else:
|
||||
G = graphviz.Digraph
|
||||
self.graph = G()
|
||||
self.graph.graph_attr['ranksep'] = '1'
|
||||
seen = set()
|
||||
for i, device_set in enumerate(self.device_sets):
|
||||
|
||||
subgraph = G(name='sg{}'.format(i))
|
||||
subgraph.graph_attr['rank'] = 'same'
|
||||
subgraph.graph_attr['directed'] = 'true'
|
||||
|
||||
# Add a pseudonode for each device_set to enforce hierarchical layout
|
||||
subgraph.node('set{}'.format(i), label='', shape='none', width='0')
|
||||
if i:
|
||||
self.graph.edge('set{}'.format(i - 1), 'set{}'.format(i), style='invis')
|
||||
|
||||
# Add each device to the graph
|
||||
devices = []
|
||||
for query in device_set.strip(';').split(';'): # Split regexes on semicolons
|
||||
devices += Device.objects.filter(name__regex=query).prefetch_related('device_role')
|
||||
# Remove duplicate devices
|
||||
devices = [d for d in devices if d.id not in seen]
|
||||
seen.update([d.id for d in devices])
|
||||
for d in devices:
|
||||
bg_color = '#{}'.format(d.device_role.color)
|
||||
fg_color = '#{}'.format(foreground_color(d.device_role.color))
|
||||
subgraph.node(d.name, style='filled', fillcolor=bg_color, fontcolor=fg_color, fontname='sans')
|
||||
|
||||
# Add an invisible connection to each successive device in a set to enforce horizontal order
|
||||
for j in range(0, len(devices) - 1):
|
||||
subgraph.edge(devices[j].name, devices[j + 1].name, style='invis')
|
||||
|
||||
self.graph.subgraph(subgraph)
|
||||
|
||||
# Compile list of all devices
|
||||
device_superset = Q()
|
||||
for device_set in self.device_sets:
|
||||
for query in device_set.split(';'): # Split regexes on semicolons
|
||||
device_superset = device_superset | Q(name__regex=query)
|
||||
devices = Device.objects.filter(*(device_superset,))
|
||||
|
||||
# Draw edges depending on graph type
|
||||
if self.type == TOPOLOGYMAP_TYPE_NETWORK:
|
||||
self.add_network_connections(devices)
|
||||
elif self.type == TOPOLOGYMAP_TYPE_CONSOLE:
|
||||
self.add_console_connections(devices)
|
||||
elif self.type == TOPOLOGYMAP_TYPE_POWER:
|
||||
self.add_power_connections(devices)
|
||||
|
||||
return self.graph.pipe(format=img_format)
|
||||
|
||||
def add_network_connections(self, devices):
|
||||
|
||||
from circuits.models import CircuitTermination
|
||||
from dcim.models import Interface
|
||||
|
||||
# Add all interface connections to the graph
|
||||
connected_interfaces = Interface.objects.prefetch_related(
|
||||
'_connected_interface__device'
|
||||
).filter(
|
||||
Q(device__in=devices) | Q(_connected_interface__device__in=devices),
|
||||
_connected_interface__isnull=False,
|
||||
pk__lt=F('_connected_interface')
|
||||
)
|
||||
for interface in connected_interfaces:
|
||||
style = 'solid' if interface.connection_status == CONNECTION_STATUS_CONNECTED else 'dashed'
|
||||
self.graph.edge(interface.device.name, interface.connected_endpoint.device.name, style=style)
|
||||
|
||||
# Add all circuits to the graph
|
||||
for termination in CircuitTermination.objects.filter(term_side='A', connected_endpoint__device__in=devices):
|
||||
peer_termination = termination.get_peer_termination()
|
||||
if (peer_termination is not None and peer_termination.interface is not None and
|
||||
peer_termination.interface.device in devices):
|
||||
self.graph.edge(termination.interface.device.name, peer_termination.interface.device.name, color='blue')
|
||||
|
||||
def add_console_connections(self, devices):
|
||||
|
||||
from dcim.models import ConsolePort
|
||||
|
||||
# Add all console connections to the graph
|
||||
for cp in ConsolePort.objects.filter(device__in=devices, connected_endpoint__device__in=devices):
|
||||
style = 'solid' if cp.connection_status == CONNECTION_STATUS_CONNECTED else 'dashed'
|
||||
self.graph.edge(cp.connected_endpoint.device.name, cp.device.name, style=style)
|
||||
|
||||
def add_power_connections(self, devices):
|
||||
|
||||
from dcim.models import PowerPort
|
||||
|
||||
# Add all power connections to the graph
|
||||
for pp in PowerPort.objects.filter(device__in=devices, _connected_poweroutlet__device__in=devices):
|
||||
style = 'solid' if pp.connection_status == CONNECTION_STATUS_CONNECTED else 'dashed'
|
||||
self.graph.edge(pp.connected_endpoint.device.name, pp.device.name, style=style)
|
||||
|
||||
|
||||
#
|
||||
# Image attachments
|
||||
#
|
||||
|
@ -242,16 +242,21 @@ class BaseScript:
|
||||
def __str__(self):
|
||||
return getattr(self.Meta, 'name', self.__class__.__name__)
|
||||
|
||||
def _get_vars(self):
|
||||
@classmethod
|
||||
def module(cls):
|
||||
return cls.__module__
|
||||
|
||||
@classmethod
|
||||
def _get_vars(cls):
|
||||
vars = OrderedDict()
|
||||
|
||||
# Infer order from Meta.field_order (Python 3.5 and lower)
|
||||
field_order = getattr(self.Meta, 'field_order', [])
|
||||
field_order = getattr(cls.Meta, 'field_order', [])
|
||||
for name in field_order:
|
||||
vars[name] = getattr(self, name)
|
||||
vars[name] = getattr(cls, name)
|
||||
|
||||
# Default to order of declaration on class
|
||||
for name, attr in self.__class__.__dict__.items():
|
||||
for name, attr in cls.__dict__.items():
|
||||
if name not in vars and issubclass(attr.__class__, ScriptVariable):
|
||||
vars[name] = attr
|
||||
|
||||
@ -382,14 +387,18 @@ def run_script(script, data, files, commit=True):
|
||||
return output, execution_time
|
||||
|
||||
|
||||
def get_scripts():
|
||||
def get_scripts(use_names=False):
|
||||
"""
|
||||
Return a dict of dicts mapping all scripts to their modules. Set use_names to True to use each module's human-
|
||||
defined name in place of the actual module name.
|
||||
"""
|
||||
scripts = OrderedDict()
|
||||
|
||||
# Iterate through all modules within the reports path. These are the user-created files in which reports are
|
||||
# defined.
|
||||
for importer, module_name, _ in pkgutil.iter_modules([settings.SCRIPTS_ROOT]):
|
||||
module = importer.find_module(module_name).load_module(module_name)
|
||||
if hasattr(module, 'name'):
|
||||
if use_names and hasattr(module, 'name'):
|
||||
module_name = module.name
|
||||
module_scripts = OrderedDict()
|
||||
for name, cls in inspect.getmembers(module, is_script):
|
||||
@ -397,3 +406,13 @@ def get_scripts():
|
||||
scripts[module_name] = module_scripts
|
||||
|
||||
return scripts
|
||||
|
||||
|
||||
def get_script(module_name, script_name):
|
||||
"""
|
||||
Retrieve a script class by module and name. Returns None if the script does not exist.
|
||||
"""
|
||||
scripts = get_scripts()
|
||||
module = scripts.get(module_name)
|
||||
if module:
|
||||
return module.get(script_name)
|
||||
|
@ -3,8 +3,10 @@ from django.urls import reverse
|
||||
from rest_framework import status
|
||||
|
||||
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Platform, Region, Site
|
||||
from extras.api.views import ScriptViewSet
|
||||
from extras.constants import GRAPH_TYPE_SITE
|
||||
from extras.models import ConfigContext, Graph, ExportTemplate, Tag
|
||||
from extras.scripts import BooleanVar, IntegerVar, Script, StringVar
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from utilities.testing import APITestCase
|
||||
|
||||
@ -520,3 +522,68 @@ class ConfigContextTest(APITestCase):
|
||||
configcontext6.sites.add(site2)
|
||||
rendered_context = device.get_config_context()
|
||||
self.assertEqual(rendered_context['bar'], 456)
|
||||
|
||||
|
||||
class ScriptTest(APITestCase):
|
||||
|
||||
class TestScript(Script):
|
||||
|
||||
class Meta:
|
||||
name = "Test script"
|
||||
|
||||
var1 = StringVar()
|
||||
var2 = IntegerVar()
|
||||
var3 = BooleanVar()
|
||||
|
||||
def run(self, data):
|
||||
|
||||
self.log_info(data['var1'])
|
||||
self.log_success(data['var2'])
|
||||
self.log_failure(data['var3'])
|
||||
|
||||
return 'Script complete'
|
||||
|
||||
def get_test_script(self, *args):
|
||||
return self.TestScript
|
||||
|
||||
def setUp(self):
|
||||
|
||||
super().setUp()
|
||||
|
||||
# Monkey-patch the API viewset's _get_script method to return our test script above
|
||||
ScriptViewSet._get_script = self.get_test_script
|
||||
|
||||
def test_get_script(self):
|
||||
|
||||
url = reverse('extras-api:script-detail', kwargs={'pk': None})
|
||||
response = self.client.get(url, **self.header)
|
||||
|
||||
self.assertEqual(response.data['name'], self.TestScript.Meta.name)
|
||||
self.assertEqual(response.data['vars']['var1'], 'StringVar')
|
||||
self.assertEqual(response.data['vars']['var2'], 'IntegerVar')
|
||||
self.assertEqual(response.data['vars']['var3'], 'BooleanVar')
|
||||
|
||||
def test_run_script(self):
|
||||
|
||||
script_data = {
|
||||
'var1': 'FooBar',
|
||||
'var2': 123,
|
||||
'var3': False,
|
||||
}
|
||||
|
||||
data = {
|
||||
'data': script_data,
|
||||
'commit': True,
|
||||
}
|
||||
|
||||
url = reverse('extras-api:script-detail', kwargs={'pk': None})
|
||||
response = self.client.post(url, data, format='json', **self.header)
|
||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||
|
||||
self.assertEqual(response.data['log'][0]['status'], 'info')
|
||||
self.assertEqual(response.data['log'][0]['message'], script_data['var1'])
|
||||
self.assertEqual(response.data['log'][1]['status'], 'success')
|
||||
self.assertEqual(response.data['log'][1]['message'], script_data['var2'])
|
||||
self.assertEqual(response.data['log'][2]['status'], 'failure')
|
||||
self.assertEqual(response.data['log'][2]['message'], script_data['var3'])
|
||||
self.assertEqual(response.data['output'], 'Script complete')
|
||||
|
@ -375,7 +375,7 @@ class ScriptListView(PermissionRequiredMixin, View):
|
||||
def get(self, request):
|
||||
|
||||
return render(request, 'extras/script_list.html', {
|
||||
'scripts': get_scripts(),
|
||||
'scripts': get_scripts(use_names=True),
|
||||
})
|
||||
|
||||
|
||||
|
@ -27,15 +27,26 @@ DATABASE = {
|
||||
SECRET_KEY = ''
|
||||
|
||||
# Redis database settings. The Redis database is used for caching and background processing such as webhooks
|
||||
# Seperate sections for webhooks and caching allow for connecting to seperate Redis instances/datbases if desired.
|
||||
# Full connection details are required in both sections, even if they are the same.
|
||||
REDIS = {
|
||||
'webhooks': {
|
||||
'HOST': 'redis.example.com',
|
||||
'PORT': 1234,
|
||||
'PASSWORD': 'foobar',
|
||||
'DATABASE': 0,
|
||||
'DEFAULT_TIMEOUT': 300,
|
||||
'SSL': False,
|
||||
},
|
||||
'caching': {
|
||||
'HOST': 'localhost',
|
||||
'PORT': 6379,
|
||||
'PASSWORD': '',
|
||||
'DATABASE': 0,
|
||||
'CACHE_DATABASE': 1,
|
||||
'DATABASE': 1,
|
||||
'DEFAULT_TIMEOUT': 300,
|
||||
'SSL': False,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#########################
|
||||
|
@ -12,7 +12,7 @@ from django.core.exceptions import ImproperlyConfigured
|
||||
# Environment setup
|
||||
#
|
||||
|
||||
VERSION = '2.6.8-dev'
|
||||
VERSION = '2.7-beta1'
|
||||
|
||||
# Hostname
|
||||
HOSTNAME = platform.node()
|
||||
@ -118,13 +118,30 @@ DATABASES = {
|
||||
# Redis
|
||||
#
|
||||
|
||||
REDIS_HOST = REDIS.get('HOST', 'localhost')
|
||||
REDIS_PORT = REDIS.get('PORT', 6379)
|
||||
REDIS_PASSWORD = REDIS.get('PASSWORD', '')
|
||||
REDIS_DATABASE = REDIS.get('DATABASE', 0)
|
||||
REDIS_CACHE_DATABASE = REDIS.get('CACHE_DATABASE', 1)
|
||||
REDIS_DEFAULT_TIMEOUT = REDIS.get('DEFAULT_TIMEOUT', 300)
|
||||
REDIS_SSL = REDIS.get('SSL', False)
|
||||
if 'webhooks' not in REDIS:
|
||||
raise ImproperlyConfigured(
|
||||
"REDIS section in configuration.py is missing webhooks subsection."
|
||||
)
|
||||
if 'caching' not in REDIS:
|
||||
raise ImproperlyConfigured(
|
||||
"REDIS section in configuration.py is missing caching subsection."
|
||||
)
|
||||
|
||||
WEBHOOKS_REDIS = REDIS.get('webhooks', {})
|
||||
WEBHOOKS_REDIS_HOST = WEBHOOKS_REDIS.get('HOST', 'localhost')
|
||||
WEBHOOKS_REDIS_PORT = WEBHOOKS_REDIS.get('PORT', 6379)
|
||||
WEBHOOKS_REDIS_PASSWORD = WEBHOOKS_REDIS.get('PASSWORD', '')
|
||||
WEBHOOKS_REDIS_DATABASE = WEBHOOKS_REDIS.get('DATABASE', 0)
|
||||
WEBHOOKS_REDIS_DEFAULT_TIMEOUT = WEBHOOKS_REDIS.get('DEFAULT_TIMEOUT', 300)
|
||||
WEBHOOKS_REDIS_SSL = WEBHOOKS_REDIS.get('SSL', False)
|
||||
|
||||
CACHING_REDIS = REDIS.get('caching', {})
|
||||
CACHING_REDIS_HOST = WEBHOOKS_REDIS.get('HOST', 'localhost')
|
||||
CACHING_REDIS_PORT = WEBHOOKS_REDIS.get('PORT', 6379)
|
||||
CACHING_REDIS_PASSWORD = WEBHOOKS_REDIS.get('PASSWORD', '')
|
||||
CACHING_REDIS_DATABASE = WEBHOOKS_REDIS.get('DATABASE', 0)
|
||||
CACHING_REDIS_DEFAULT_TIMEOUT = WEBHOOKS_REDIS.get('DEFAULT_TIMEOUT', 300)
|
||||
CACHING_REDIS_SSL = WEBHOOKS_REDIS.get('SSL', False)
|
||||
|
||||
|
||||
#
|
||||
@ -341,15 +358,20 @@ if LDAP_CONFIG is not None:
|
||||
# Caching
|
||||
#
|
||||
|
||||
if REDIS_SSL:
|
||||
if CACHING_REDIS_SSL:
|
||||
REDIS_CACHE_CON_STRING = 'rediss://'
|
||||
else:
|
||||
REDIS_CACHE_CON_STRING = 'redis://'
|
||||
|
||||
if REDIS_PASSWORD:
|
||||
REDIS_CACHE_CON_STRING = '{}:{}@'.format(REDIS_CACHE_CON_STRING, REDIS_PASSWORD)
|
||||
if CACHING_REDIS_PASSWORD:
|
||||
REDIS_CACHE_CON_STRING = '{}:{}@'.format(REDIS_CACHE_CON_STRING, CACHING_REDIS_PASSWORD)
|
||||
|
||||
REDIS_CACHE_CON_STRING = '{}{}:{}/{}'.format(REDIS_CACHE_CON_STRING, REDIS_HOST, REDIS_PORT, REDIS_CACHE_DATABASE)
|
||||
REDIS_CACHE_CON_STRING = '{}{}:{}/{}'.format(
|
||||
REDIS_CACHE_CON_STRING,
|
||||
CACHING_REDIS_HOST,
|
||||
CACHING_REDIS_PORT,
|
||||
CACHING_REDIS_DATABASE
|
||||
)
|
||||
|
||||
if not CACHE_TIMEOUT:
|
||||
CACHEOPS_ENABLED = False
|
||||
@ -468,12 +490,12 @@ SWAGGER_SETTINGS = {
|
||||
|
||||
RQ_QUEUES = {
|
||||
'default': {
|
||||
'HOST': REDIS_HOST,
|
||||
'PORT': REDIS_PORT,
|
||||
'DB': REDIS_DATABASE,
|
||||
'PASSWORD': REDIS_PASSWORD,
|
||||
'DEFAULT_TIMEOUT': REDIS_DEFAULT_TIMEOUT,
|
||||
'SSL': REDIS_SSL,
|
||||
'HOST': WEBHOOKS_REDIS_HOST,
|
||||
'PORT': WEBHOOKS_REDIS_PORT,
|
||||
'DB': WEBHOOKS_REDIS_DATABASE,
|
||||
'PASSWORD': WEBHOOKS_REDIS_PASSWORD,
|
||||
'DEFAULT_TIMEOUT': WEBHOOKS_REDIS_DEFAULT_TIMEOUT,
|
||||
'SSL': WEBHOOKS_REDIS_SSL,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -21,7 +21,7 @@ from dcim.tables import (
|
||||
CableTable, DeviceDetailTable, DeviceTypeTable, PowerFeedTable, RackTable, RackGroupTable, SiteTable,
|
||||
VirtualChassisTable,
|
||||
)
|
||||
from extras.models import ObjectChange, ReportResult, TopologyMap
|
||||
from extras.models import ObjectChange, ReportResult
|
||||
from ipam.filters import AggregateFilter, IPAddressFilter, PrefixFilter, VLANFilter, VRFFilter
|
||||
from ipam.models import Aggregate, IPAddress, Prefix, VLAN, VRF
|
||||
from ipam.tables import AggregateTable, IPAddressTable, PrefixTable, VLANTable, VRFTable
|
||||
@ -245,7 +245,6 @@ class HomeView(View):
|
||||
return render(request, self.template_name, {
|
||||
'search_form': SearchForm(),
|
||||
'stats': stats,
|
||||
'topology_maps': TopologyMap.objects.filter(site__isnull=True),
|
||||
'report_results': ReportResult.objects.order_by('-created')[:10],
|
||||
'changelog': ObjectChange.objects.prefetch_related('user', 'changed_object_type')[:50]
|
||||
})
|
||||
|
@ -628,6 +628,7 @@
|
||||
<th class="pk"><input type="checkbox" class="toggle" title="Toggle all" /></th>
|
||||
{% endif %}
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>Description</th>
|
||||
<th>Cable</th>
|
||||
<th colspan="2">Connection</th>
|
||||
@ -687,6 +688,7 @@
|
||||
<th class="pk"><input type="checkbox" class="toggle" title="Toggle all" /></th>
|
||||
{% endif %}
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>Input/Leg</th>
|
||||
<th>Description</th>
|
||||
<th>Cable</th>
|
||||
|
@ -1,4 +1,4 @@
|
||||
{% extends 'utilities/obj_import.html' %}
|
||||
{% extends 'utilities/obj_bulk_import.html' %}
|
||||
|
||||
{% block tabs %}
|
||||
{% include 'dcim/inc/device_import_header.html' %}
|
||||
|
@ -1,4 +1,4 @@
|
||||
{% extends 'utilities/obj_import.html' %}
|
||||
{% extends 'utilities/obj_bulk_import.html' %}
|
||||
|
||||
{% block tabs %}
|
||||
{% include 'dcim/inc/device_import_header.html' with active_tab='child_import' %}
|
||||
|
@ -4,6 +4,12 @@
|
||||
<td>
|
||||
<i class="fa fa-fw fa-keyboard-o"></i> {{ cp }}
|
||||
</td>
|
||||
|
||||
{# Type #}
|
||||
<td>
|
||||
{% if cp.type %}{{ cp.get_type_display }}{% else %}—{% endif %}
|
||||
</td>
|
||||
|
||||
<td></td>
|
||||
|
||||
{# Description #}
|
||||
|
@ -14,6 +14,11 @@
|
||||
<i class="fa fa-fw fa-keyboard-o"></i> {{ csp }}
|
||||
</td>
|
||||
|
||||
{# Type #}
|
||||
<td>
|
||||
{% if csp.type %}{{ csp.get_type_display }}{% else %}—{% endif %}
|
||||
</td>
|
||||
|
||||
{# Description #}
|
||||
<td>
|
||||
{{ csp.description|placeholder }}
|
||||
|
@ -14,6 +14,11 @@
|
||||
<i class="fa fa-fw fa-bolt"></i> {{ po }}
|
||||
</td>
|
||||
|
||||
{# Type #}
|
||||
<td>
|
||||
{{ po.get_type_display }}
|
||||
</td>
|
||||
|
||||
{# Input/leg #}
|
||||
<td>
|
||||
{% if po.power_port %}
|
||||
|
@ -5,6 +5,11 @@
|
||||
<i class="fa fa-fw fa-bolt"></i> {{ pp }}
|
||||
</td>
|
||||
|
||||
{# Type #}
|
||||
<td>
|
||||
{{ pp.get_type_display }}
|
||||
</td>
|
||||
|
||||
{# Current draw #}
|
||||
<td>
|
||||
{% if pp.allocated_draw %}
|
||||
|
@ -285,25 +285,6 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Topology Maps</strong>
|
||||
</div>
|
||||
{% if topology_maps %}
|
||||
<table class="table table-hover panel-body">
|
||||
{% for tm in topology_maps %}
|
||||
<tr>
|
||||
<td><i class="fa fa-fw fa-map-o"></i> <a href="{% url 'extras-api:topologymap-render' pk=tm.pk %}" target="_blank">{{ tm }}</a></td>
|
||||
<td>{{ tm.description }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="panel-body text-muted">
|
||||
None
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'inc/modal.html' with modal_name='graphs' %}
|
||||
|
@ -19,7 +19,7 @@
|
||||
{% for class_name, script in module_scripts.items %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{% url 'extras:script' module=module name=class_name %}" name="script.{{ class_name }}"><strong>{{ script }}</strong></a>
|
||||
<a href="{% url 'extras:script' module=script.module name=class_name %}" name="script.{{ class_name }}"><strong>{{ script }}</strong></a>
|
||||
</td>
|
||||
<td>{{ script.Meta.description }}</td>
|
||||
</tr>
|
||||
|
@ -259,29 +259,6 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Global Topology Maps</strong>
|
||||
</div>
|
||||
{% if topology_maps and perms.extras.view_topologymap %}
|
||||
<table class="table table-hover panel-body">
|
||||
{% for tm in topology_maps %}
|
||||
<tr>
|
||||
<td><i class="fa fa-fw fa-map-o"></i> <a href="{% url 'extras-api:topologymap-render' pk=tm.pk %}" target="_blank">{{ tm }}</a></td>
|
||||
<td>{{ tm.description }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% elif perms.extras.view_topologymap %}
|
||||
<div class="panel-body text-muted">
|
||||
None found
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="panel-body text-muted">
|
||||
<i class="fa fa-lock"></i> No permission
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Reports</strong>
|
||||
|
@ -16,7 +16,7 @@
|
||||
<div id="navbar" class="navbar-collapse collapse">
|
||||
{% if request.user.is_authenticated or not settings.LOGIN_REQUIRED %}
|
||||
<ul class="nav navbar-nav">
|
||||
<li class="dropdown{% if request.path|contains:'/dcim/sites/,/dcim/regions/,/tenancy/,/extras/tags/,/extras/reports/' %} active{% endif %}">
|
||||
<li class="dropdown">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Organization <span class="caret"></span></a>
|
||||
<ul class="dropdown-menu">
|
||||
<li class="dropdown-header">Sites</li>
|
||||
@ -39,47 +39,6 @@
|
||||
<a href="{% url 'dcim:region_list' %}">Regions</a>
|
||||
</li>
|
||||
<li class="divider"></li>
|
||||
<li class="dropdown-header">Tenancy</li>
|
||||
<li{% if not perms.tenancy.view_tenant %} class="disabled"{% endif %}>
|
||||
{% if perms.tenancy.add_tenant %}
|
||||
<div class="buttons pull-right">
|
||||
<a href="{% url 'tenancy:tenant_add' %}" class="btn btn-xs btn-success" title="Add"><i class="fa fa-plus"></i></a>
|
||||
<a href="{% url 'tenancy:tenant_import' %}" class="btn btn-xs btn-info" title="Import"><i class="fa fa-download"></i></a>
|
||||
</div>
|
||||
{% endif %}
|
||||
<a href="{% url 'tenancy:tenant_list' %}">Tenants</a>
|
||||
</li>
|
||||
<li{% if not perms.tenancy.view_tenantgroup %} class="disabled"{% endif %}>
|
||||
{% if perms.tenancy.add_tenantgroup %}
|
||||
<div class="buttons pull-right">
|
||||
<a href="{% url 'tenancy:tenantgroup_add' %}" class="btn btn-xs btn-success" title="Add"><i class="fa fa-plus"></i></a>
|
||||
<a href="{% url 'tenancy:tenantgroup_import' %}" class="btn btn-xs btn-info" title="Import"><i class="fa fa-download"></i></a>
|
||||
</div>
|
||||
{% endif %}
|
||||
<a href="{% url 'tenancy:tenantgroup_list' %}">Tenant Groups</a>
|
||||
</li>
|
||||
<li class="divider"></li>
|
||||
<li class="dropdown-header">Miscellaneous</li>
|
||||
<li{% if not perms.extras.view_tag %} class="disabled"{% endif %}>
|
||||
<a href="{% url 'extras:tag_list' %}">Tags</a>
|
||||
</li>
|
||||
<li{% if not perms.extras.view_configcontext %} class="disabled"{% endif %}>
|
||||
<a href="{% url 'extras:configcontext_list' %}">Config Contexts</a>
|
||||
</li>
|
||||
<li{% if not perms.extras.view_script %} class="disabled"{% endif %}>
|
||||
<a href="{% url 'extras:script_list' %}">Scripts</a>
|
||||
</li>
|
||||
<li{% if not perms.extras.view_reportresult %} class="disabled"{% endif %}>
|
||||
<a href="{% url 'extras:report_list' %}">Reports</a>
|
||||
</li>
|
||||
<li{% if not perms.extras.view_objectchange %} class="disabled"{% endif %}>
|
||||
<a href="{% url 'extras:objectchange_list' %}">Changelog</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="dropdown{% if request.path|contains:'/dcim/rack' %} active{% endif %}">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Racks <span class="caret"></span></a>
|
||||
<ul class="dropdown-menu">
|
||||
<li class="dropdown-header">Racks</li>
|
||||
<li{% if not perms.dcim.view_rack %} class="disabled"{% endif %}>
|
||||
{% if perms.dcim.add_rack %}
|
||||
@ -114,9 +73,34 @@
|
||||
<li{% if not perms.dcim.view_rackreservation %} class="disabled"{% endif %}>
|
||||
<a href="{% url 'dcim:rackreservation_list' %}">Reservations</a>
|
||||
</li>
|
||||
<li class="divider"></li>
|
||||
<li class="dropdown-header">Tenancy</li>
|
||||
<li{% if not perms.tenancy.view_tenant %} class="disabled"{% endif %}>
|
||||
{% if perms.tenancy.add_tenant %}
|
||||
<div class="buttons pull-right">
|
||||
<a href="{% url 'tenancy:tenant_add' %}" class="btn btn-xs btn-success" title="Add"><i class="fa fa-plus"></i></a>
|
||||
<a href="{% url 'tenancy:tenant_import' %}" class="btn btn-xs btn-info" title="Import"><i class="fa fa-download"></i></a>
|
||||
</div>
|
||||
{% endif %}
|
||||
<a href="{% url 'tenancy:tenant_list' %}">Tenants</a>
|
||||
</li>
|
||||
<li{% if not perms.tenancy.view_tenantgroup %} class="disabled"{% endif %}>
|
||||
{% if perms.tenancy.add_tenantgroup %}
|
||||
<div class="buttons pull-right">
|
||||
<a href="{% url 'tenancy:tenantgroup_add' %}" class="btn btn-xs btn-success" title="Add"><i class="fa fa-plus"></i></a>
|
||||
<a href="{% url 'tenancy:tenantgroup_import' %}" class="btn btn-xs btn-info" title="Import"><i class="fa fa-download"></i></a>
|
||||
</div>
|
||||
{% endif %}
|
||||
<a href="{% url 'tenancy:tenantgroup_list' %}">Tenant Groups</a>
|
||||
</li>
|
||||
<li class="divider"></li>
|
||||
<li class="dropdown-header">Tags</li>
|
||||
<li{% if not perms.extras.view_tag %} class="disabled"{% endif %}>
|
||||
<a href="{% url 'extras:tag_list' %}">Tags</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="dropdown{% if request.path|contains:'/dcim/device,/dcim/virtual-chassis,/dcim/manufacturers/,/dcim/platforms/,/dcim/cable,-connections/,/dcim/inventory-items/' %} active{% endif %}">
|
||||
<li class="dropdown">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Devices <span class="caret"></span></a>
|
||||
<ul class="dropdown-menu">
|
||||
<li class="dropdown-header">Devices</li>
|
||||
@ -201,7 +185,7 @@
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="dropdown{% if request.path|contains:'/ipam/' %} active{% endif %}">
|
||||
<li class="dropdown">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">IPAM <span class="caret"></span></a>
|
||||
<ul class="dropdown-menu">
|
||||
<li class="dropdown-header">IP Addresses</li>
|
||||
@ -292,7 +276,7 @@
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="dropdown{% if request.path|contains:'/virtualization/' %} active{% endif %}">
|
||||
<li class="dropdown">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Virtualization <span class="caret"></span></a>
|
||||
<ul class="dropdown-menu">
|
||||
<li class="dropdown-header">Virtual Machines</li>
|
||||
@ -336,7 +320,7 @@
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="dropdown{% if request.path|contains:'/circuits/' %} active{% endif %}">
|
||||
<li class="dropdown">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Circuits <span class="caret"></span></a>
|
||||
<ul class="dropdown-menu">
|
||||
<li class="dropdown-header">Circuits</li>
|
||||
@ -371,7 +355,7 @@
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="dropdown{% if request.path|contains:'/dcim/power' %} active{% endif %}">
|
||||
<li class="dropdown">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Power <span class="caret"></span></a>
|
||||
<ul class="dropdown-menu">
|
||||
<li class="dropdown-header">Power</li>
|
||||
@ -395,7 +379,7 @@
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="dropdown{% if request.path|contains:'/secrets/' %} active{% endif %}">
|
||||
<li class="dropdown">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Secrets <span class="caret"></span></a>
|
||||
<ul class="dropdown-menu">
|
||||
<li class="dropdown-header">Secrets</li>
|
||||
@ -418,6 +402,26 @@
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="dropdown">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Other <span class="caret"></span></a>
|
||||
<ul class="dropdown-menu">
|
||||
<li class="dropdown-header">Logging</li>
|
||||
<li{% if not perms.extras.view_objectchange %} class="disabled"{% endif %}>
|
||||
<a href="{% url 'extras:objectchange_list' %}">Changelog</a>
|
||||
</li>
|
||||
<li class="divider"></li>
|
||||
<li class="dropdown-header">Miscellaneous</li>
|
||||
<li{% if not perms.extras.view_configcontext %} class="disabled"{% endif %}>
|
||||
<a href="{% url 'extras:configcontext_list' %}">Config Contexts</a>
|
||||
</li>
|
||||
<li{% if not perms.extras.view_script %} class="disabled"{% endif %}>
|
||||
<a href="{% url 'extras:script_list' %}">Scripts</a>
|
||||
</li>
|
||||
<li{% if not perms.extras.view_reportresult %} class="disabled"{% endif %}>
|
||||
<a href="{% url 'extras:report_list' %}">Reports</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
<ul class="nav navbar-nav navbar-right">
|
||||
|
@ -1,4 +1,4 @@
|
||||
{% extends 'utilities/obj_import.html' %}
|
||||
{% extends 'utilities/obj_bulk_import.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
|
@ -142,6 +142,10 @@
|
||||
<h2><a href="{% url 'virtualization:virtualmachine_list' %}?tenant={{ tenant.slug }}" class="btn {% if stats.virtualmachine_count %}btn-primary{% else %}btn-default{% endif %} btn-lg">{{ stats.virtualmachine_count }}</a></h2>
|
||||
<p>Virtual machines</p>
|
||||
</div>
|
||||
<div class="col-md-4 text-center">
|
||||
<h2><a href="{% url 'virtualization:cluster_list' %}?tenant={{ tenant.slug }}" class="btn {% if stats.cluster_count %}btn-primary{% else %}btn-default{% endif %} btn-lg">{{ stats.cluster_count }}</a></h2>
|
||||
<p>Clusters</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
60
netbox/templates/utilities/obj_bulk_import.html
Normal file
60
netbox/templates/utilities/obj_bulk_import.html
Normal file
@ -0,0 +1,60 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load helpers %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block content %}
|
||||
<h1>{% block title %}{{ obj_type|bettertitle }} Bulk Import{% endblock %}</h1>
|
||||
{% block tabs %}{% endblock %}
|
||||
<div class="row">
|
||||
<div class="col-md-7">
|
||||
{% if form.non_field_errors %}
|
||||
<div class="panel panel-danger">
|
||||
<div class="panel-heading"><strong>Errors</strong></div>
|
||||
<div class="panel-body">
|
||||
{{ form.non_field_errors }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<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="{{ return_url }}" class="btn btn-default">Cancel</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
{% if fields %}
|
||||
<h4 class="text-center">CSV Format</h4>
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Required</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
{% for name, field in fields.items %}
|
||||
<tr>
|
||||
<td><code>{{ name }}</code></td>
|
||||
<td>{% if field.required %}<i class="glyphicon glyphicon-ok" title="Required"></i>{% endif %}</td>
|
||||
<td>
|
||||
{{ field.help_text|default:field.label }}
|
||||
{% if field.choices %}
|
||||
<br /><small class="text-muted">Choices: {{ field|example_choices }}</small>
|
||||
{% elif field|widget_type == 'dateinput' %}
|
||||
<br /><small class="text-muted">Format: YYYY-MM-DD</small>
|
||||
{% elif field|widget_type == 'checkboxinput' %}
|
||||
<br /><small class="text-muted">Specify "true" or "false"</small>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
@ -6,7 +6,7 @@
|
||||
<h1>{% block title %}{{ obj_type|bettertitle }} Import{% endblock %}</h1>
|
||||
{% block tabs %}{% endblock %}
|
||||
<div class="row">
|
||||
<div class="col-md-7">
|
||||
<div class="col-md-8 col-md-offset-2">
|
||||
{% if form.non_field_errors %}
|
||||
<div class="panel panel-danger">
|
||||
<div class="panel-heading"><strong>Errors</strong></div>
|
||||
@ -15,12 +15,13 @@
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<form action="" method="post" class="form">
|
||||
<form action="" method="post" class="form form-horizontal">
|
||||
{% 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>
|
||||
<button type="submit" name="_create" class="btn btn-primary">Submit</button>
|
||||
<button type="submit" name="_addanother" class="btn btn-primary">Submit and Import Another</button>
|
||||
{% if return_url %}
|
||||
<a href="{{ return_url }}" class="btn btn-default">Cancel</a>
|
||||
{% endif %}
|
||||
@ -28,33 +29,5 @@
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
{% if fields %}
|
||||
<h4 class="text-center">CSV Format</h4>
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Required</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
{% for name, field in fields.items %}
|
||||
<tr>
|
||||
<td><code>{{ name }}</code></td>
|
||||
<td>{% if field.required %}<i class="glyphicon glyphicon-ok" title="Required"></i>{% endif %}</td>
|
||||
<td>
|
||||
{{ field.help_text|default:field.label }}
|
||||
{% if field.choices %}
|
||||
<br /><small class="text-muted">Choices: {{ field|example_choices }}</small>
|
||||
{% elif field|widget_type == 'dateinput' %}
|
||||
<br /><small class="text-muted">Format: YYYY-MM-DD</small>
|
||||
{% elif field|widget_type == 'checkboxinput' %}
|
||||
<br /><small class="text-muted">Specify "true" or "false"</small>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -83,6 +83,16 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Tenant</td>
|
||||
<td>
|
||||
{% if cluster.tenant %}
|
||||
<a href="{{ cluster.tenant.get_absolute_url }}">{{ cluster.tenant }}</a>
|
||||
{% else %}
|
||||
<span class="text-muted">None</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Site</td>
|
||||
<td>
|
||||
|
@ -8,6 +8,7 @@
|
||||
{% render_field form.name %}
|
||||
{% render_field form.type %}
|
||||
{% render_field form.group %}
|
||||
{% render_field form.tenant %}
|
||||
{% render_field form.site %}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -31,11 +31,12 @@ class TenantSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
virtualmachine_count = serializers.IntegerField(read_only=True)
|
||||
vlan_count = serializers.IntegerField(read_only=True)
|
||||
vrf_count = serializers.IntegerField(read_only=True)
|
||||
cluster_count = serializers.IntegerField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Tenant
|
||||
fields = [
|
||||
'id', 'name', 'slug', 'group', 'description', 'comments', 'tags', 'custom_fields', 'created',
|
||||
'last_updated', 'circuit_count', 'device_count', 'ipaddress_count', 'prefix_count', 'rack_count',
|
||||
'site_count', 'virtualmachine_count', 'vlan_count', 'vrf_count',
|
||||
'site_count', 'virtualmachine_count', 'vlan_count', 'vrf_count', 'cluster_count',
|
||||
]
|
||||
|
@ -9,7 +9,7 @@ from ipam.models import IPAddress, Prefix, VLAN, VRF
|
||||
from utilities.views import (
|
||||
BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
|
||||
)
|
||||
from virtualization.models import VirtualMachine
|
||||
from virtualization.models import VirtualMachine, Cluster
|
||||
from . import filters, forms, tables
|
||||
from .models import Tenant, TenantGroup
|
||||
|
||||
@ -80,6 +80,7 @@ class TenantView(PermissionRequiredMixin, View):
|
||||
'vlan_count': VLAN.objects.filter(tenant=tenant).count(),
|
||||
'circuit_count': Circuit.objects.filter(tenant=tenant).count(),
|
||||
'virtualmachine_count': VirtualMachine.objects.filter(tenant=tenant).count(),
|
||||
'cluster_count': Cluster.objects.filter(tenant=tenant).count(),
|
||||
}
|
||||
|
||||
return render(request, 'tenancy/tenant.html', {
|
||||
|
@ -2,6 +2,7 @@ import csv
|
||||
import json
|
||||
import re
|
||||
from io import StringIO
|
||||
import yaml
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
@ -722,3 +723,41 @@ class BulkEditForm(forms.Form):
|
||||
# Copy any nullable fields defined in Meta
|
||||
if hasattr(self.Meta, 'nullable_fields'):
|
||||
self.nullable_fields = self.Meta.nullable_fields
|
||||
|
||||
|
||||
class ImportForm(BootstrapMixin, forms.Form):
|
||||
"""
|
||||
Generic form for creating an object from JSON/YAML data
|
||||
"""
|
||||
data = forms.CharField(
|
||||
widget=forms.Textarea,
|
||||
help_text="Enter object data in JSON or YAML format."
|
||||
)
|
||||
format = forms.ChoiceField(
|
||||
choices=(
|
||||
('json', 'JSON'),
|
||||
('yaml', 'YAML')
|
||||
),
|
||||
initial='yaml'
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
|
||||
data = self.cleaned_data['data']
|
||||
format = self.cleaned_data['format']
|
||||
|
||||
# Process JSON/YAML data
|
||||
if format == 'json':
|
||||
try:
|
||||
self.cleaned_data['data'] = json.loads(data)
|
||||
except json.decoder.JSONDecodeError as err:
|
||||
raise forms.ValidationError({
|
||||
'data': "Invalid JSON data: {}".format(err)
|
||||
})
|
||||
else:
|
||||
try:
|
||||
self.cleaned_data['data'] = yaml.load(data, Loader=yaml.SafeLoader)
|
||||
except yaml.scanner.ScannerError as err:
|
||||
raise forms.ValidationError({
|
||||
'data': "Invalid YAML data: {}".format(err)
|
||||
})
|
||||
|
@ -1,4 +1,6 @@
|
||||
import json
|
||||
import sys
|
||||
import yaml
|
||||
from copy import deepcopy
|
||||
|
||||
from django.conf import settings
|
||||
@ -24,10 +26,11 @@ from django_tables2 import RequestConfig
|
||||
|
||||
from extras.models import CustomField, CustomFieldValue, ExportTemplate
|
||||
from extras.querysets import CustomFieldQueryset
|
||||
from utilities.exceptions import AbortTransaction
|
||||
from utilities.forms import BootstrapMixin, CSVDataField
|
||||
from utilities.utils import csv_format
|
||||
from .error_handlers import handle_protectederror
|
||||
from .forms import ConfirmationForm
|
||||
from .forms import ConfirmationForm, ImportForm
|
||||
from .paginator import EnhancedPaginator
|
||||
|
||||
|
||||
@ -394,6 +397,106 @@ class BulkCreateView(GetReturnURLMixin, View):
|
||||
})
|
||||
|
||||
|
||||
class ObjectImportView(GetReturnURLMixin, View):
|
||||
"""
|
||||
Import a single object (YAML or JSON format).
|
||||
"""
|
||||
model = None
|
||||
model_form = None
|
||||
related_object_forms = dict()
|
||||
template_name = 'utilities/obj_import.html'
|
||||
|
||||
def get(self, request):
|
||||
|
||||
form = ImportForm()
|
||||
|
||||
return render(request, self.template_name, {
|
||||
'form': form,
|
||||
'obj_type': self.model._meta.verbose_name,
|
||||
'return_url': self.get_return_url(request),
|
||||
})
|
||||
|
||||
def post(self, request):
|
||||
|
||||
form = ImportForm(request.POST)
|
||||
if form.is_valid():
|
||||
|
||||
# Initialize model form
|
||||
data = form.cleaned_data['data']
|
||||
model_form = self.model_form(data)
|
||||
|
||||
# Assign default values for any fields which were not specified. We have to do this manually because passing
|
||||
# 'initial=' to the form on initialization merely sets default values for the widgets. Since widgets are not
|
||||
# used for YAML/JSON import, we first bind the imported data normally, then update the form's data with the
|
||||
# applicable field defaults as needed prior to form validation.
|
||||
for field_name, field in model_form.fields.items():
|
||||
if field_name not in data and hasattr(field, 'initial'):
|
||||
model_form.data[field_name] = field.initial
|
||||
|
||||
if model_form.is_valid():
|
||||
|
||||
try:
|
||||
with transaction.atomic():
|
||||
|
||||
# Save the primary object
|
||||
obj = model_form.save()
|
||||
|
||||
# Iterate through the related object forms (if any), validating and saving each instance.
|
||||
for field_name, related_object_form in self.related_object_forms.items():
|
||||
|
||||
for i, rel_obj_data in enumerate(data.get(field_name, list())):
|
||||
|
||||
f = related_object_form(obj, rel_obj_data)
|
||||
|
||||
for subfield_name, field in f.fields.items():
|
||||
if subfield_name not in rel_obj_data and hasattr(field, 'initial'):
|
||||
f.data[subfield_name] = field.initial
|
||||
|
||||
if f.is_valid():
|
||||
f.save()
|
||||
else:
|
||||
# Replicate errors on the related object form to the primary form for display
|
||||
for subfield_name, errors in f.errors.items():
|
||||
for err in errors:
|
||||
err_msg = "{}[{}] {}: {}".format(field_name, i, subfield_name, err)
|
||||
model_form.add_error(None, err_msg)
|
||||
raise AbortTransaction()
|
||||
|
||||
except AbortTransaction:
|
||||
pass
|
||||
|
||||
if not model_form.errors:
|
||||
|
||||
messages.success(request, mark_safe('Imported object: <a href="{}">{}</a>'.format(
|
||||
obj.get_absolute_url(), obj
|
||||
)))
|
||||
|
||||
if '_addanother' in request.POST:
|
||||
return redirect(request.get_full_path())
|
||||
|
||||
return_url = form.cleaned_data.get('return_url')
|
||||
if return_url is not None and is_safe_url(url=return_url, allowed_hosts=request.get_host()):
|
||||
return redirect(return_url)
|
||||
else:
|
||||
return redirect(self.get_return_url(request, obj))
|
||||
|
||||
else:
|
||||
|
||||
# Replicate model form errors for display
|
||||
for field, errors in model_form.errors.items():
|
||||
for err in errors:
|
||||
if field == '__all__':
|
||||
form.add_error(None, err)
|
||||
else:
|
||||
form.add_error(None, "{}: {}".format(field, err))
|
||||
|
||||
return render(request, self.template_name, {
|
||||
'form': form,
|
||||
'obj_type': self.model._meta.verbose_name,
|
||||
'return_url': self.get_return_url(request),
|
||||
})
|
||||
|
||||
|
||||
class BulkImportView(GetReturnURLMixin, View):
|
||||
"""
|
||||
Import objects in bulk (CSV format).
|
||||
@ -405,7 +508,7 @@ class BulkImportView(GetReturnURLMixin, View):
|
||||
"""
|
||||
model_form = None
|
||||
table = None
|
||||
template_name = 'utilities/obj_import.html'
|
||||
template_name = 'utilities/obj_bulk_import.html'
|
||||
widget_attrs = {}
|
||||
|
||||
def _import_form(self, *args, **kwargs):
|
||||
|
@ -38,6 +38,7 @@ class ClusterGroupSerializer(ValidatedModelSerializer):
|
||||
class ClusterSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
type = NestedClusterTypeSerializer()
|
||||
group = NestedClusterGroupSerializer(required=False, allow_null=True)
|
||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||
site = NestedSiteSerializer(required=False, allow_null=True)
|
||||
tags = TagListSerializerField(required=False)
|
||||
device_count = serializers.IntegerField(read_only=True)
|
||||
@ -46,7 +47,7 @@ class ClusterSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
class Meta:
|
||||
model = Cluster
|
||||
fields = [
|
||||
'id', 'name', 'type', 'group', 'site', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||
'id', 'name', 'type', 'group', 'tenant', 'site', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||
'device_count', 'virtualmachine_count',
|
||||
]
|
||||
|
||||
|
@ -41,7 +41,7 @@ class ClusterGroupViewSet(ModelViewSet):
|
||||
|
||||
class ClusterViewSet(CustomFieldModelViewSet):
|
||||
queryset = Cluster.objects.prefetch_related(
|
||||
'type', 'group', 'site', 'tags'
|
||||
'type', 'group', 'tenant', 'site', 'tags'
|
||||
).annotate(
|
||||
device_count=get_subquery(Device, 'cluster'),
|
||||
virtualmachine_count=get_subquery(VirtualMachine, 'cluster')
|
||||
|
@ -6,6 +6,7 @@ from netaddr.core import AddrFormatError
|
||||
from dcim.models import DeviceRole, Interface, Platform, Region, Site
|
||||
from extras.filters import CustomFieldFilterSet, CreatedUpdatedFilterSet
|
||||
from tenancy.filtersets import TenancyFilterSet
|
||||
from tenancy.models import Tenant
|
||||
from utilities.filters import (
|
||||
MultiValueMACAddressFilter, NameSlugSearchFilterSet, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter,
|
||||
)
|
||||
@ -56,6 +57,10 @@ class ClusterFilter(CustomFieldFilterSet, CreatedUpdatedFilterSet):
|
||||
to_field_name='slug',
|
||||
label='Cluster type (slug)',
|
||||
)
|
||||
tenant = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Tenant.objects.all(),
|
||||
label="Tenant (ID)"
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Site.objects.all(),
|
||||
label='Site (ID)',
|
||||
|
@ -86,7 +86,7 @@ class ClusterForm(BootstrapMixin, CustomFieldForm):
|
||||
class Meta:
|
||||
model = Cluster
|
||||
fields = [
|
||||
'name', 'type', 'group', 'site', 'comments', 'tags',
|
||||
'name', 'type', 'group', 'tenant', 'site', 'comments', 'tags',
|
||||
]
|
||||
widgets = {
|
||||
'type': APISelect(
|
||||
@ -128,6 +128,15 @@ class ClusterCSVForm(forms.ModelForm):
|
||||
'invalid_choice': 'Invalid site name.',
|
||||
}
|
||||
)
|
||||
tenant = forms.ModelChoiceField(
|
||||
queryset=Tenant.objects.all(),
|
||||
to_field_name='name',
|
||||
required=False,
|
||||
help_text='Name of assigned tenant',
|
||||
error_messages={
|
||||
'invalid_choice': 'Invalid tenant name'
|
||||
}
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Cluster
|
||||
@ -153,6 +162,10 @@ class ClusterBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEdit
|
||||
api_url="/api/virtualization/cluster-groups/"
|
||||
)
|
||||
)
|
||||
tenant = forms.ModelChoiceField(
|
||||
queryset=Tenant.objects.all(),
|
||||
required=False
|
||||
)
|
||||
site = forms.ModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
required=False,
|
||||
@ -166,7 +179,7 @@ class ClusterBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEdit
|
||||
|
||||
class Meta:
|
||||
nullable_fields = [
|
||||
'group', 'site', 'comments',
|
||||
'group', 'site', 'comments', 'tenant',
|
||||
]
|
||||
|
||||
|
||||
@ -193,6 +206,15 @@ class ClusterFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
null_option=True,
|
||||
)
|
||||
)
|
||||
tenant = FilterChoiceField(
|
||||
queryset=Tenant.objects.all(),
|
||||
null_label='-- None --',
|
||||
required=False,
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/tenancy/tenants/",
|
||||
null_option=True,
|
||||
)
|
||||
)
|
||||
site = FilterChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
|
18
netbox/virtualization/migrations/0010_cluster_add_tenant.py
Normal file
18
netbox/virtualization/migrations/0010_cluster_add_tenant.py
Normal file
@ -0,0 +1,18 @@
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('tenancy', '0001_initial'),
|
||||
('virtualization', '0009_custom_tag_models'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='cluster',
|
||||
name='tenant',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='clusters', to='tenancy.Tenant'),
|
||||
),
|
||||
]
|
@ -103,6 +103,13 @@ class Cluster(ChangeLoggedModel, CustomFieldModel):
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
tenant = models.ForeignKey(
|
||||
to='tenancy.Tenant',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='clusters',
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
site = models.ForeignKey(
|
||||
to='dcim.Site',
|
||||
on_delete=models.PROTECT,
|
||||
@ -150,6 +157,7 @@ class Cluster(ChangeLoggedModel, CustomFieldModel):
|
||||
self.type.name,
|
||||
self.group.name if self.group else None,
|
||||
self.site.name if self.site else None,
|
||||
self.tenant.name if self.tenant else None,
|
||||
self.comments,
|
||||
)
|
||||
|
||||
|
@ -84,13 +84,14 @@ class ClusterGroupTable(BaseTable):
|
||||
class ClusterTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.LinkColumn()
|
||||
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
|
||||
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')])
|
||||
device_count = tables.Column(accessor=Accessor('devices.count'), orderable=False, verbose_name='Devices')
|
||||
vm_count = tables.Column(accessor=Accessor('virtual_machines.count'), orderable=False, verbose_name='VMs')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Cluster
|
||||
fields = ('pk', 'name', 'type', 'group', 'site', 'device_count', 'vm_count')
|
||||
fields = ('pk', 'name', 'type', 'group', 'tenant', 'site', 'device_count', 'vm_count')
|
||||
|
||||
|
||||
#
|
||||
|
@ -96,7 +96,7 @@ class ClusterGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
|
||||
class ClusterListView(PermissionRequiredMixin, ObjectListView):
|
||||
permission_required = 'virtualization.view_cluster'
|
||||
queryset = Cluster.objects.prefetch_related('type', 'group', 'site')
|
||||
queryset = Cluster.objects.prefetch_related('type', 'group', 'site', 'tenant')
|
||||
table = tables.ClusterTable
|
||||
filter = filters.ClusterFilter
|
||||
filter_form = forms.ClusterFilterForm
|
||||
|
@ -12,7 +12,6 @@ django-taggit-serializer==0.1.7
|
||||
django-timezone-field==3.0
|
||||
djangorestframework==3.9.4
|
||||
drf-yasg[validation]==1.16.0
|
||||
graphviz==0.10.1
|
||||
Jinja2==2.10.1
|
||||
Markdown==2.6.11
|
||||
netaddr==0.7.19
|
||||
|
@ -41,13 +41,21 @@ sed -i -e "s/ALLOWED_HOSTS = \[\]/ALLOWED_HOSTS = \['*'\]/g" $CONFIG
|
||||
sed -i -e "s/SECRET_KEY = ''/SECRET_KEY = 'netboxci'/g" $CONFIG
|
||||
|
||||
# Run NetBox tests
|
||||
./netbox/manage.py test netbox/
|
||||
coverage run --source="netbox/" netbox/manage.py test netbox/
|
||||
RC=$?
|
||||
if [[ $RC != 0 ]]; then
|
||||
echo -e "\n$(info) one or more tests failed, failing build."
|
||||
EXIT=$RC
|
||||
fi
|
||||
|
||||
# Show code coverage report
|
||||
coverage report --skip-covered --omit *migrations*
|
||||
RC=$?
|
||||
if [[ $RC != 0 ]]; then
|
||||
echo -e "\n$(info) failed to generate code coverage report."
|
||||
EXIT=$RC
|
||||
fi
|
||||
|
||||
# Show build duration
|
||||
END=$(date +%s)
|
||||
echo "$(info) exiting with code $EXIT after $(($END - $START)) seconds."
|
||||
|
Loading…
Reference in New Issue
Block a user