Compare commits

..

1 Commits

Author SHA1 Message Date
Jeremy Stretch
9f7313e492 Merge pull request #3661 from netbox-community/develop
Release v2.6.7
2019-11-01 15:43:38 -04:00
961 changed files with 3557 additions and 87593 deletions

4
.gitignore vendored
View File

@@ -12,9 +12,5 @@
fabfile.py fabfile.py
*.swp *.swp
gunicorn_config.py gunicorn_config.py
gunicorn.conf
netbox.log
netbox.pid
.DS_Store .DS_Store
.vscode .vscode
.coverage

View File

@@ -10,7 +10,6 @@ python:
install: install:
- pip install -r requirements.txt - pip install -r requirements.txt
- pip install pycodestyle - pip install pycodestyle
- pip install coverage
before_script: before_script:
- psql --version - psql --version
- psql -U postgres -c 'SELECT version();' - psql -U postgres -c 'SELECT version();'

View File

@@ -36,6 +36,13 @@ Please see [the documentation](http://netbox.readthedocs.io/en/stable/) for
instructions on installing NetBox. To upgrade NetBox, please download the [latest release](https://github.com/netbox-community/netbox/releases) instructions on installing NetBox. To upgrade NetBox, please download the [latest release](https://github.com/netbox-community/netbox/releases)
and run `upgrade.sh`. and run `upgrade.sh`.
## Alternative Installations
* [Docker container](https://github.com/netbox-community/netbox-docker) (via [@cimnine](https://github.com/cimnine))
* [Vagrant deployment](https://github.com/ryanmerolle/netbox-vagrant) (via [@ryanmerolle](https://github.com/ryanmerolle))
* [Ansible deployment](https://github.com/lae/ansible-role-netbox) (via [@lae](https://github.com/lae))
* [Kubernetes deployment](https://github.com/CENGN/netbox-kubernetes) (via [@CENGN](https://github.com/CENGN))
# Providing Feedback # Providing Feedback
Feature requests and bug reports must be submitted as GiHub issues. (Please be Feature requests and bug reports must be submitted as GiHub issues. (Please be

View File

@@ -22,14 +22,14 @@ django-filter
# https://github.com/django-mptt/django-mptt # https://github.com/django-mptt/django-mptt
django-mptt django-mptt
# Prometheus metrics library for Django
# https://github.com/korfuri/django-prometheus
django-prometheus
# Django integration for RQ (Reqis queuing) # Django integration for RQ (Reqis queuing)
# https://github.com/rq/django-rq # https://github.com/rq/django-rq
django-rq django-rq
# Prometheus metrics library for Django
# https://github.com/korfuri/django-prometheus
django-prometheus
# Abstraction models for rendering and paginating HTML tables # Abstraction models for rendering and paginating HTML tables
# https://github.com/jieter/django-tables2 # https://github.com/jieter/django-tables2
django-tables2 django-tables2
@@ -54,6 +54,10 @@ djangorestframework
# https://github.com/axnsan12/drf-yasg # https://github.com/axnsan12/drf-yasg
drf-yasg[validation] drf-yasg[validation]
# Python interface to the graphviz graph rendering utility
# https://github.com/xflr6/graphviz
graphviz
# Simple markup language for rendering HTML # Simple markup language for rendering HTML
# https://github.com/Python-Markdown/markdown # https://github.com/Python-Markdown/markdown
# py-gfm requires Markdown<3.0 # py-gfm requires Markdown<3.0
@@ -78,11 +82,3 @@ py-gfm
# Extensive cryptographic library (fork of pycrypto) # Extensive cryptographic library (fork of pycrypto)
# https://github.com/Legrandin/pycryptodome # https://github.com/Legrandin/pycryptodome
pycryptodome pycryptodome
# In-memory key/value store used for caching and queuing
# https://github.com/andymccurdy/redis-py
redis
# Python Package to write SVG files - used for rack elevations
# https://github.com/mozman/svgwrite
svgwrite

View File

@@ -1,16 +0,0 @@
# The IP address (typically localhost) and port that the Netbox WSGI process should listen on
bind = '127.0.0.1:8001'
# Number of gunicorn workers to spawn. This should typically be 2n+1, where
# n is the number of CPU cores present.
workers = 5
# Number of threads per worker process
threads = 3
# Timeout (in seconds) for a request to complete
timeout = 120
# The maximum number of requests a worker can handle before being respawned
max_requests = 5000
max_requests_jitter = 500

View File

@@ -1,22 +0,0 @@
[Unit]
Description=NetBox Request Queue Worker
Documentation=https://netbox.readthedocs.io/en/stable/
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=www-data
Group=www-data
WorkingDirectory=/opt/netbox
ExecStart=/usr/bin/python3 /opt/netbox/netbox/manage.py rqworker
Restart=on-failure
RestartSec=30
PrivateTmp=true
[Install]
WantedBy=multi-user.target

View File

@@ -1,22 +0,0 @@
[Unit]
Description=NetBox WSGI Service
Documentation=https://netbox.readthedocs.io/en/stable/
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=www-data
Group=www-data
PIDFile=/var/tmp/netbox.pid
WorkingDirectory=/opt/netbox
ExecStart=/usr/local/bin/gunicorn --pid /var/tmp/netbox.pid --pythonpath /opt/netbox/netbox --config /opt/netbox/gunicorn.py netbox.wsgi
Restart=on-failure
RestartSec=30
PrivateTmp=true
[Install]
WantedBy=multi-user.target

View File

@@ -182,7 +182,7 @@ class NewBranchScript(Script):
class Meta: class Meta:
name = "New Branch" name = "New Branch"
description = "Provision a new branch site" description = "Provision a new branch site"
field_order = ['site_name', 'switch_count', 'switch_model'] fields = ['site_name', 'switch_count', 'switch_model']
site_name = StringVar( site_name = StringVar(
description="Name of the new site" description="Name of the new site"

View File

@@ -0,0 +1,17 @@
# 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.

View File

@@ -293,26 +293,6 @@ Session data is used to track authenticated users when they access NetBox. By de
--- ---
## STORAGE_BACKEND
Default: None (local storage)
The backend storage engine for handling uploaded files (e.g. image attachments). NetBox supports integration with the [`django-storages`](https://django-storages.readthedocs.io/en/stable/) package, which provides backends for several popular file storage services. If not configured, local filesystem storage will be used.
The configuration parameters for the specified storage backend are defined under the `STORAGE_CONFIG` setting.
---
## STORAGE_CONFIG
Default: Empty
A dictionary of configuration parameters for the storage backend configured as `STORAGE_BACKEND`. The specific parameters to be used here are specific to each backend; see the [`django-storages` documentation](https://django-storages.readthedocs.io/en/stable/) for more detail.
If `STORAGE_BACKEND` is not defined, this setting will be ignored.
---
## TIME_ZONE ## TIME_ZONE
Default: UTC Default: UTC
@@ -321,6 +301,14 @@ The time zone NetBox will use when dealing with dates and times. It is recommend
--- ---
## WEBHOOKS_ENABLED
Default: False
Enable this option to run the webhook backend. See the docs section on the webhook backend [here](../../additional-features/webhooks/) for more information on setup and use.
---
## Date and Time Formatting ## Date and Time Formatting
You may define custom formatting for date and times. For detailed instructions on writing format strings, please see [the Django documentation](https://docs.djangoproject.com/en/stable/ref/templates/builtins/#date). You may define custom formatting for date and times. For detailed instructions on writing format strings, please see [the Django documentation](https://docs.djangoproject.com/en/stable/ref/templates/builtins/#date).

View File

@@ -25,7 +25,7 @@ NetBox requires access to a PostgreSQL database service to store data. This serv
Example: Example:
```python ```
DATABASE = { DATABASE = {
'NAME': 'netbox', # Database name 'NAME': 'netbox', # Database name
'USER': 'netbox', # PostgreSQL username 'USER': 'netbox', # PostgreSQL username
@@ -42,48 +42,40 @@ DATABASE = {
[Redis](https://redis.io/) is an in-memory data store similar to memcached. While Redis has been an optional component of [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 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). In 2.7, the connection settings were broken down into two sections for functionality (as well as other planned features).
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` and these settings are the same for both of the `webhooks` and `caching` subsections: Redis is configured using a configuration setting similar to `DATABASE`:
* `HOST` - Name or IP address of the Redis server (use `localhost` if running locally) * `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) * `PORT` - TCP port of the Redis service; leave blank for default port (6379)
* `PASSWORD` - Redis password (if set) * `PASSWORD` - Redis password (if set)
* `DATABASE` - Numeric database ID * `DATABASE` - Numeric database ID for webhooks
* `CACHE_DATABASE` - Numeric database ID for caching
* `DEFAULT_TIMEOUT` - Connection timeout in seconds * `DEFAULT_TIMEOUT` - Connection timeout in seconds
* `SSL` - Use SSL connection to Redis * `SSL` - Use SSL connection to Redis
Example: Example:
```python ```
REDIS = { REDIS = {
'webhooks': { 'HOST': 'localhost',
'HOST': 'redis.example.com', 'PORT': 6379,
'PORT': 1234, 'PASSWORD': '',
'PASSWORD': 'foobar', 'DATABASE': 0,
'DATABASE': 0, 'CACHE_DATABASE': 1,
'DEFAULT_TIMEOUT': 300, 'DEFAULT_TIMEOUT': 300,
'SSL': False, 'SSL': False,
},
'caching': {
'HOST': 'localhost',
'PORT': 6379,
'PASSWORD': '',
'DATABASE': 1,
'DEFAULT_TIMEOUT': 300,
'SSL': False,
}
} }
``` ```
!!! note: !!! note:
If you are upgrading from a version prior to v2.7, please note that the Redis connection configuration settings have If you were using these settings in a prior release with webhooks, the `DATABASE` setting remains the same but
changed. Manual modification to bring the `REDIS` section inline with the above specification is necessary 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.
!!! warning: !!! warning:
It is highly recommended to keep the webhook and cache databases separate. Using the same database number on the It is highly recommended to keep the webhook and cache databases seperate. Using the same database number for both may result in webhook
same Redis instance for both may result in webhook processing data being lost during cache flushing events. processing data being lost in cache flushing events.
--- ---

View File

@@ -4,7 +4,7 @@ NetBox requires a PostgreSQL database to store data. This can be hosted locally
The installation instructions provided here have been tested to work on Ubuntu 18.04 and CentOS 7.5. The particular commands needed to install dependencies on other distributions may vary significantly. Unfortunately, this is outside the control of the NetBox maintainers. Please consult your distribution's documentation for assistance with any errors. The installation instructions provided here have been tested to work on Ubuntu 18.04 and CentOS 7.5. The particular commands needed to install dependencies on other distributions may vary significantly. Unfortunately, this is outside the control of the NetBox maintainers. Please consult your distribution's documentation for assistance with any errors.
!!! warning !!! warning
NetBox requires PostgreSQL 9.4 or higher. NetBox v2.2 and later requires PostgreSQL 9.4 or higher.
# Installation # Installation

View File

@@ -5,14 +5,14 @@ This section of the documentation discusses installing and configuring the NetBo
**Ubuntu** **Ubuntu**
```no-highlight ```no-highlight
# 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 # 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
``` ```
**CentOS** **CentOS**
```no-highlight ```no-highlight
# yum install -y epel-release # yum install -y epel-release
# yum install -y gcc python36 python36-devel python36-setuptools libxml2-devel libxslt-devel libffi-devel openssl-devel redhat-rpm-config redis # yum install -y gcc python36 python36-devel python36-setuptools libxml2-devel libxslt-devel libffi-devel graphviz openssl-devel redhat-rpm-config redis
# easy_install-3.6 pip # easy_install-3.6 pip
# ln -s /usr/bin/python36 /usr/bin/python3 # ln -s /usr/bin/python36 /usr/bin/python3
``` ```
@@ -90,14 +90,6 @@ NetBox supports integration with the [NAPALM automation](https://napalm-automati
# pip3 install napalm # pip3 install napalm
``` ```
## Remote File Storage (Optional)
By default, NetBox will use the local filesystem to storage uploaded files. To use a remote filesystem, install the [`django-storages`](https://django-storages.readthedocs.io/en/stable/) library and configure your [desired backend](../../configuration/optional-settings/#storage_backend) in `configuration.py`.
```no-highlight
# pip3 install django-storages
```
# Configuration # Configuration
Move into the NetBox configuration directory and make a copy of `configuration.example.py` named `configuration.py`. Move into the NetBox configuration directory and make a copy of `configuration.example.py` named `configuration.py`.
@@ -147,22 +139,13 @@ Redis is a in-memory key-value store required as part of the NetBox installation
```python ```python
REDIS = { REDIS = {
'webhooks': { 'HOST': 'localhost',
'HOST': 'redis.example.com', 'PORT': 6379,
'PORT': 1234, 'PASSWORD': '',
'PASSWORD': 'foobar', 'DATABASE': 0,
'DATABASE': 0, 'CACHE_DATABASE': 1,
'DEFAULT_TIMEOUT': 300, 'DEFAULT_TIMEOUT': 300,
'SSL': False, 'SSL': False,
},
'caching': {
'HOST': 'localhost',
'PORT': 6379,
'PASSWORD': '',
'DATABASE': 1,
'DEFAULT_TIMEOUT': 300,
'SSL': False,
}
} }
``` ```
@@ -212,7 +195,27 @@ Superuser created successfully.
```no-highlight ```no-highlight
# python3 manage.py collectstatic --no-input # python3 manage.py collectstatic --no-input
959 static files copied to '/opt/netbox/netbox/static'. You have requested to collect static files at the destination
location as specified in your settings:
/opt/netbox/netbox/static
This will overwrite existing files!
Are you sure you want to do this?
Type 'yes' to continue, or 'no' to cancel: yes
```
# Load Initial Data (Optional)
NetBox ships with some initial data to help you get started: RIR definitions, common devices roles, etc. You can delete any seed data that you don't want to keep.
!!! note
This step is optional. It's perfectly fine to start using NetBox without using this initial data if you'd rather create everything from scratch.
```no-highlight
# python3 manage.py loaddata initial_data
Installed 43 object(s) from 4 fixture(s)
``` ```
# Test the Application # Test the Application
@@ -234,11 +237,3 @@ Next, connect to the name or IP of the server (as defined in `ALLOWED_HOSTS`) on
!!! warning !!! warning
If the test service does not run, or you cannot reach the NetBox home page, something has gone wrong. Do not proceed with the rest of this guide until the installation has been corrected. If the test service does not run, or you cannot reach the NetBox home page, something has gone wrong. Do not proceed with the rest of this guide until the installation has been corrected.
Note that the initial UI will be locked down for non-authenticated users.
![NetBox UI as seen by a non-authenticated user](../media/installation/netbox_ui_guest.png)
After logging in as the superuser you created earlier, all areas of the UI will be available.
![NetBox UI as seen by an administrator](../media/installation/netbox_ui_admin.png)

View File

@@ -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 use systemd to enable service persistence. We'll set up a simple WSGI front end using [gunicorn](http://gunicorn.org/) for the purposes of this guide. For web servers, we provide example configurations for both [nginx](https://www.nginx.com/resources/wiki/) and [Apache](http://httpd.apache.org/docs/2.4). (You are of course free to use whichever combination of HTTP and WSGI services you'd like.) We'll also use [supervisord](http://supervisord.org/) to enable service persistence.
!!! info !!! info
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. 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.
@@ -32,6 +32,7 @@ server {
proxy_set_header X-Forwarded-Host $server_name; proxy_set_header X-Forwarded-Host $server_name;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Proto $scheme;
add_header P3P 'CP="ALL DSP COR PSAa PSDa OUR NOR ONL UNI COM NAV"';
} }
} }
``` ```
@@ -107,53 +108,47 @@ Install gunicorn:
# pip3 install gunicorn # pip3 install gunicorn
``` ```
Copy `contrib/gunicorn.conf` to `/opt/netbox/gunicorn.conf`. We make a copy of this file to ensure that any changes to it do not get overwritten by a future upgrade. 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 ```no-highlight
# cp contrib/gunicorn.py /opt/netbox/gunicorn.py command = '/usr/bin/gunicorn'
pythonpath = '/opt/netbox/netbox'
bind = '127.0.0.1:8001'
workers = 3
user = 'www-data'
max_requests = 5000
max_requests_jitter = 500
``` ```
You may wish to edit this file to change the bound IP address or port number, or to make performance-related adjustments. # supervisord Installation
# systemd configuration Install supervisor:
We'll use systemd to control the daemonization of NetBox services. First, copy `contrib/netbox.service` and `contrib/netbox-rq.service` to the `/etc/systemd/system/` directory:
```no-highlight ```no-highlight
# cp contrib/*.service /etc/systemd/system/ # apt-get install -y supervisor
``` ```
!!! note 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`.
These service files assume that gunicorn is installed at `/usr/local/bin/gunicorn`. If the output of `which gunicorn` indicates a different path, you'll need to correct the `ExecStart` path in both files.
Then, start the `netbox` and `netbox-rq` services and enable them to initiate at boot time:
```no-highlight ```no-highlight
# systemctl daemon-reload [program:netbox]
# systemctl start netbox.service command = gunicorn -c /opt/netbox/gunicorn_config.py netbox.wsgi
# systemctl start netbox-rq.service directory = /opt/netbox/netbox/
# systemctl enable netbox.service user = www-data
# systemctl enable netbox-rq.service
[program:netbox-rqworker]
command = python3 /opt/netbox/netbox/manage.py rqworker
directory = /opt/netbox/netbox/
user = www-data
``` ```
You can use the command `systemctl status netbox` to verify that the WSGI service is running: Then, restart the supervisor service to detect and run the gunicorn service:
``` ```no-highlight
# systemctl status netbox.service # service supervisor restart
● netbox.service - NetBox WSGI Service
Loaded: loaded (/etc/systemd/system/netbox.service; enabled; vendor preset: enabled)
Active: active (running) since Thu 2019-12-12 19:23:40 UTC; 25s ago
Docs: https://netbox.readthedocs.io/en/stable/
Main PID: 11993 (gunicorn)
Tasks: 6 (limit: 2362)
CGroup: /system.slice/netbox.service
├─11993 /usr/bin/python3 /usr/local/bin/gunicorn --pid /var/tmp/netbox.pid --pythonpath /opt/netbox/...
├─12015 /usr/bin/python3 /usr/local/bin/gunicorn --pid /var/tmp/netbox.pid --pythonpath /opt/netbox/...
├─12016 /usr/bin/python3 /usr/local/bin/gunicorn --pid /var/tmp/netbox.pid --pythonpath /opt/netbox/...
...
``` ```
At this point, you should be able to connect to the 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. 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.
!!! info !!! info
Please keep in mind that the configurations provided here are bare minimums required to get NetBox up and running. You may want to make adjustments to better suit your production environment. Please keep in mind that the configurations provided here are bare minimums required to get NetBox up and running. You will almost certainly want to make some changes to better suit your production environment.

View File

@@ -12,5 +12,3 @@ 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). 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 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.

View File

@@ -1,100 +0,0 @@
# 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'
```
Finally, start the `netbox` and `netbox-rq` services and enable them to initiate at boot time:
```no-highlight
# systemctl daemon-reload
# systemctl start netbox.service
# systemctl start netbox-rq.service
# systemctl enable netbox.service
# systemctl enable netbox-rq.service
```

View File

@@ -84,12 +84,14 @@ This script:
# Restart the WSGI Service # Restart the WSGI Service
Finally, restart the WSGI services to run the new code. If you followed this guide for the initial installation, this is done using `systemctl: Finally, restart the WSGI service to run the new code. If you followed this guide for the initial installation, this is done using `supervisorctl`:
```no-highlight ```no-highlight
# sudo systemctl restart netbox # sudo supervisorctl restart netbox
# sudo systemctl restart netbox-rqworker
``` ```
!!! note If using webhooks, also restart the Redis worker:
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.
```no-highlight
# sudo supervisorctl restart netbox-rqworker
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

View File

@@ -1 +1 @@
version-2.7.md version-2.6.md

View File

@@ -1,27 +1,3 @@
# v2.6.8 (2019-12-10)
## Enhancements
* [#3139](https://github.com/netbox-community/netbox/issues/3139) - Disable password change form for LDAP-authenticated users
* [#3457](https://github.com/netbox-community/netbox/issues/3457) - Display cable colors on device view
* [#3329](https://github.com/netbox-community/netbox/issues/3329) - Remove obsolete P3P policy header
* [#3663](https://github.com/netbox-community/netbox/issues/3663) - Add query filters for `created` and `last_updated` fields
* [#3722](https://github.com/netbox-community/netbox/issues/3722) - Allow the underscore character in IPAddress DNS names
## Bug Fixes
* [#3312](https://github.com/netbox-community/netbox/issues/3312) - Fix validation error when editing power cables in bulk
* [#3644](https://github.com/netbox-community/netbox/issues/3644) - Fix exception when connecting a cable to a RearPort with no corresponding FrontPort
* [#3669](https://github.com/netbox-community/netbox/issues/3669) - Include `weight` field in prefix/VLAN role form
* [#3674](https://github.com/netbox-community/netbox/issues/3674) - Include comments on PowerFeed view
* [#3679](https://github.com/netbox-community/netbox/issues/3679) - Fix link for assigned ipaddress in interface page
* [#3709](https://github.com/netbox-community/netbox/issues/3709) - Prevent exception when importing an invalid cable definition
* [#3720](https://github.com/netbox-community/netbox/issues/3720) - Correctly indicate power feed terminations on cable list
* [#3724](https://github.com/netbox-community/netbox/issues/3724) - Fix API filtering of interfaces by more than one device name
* [#3725](https://github.com/netbox-community/netbox/issues/3725) - Enforce client validation for minimum service port number
---
# v2.6.7 (2019-11-01) # v2.6.7 (2019-11-01)
## Enhancements ## Enhancements

View File

@@ -1,258 +0,0 @@
# v2.7.0 (FUTURE)
**Note:** NetBox v2.7 is the last major release that will support Python 3.5. Beginning with NetBox v2.8, Python 3.6 or
higher will be required.
## 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 definition written in YAML or
JSON. 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.
### Bulk Import of Device Components ([#822](https://github.com/netbox-community/netbox/issues/822))
NetBox now supports the bulk import of device components such as console ports, power ports, and interfaces across
multiple devices. Device components can be imported in CSV-format.
Here's an example bulk import of interfaces to several devices:
```
device,name,type
Switch1,Vlan100,Virtual
Switch1,Vlan200,Virtual
Switch2,Vlan100,Virtual
Switch2,Vlan200,Virtual
```
### External File Storage ([#1814](https://github.com/netbox-community/netbox/issues/1814))
In prior releases, the only option for storing uploaded files (e.g. image attachments) was to save them to the local
filesystem on the NetBox server. This release introduces support for several remote storage backends provided by the
[`django-storages`](https://django-storages.readthedocs.io/en/stable/) library. These include:
* Amazon S3
* ApacheLibcloud
* Azure Storage
* DigitalOcean Spaces
* Dropbox
* FTP
* Google Cloud Storage
* SFTP
To enable remote file storage, first install `django-storages`:
```
pip install django-storages
```
Then, set the appropriate storage backend and its configuration in `configuration.py`. Here's an example using Amazon
S3:
```python
STORAGE_BACKEND = 'storages.backends.s3boto3.S3Boto3Storage'
STORAGE_CONFIG = {
'AWS_ACCESS_KEY_ID': '<Key>',
'AWS_SECRET_ACCESS_KEY': '<Secret>',
'AWS_STORAGE_BUCKET_NAME': 'netbox',
'AWS_S3_REGION_NAME': 'eu-west-1',
}
```
Thanks to [@steffann](https://github.com/steffann) for contributing this work!
## Changes
### Rack Elevations Rendered via SVG ([#2248](https://github.com/netbox-community/netbox/issues/2248))
NetBox v2.7 introduces a new method of rendering rack elevations as an
[SVG](https://en.wikipedia.org/wiki/Scalable_Vector_Graphics) via a REST API endpoint. This replaces the prior method of
rendering elevations using pure HTML which was cumbersome and had several shortcomings. Allowing elevations to be
rendered as an SVG image in the API allows users to retrieve and make use of the drawings in their own tooling. This
also opens the door to other feature requests related to rack elevations in the NetBox backlog.
This feature implements a new REST API endpoint:
```
/api/dcim/racks/<id>/elevation/
```
By default, this endpoint returns a paginated JSON response representing each rack unit in the given elevation. This is
the same response returned by the rack units detail endpoint and for this reason the rack units endpoint has been
deprecated and will be removed in v2.8 (see [#3753](https://github.com/netbox-community/netbox/issues/3753)):
```
/api/dcim/racks/<id>/units/
```
In order to render the elevation as an SVG, include the `render=svg` query parameter in the request. You may also
control the width of the elevation drawing in pixels with `unit_width=<width in pixels>` and the height of each rack
unit with `unit_height=<height in pixels>`. The `unit_width` defaults to `230` and the `unit_height` default to `20`
which produces elevations the same size as those that appear in the NetBox Web UI. The query parameter `face` is used to
request either the `front` or `rear` of the elevation and defaults to `front`.
Here is an example of the request url for an SVG rendering using the default parameters to render the front of the
elevation:
```
/api/dcim/racks/<id>/elevation/?render=svg
```
Here is an example of the request url for an SVG rendering of the rear of the elevation having a width of 300 pixels and
per unit height of 35 pixels:
```
/api/dcim/racks/<id>/elevation/?render=svg&face=rear&unit_width=300&unit_height=35
```
Thanks to [@hellerve](https://github.com/hellerve) for doing the heavy lifting on this!
### 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.
### WEBHOOKS_ENABLED Configuration Setting Removed ([#3408](https://github.com/netbox-community/netbox/issues/3408))
As `django-rq` is now a required library, NetBox assumes that the RQ worker process is running. The installation and
upgrade documentation has been updated to reflect this, and the `WEBHOOKS_ENABLED` configuration parameter is no longer
used. Please ensure that both the NetBox WSGI service and the RQ worker process are running on all production
installations.
### API Choice Fields Now Use String Values ([#3569](https://github.com/netbox-community/netbox/issues/3569))
NetBox's REST API presents fields which reference a particular choice as a dictionary with two keys: `value` and
`label`. In previous versions, `value` was an integer which represented the particular choice in the database. This has
been changed to a more human-friendly "slug" string, which is essentially a simplified version of the choice's `label`.
For example, The site status field was previously represented as:
```json
"status": {
"value": 1,
"label": "Active"
},
```
Beginning with v2.7.0, it now looks like this:
```json
"status": {
"value": "active",
"label": "Active"
},
```
This change allows for much more intuitive representation of values, and obviates the need for API consumers to maintain
a mapping of static integer values.
Note that that all v2.7 releases will continue to accept the legacy integer values in write requests (POST, PUT, and
PATCH) to maintain backward compatibility. This behavior will be discontinued beginning in v2.8.0.
## Enhancements
* [#33](https://github.com/digitalocean/netbox/issues/33) - Add ability to clone objects (pre-populate form fields)
* [#648](https://github.com/digitalocean/netbox/issues/648) - Pre-populate forms when selecting "create and add another"
* [#792](https://github.com/digitalocean/netbox/issues/792) - Add power port and power outlet types
* [#1865](https://github.com/digitalocean/netbox/issues/1865) - Add console port and console server port types
* [#2669](https://github.com/digitalocean/netbox/issues/2669) - Relax uniqueness constraint on device and VM names
* [#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
* [#3564](https://github.com/digitalocean/netbox/issues/3564) - Add list views for device components
* [#3538](https://github.com/digitalocean/netbox/issues/3538) - Introduce a REST API endpoint for executing custom
scripts
* [#3655](https://github.com/digitalocean/netbox/issues/3655) - Add `description` field to organizational models
* [#3664](https://github.com/digitalocean/netbox/issues/3664) - Enable applying configuration contexts by tags
* [#3706](https://github.com/digitalocean/netbox/issues/3706) - Increase `available_power` maximum value on PowerFeed
* [#3731](https://github.com/digitalocean/netbox/issues/3731) - Change Graph.type to a ContentType foreign key field
## API Changes
* Choice fields now use human-friendly strings for their values instead of integers (see
[#3569](https://github.com/netbox-community/netbox/issues/3569)).
* Introduced `/api/extras/scripts/` endpoint for retrieving and executing custom scripts
* circuits.CircuitType: Added field `description`
* dcim.ConsolePort: Added field `type`
* dcim.ConsolePortTemplate: Added field `type`
* dcim.ConsoleServerPort: Added field `type`
* dcim.ConsoleServerPortTemplate: Added field `type`
* dcim.DeviceRole: Added field `description`
* dcim.PowerPort: Added field `type`
* dcim.PowerPortTemplate: Added field `type`
* dcim.PowerOutlet: Added field `type`
* dcim.PowerOutletTemplate: Added field `type`
* dcim.RackRole: Added field `description`
* extras.Graph: The `type` field has been changed to a content type foreign key. Models are specified as
`<app>.<model>`; e.g. `dcim.site`.
* ipam.Role: Added field `description`
* secrets.SecretRole: Added field `description`
* virtualization.Cluster: Added field `tenant`

View File

@@ -31,7 +31,6 @@ pages:
- Change Logging: 'additional-features/change-logging.md' - Change Logging: 'additional-features/change-logging.md'
- Context Data: 'additional-features/context-data.md' - Context Data: 'additional-features/context-data.md'
- Custom Fields: 'additional-features/custom-fields.md' - Custom Fields: 'additional-features/custom-fields.md'
- Custom Links: 'additional-features/custom-links.md'
- Custom Scripts: 'additional-features/custom-scripts.md' - Custom Scripts: 'additional-features/custom-scripts.md'
- Export Templates: 'additional-features/export-templates.md' - Export Templates: 'additional-features/export-templates.md'
- Graphs: 'additional-features/graphs.md' - Graphs: 'additional-features/graphs.md'

View File

@@ -1,7 +1,7 @@
from rest_framework import serializers from rest_framework import serializers
from taggit_serializer.serializers import TaggitSerializer, TagListSerializerField from taggit_serializer.serializers import TaggitSerializer, TagListSerializerField
from circuits.choices import CircuitStatusChoices from circuits.constants import CIRCUIT_STATUS_CHOICES
from circuits.models import Provider, Circuit, CircuitTermination, CircuitType from circuits.models import Provider, Circuit, CircuitTermination, CircuitType
from dcim.api.nested_serializers import NestedCableSerializer, NestedSiteSerializer from dcim.api.nested_serializers import NestedCableSerializer, NestedSiteSerializer
from dcim.api.serializers import ConnectedEndpointSerializer from dcim.api.serializers import ConnectedEndpointSerializer
@@ -36,12 +36,12 @@ class CircuitTypeSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = CircuitType model = CircuitType
fields = ['id', 'name', 'slug', 'description', 'circuit_count'] fields = ['id', 'name', 'slug', 'circuit_count']
class CircuitSerializer(TaggitSerializer, CustomFieldModelSerializer): class CircuitSerializer(TaggitSerializer, CustomFieldModelSerializer):
provider = NestedProviderSerializer() provider = NestedProviderSerializer()
status = ChoiceField(choices=CircuitStatusChoices, required=False) status = ChoiceField(choices=CIRCUIT_STATUS_CHOICES, required=False)
type = NestedCircuitTypeSerializer() type = NestedCircuitTypeSerializer()
tenant = NestedTenantSerializer(required=False, allow_null=True) tenant = NestedTenantSerializer(required=False, allow_null=True)
tags = TagListSerializerField(required=False) tags = TagListSerializerField(required=False)

View File

@@ -7,7 +7,7 @@ from circuits import filters
from circuits.models import Provider, CircuitTermination, CircuitType, Circuit from circuits.models import Provider, CircuitTermination, CircuitType, Circuit
from extras.api.serializers import RenderedGraphSerializer from extras.api.serializers import RenderedGraphSerializer
from extras.api.views import CustomFieldModelViewSet from extras.api.views import CustomFieldModelViewSet
from extras.models import Graph from extras.models import Graph, GRAPH_TYPE_PROVIDER
from utilities.api import FieldChoicesViewSet, ModelViewSet from utilities.api import FieldChoicesViewSet, ModelViewSet
from . import serializers from . import serializers
@@ -40,7 +40,7 @@ class ProviderViewSet(CustomFieldModelViewSet):
A convenience method for rendering graphs for a particular provider. A convenience method for rendering graphs for a particular provider.
""" """
provider = get_object_or_404(Provider, pk=pk) provider = get_object_or_404(Provider, pk=pk)
queryset = Graph.objects.filter(type__model='provider') queryset = Graph.objects.filter(type=GRAPH_TYPE_PROVIDER)
serializer = RenderedGraphSerializer(queryset, many=True, context={'graphed_object': provider}) serializer = RenderedGraphSerializer(queryset, many=True, context={'graphed_object': provider})
return Response(serializer.data) return Response(serializer.data)

View File

@@ -1,48 +0,0 @@
from utilities.choices import ChoiceSet
#
# Circuits
#
class CircuitStatusChoices(ChoiceSet):
STATUS_DEPROVISIONING = 'deprovisioning'
STATUS_ACTIVE = 'active'
STATUS_PLANNED = 'planned'
STATUS_PROVISIONING = 'provisioning'
STATUS_OFFLINE = 'offline'
STATUS_DECOMMISSIONED = 'decommissioned'
CHOICES = (
(STATUS_PLANNED, 'Planned'),
(STATUS_PROVISIONING, 'Provisioning'),
(STATUS_ACTIVE, 'Active'),
(STATUS_OFFLINE, 'Offline'),
(STATUS_DEPROVISIONING, 'Deprovisioning'),
(STATUS_DECOMMISSIONED, 'Decommissioned'),
)
LEGACY_MAP = {
STATUS_DEPROVISIONING: 0,
STATUS_ACTIVE: 1,
STATUS_PLANNED: 2,
STATUS_PROVISIONING: 3,
STATUS_OFFLINE: 4,
STATUS_DECOMMISSIONED: 5,
}
#
# CircuitTerminations
#
class CircuitTerminationSideChoices(ChoiceSet):
SIDE_A = 'A'
SIDE_Z = 'Z'
CHOICES = (
(SIDE_A, 'A'),
(SIDE_Z, 'Z')
)

View File

@@ -0,0 +1,23 @@
# Circuit statuses
CIRCUIT_STATUS_DEPROVISIONING = 0
CIRCUIT_STATUS_ACTIVE = 1
CIRCUIT_STATUS_PLANNED = 2
CIRCUIT_STATUS_PROVISIONING = 3
CIRCUIT_STATUS_OFFLINE = 4
CIRCUIT_STATUS_DECOMMISSIONED = 5
CIRCUIT_STATUS_CHOICES = [
[CIRCUIT_STATUS_PLANNED, 'Planned'],
[CIRCUIT_STATUS_PROVISIONING, 'Provisioning'],
[CIRCUIT_STATUS_ACTIVE, 'Active'],
[CIRCUIT_STATUS_OFFLINE, 'Offline'],
[CIRCUIT_STATUS_DEPROVISIONING, 'Deprovisioning'],
[CIRCUIT_STATUS_DECOMMISSIONED, 'Decommissioned'],
]
# CircuitTermination sides
TERM_SIDE_A = 'A'
TERM_SIDE_Z = 'Z'
TERM_SIDE_CHOICES = (
(TERM_SIDE_A, 'A'),
(TERM_SIDE_Z, 'Z'),
)

View File

@@ -2,14 +2,14 @@ import django_filters
from django.db.models import Q from django.db.models import Q
from dcim.models import Region, Site from dcim.models import Region, Site
from extras.filters import CustomFieldFilterSet, CreatedUpdatedFilterSet from extras.filters import CustomFieldFilterSet
from tenancy.filtersets import TenancyFilterSet from tenancy.filtersets import TenancyFilterSet
from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter
from .choices import * from .constants import *
from .models import Circuit, CircuitTermination, CircuitType, Provider from .models import Circuit, CircuitTermination, CircuitType, Provider
class ProviderFilter(CustomFieldFilterSet, CreatedUpdatedFilterSet): class ProviderFilter(CustomFieldFilterSet):
id__in = NumericInFilter( id__in = NumericInFilter(
field_name='id', field_name='id',
lookup_expr='in' lookup_expr='in'
@@ -54,7 +54,7 @@ class CircuitTypeFilter(NameSlugSearchFilterSet):
fields = ['id', 'name', 'slug'] fields = ['id', 'name', 'slug']
class CircuitFilter(CustomFieldFilterSet, TenancyFilterSet, CreatedUpdatedFilterSet): class CircuitFilter(CustomFieldFilterSet, TenancyFilterSet):
id__in = NumericInFilter( id__in = NumericInFilter(
field_name='id', field_name='id',
lookup_expr='in' lookup_expr='in'
@@ -84,7 +84,7 @@ class CircuitFilter(CustomFieldFilterSet, TenancyFilterSet, CreatedUpdatedFilter
label='Circuit type (slug)', label='Circuit type (slug)',
) )
status = django_filters.MultipleChoiceFilter( status = django_filters.MultipleChoiceFilter(
choices=CircuitStatusChoices, choices=CIRCUIT_STATUS_CHOICES,
null_value=None null_value=None
) )
site_id = django_filters.ModelMultipleChoiceFilter( site_id = django_filters.ModelMultipleChoiceFilter(

View File

@@ -0,0 +1,26 @@
[
{
"model": "circuits.circuittype",
"pk": 1,
"fields": {
"name": "Internet",
"slug": "internet"
}
},
{
"model": "circuits.circuittype",
"pk": 2,
"fields": {
"name": "Private WAN",
"slug": "private-wan"
}
},
{
"model": "circuits.circuittype",
"pk": 3,
"fields": {
"name": "Out-of-Band",
"slug": "out-of-band"
}
}
]

View File

@@ -9,7 +9,7 @@ from utilities.forms import (
APISelect, APISelectMultiple, add_blank_choice, BootstrapMixin, CommentField, CSVChoiceField, APISelect, APISelectMultiple, add_blank_choice, BootstrapMixin, CommentField, CSVChoiceField,
FilterChoiceField, SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple FilterChoiceField, SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple
) )
from .choices import CircuitStatusChoices from .constants import *
from .models import Circuit, CircuitTermination, CircuitType, Provider from .models import Circuit, CircuitTermination, CircuitType, Provider
@@ -128,7 +128,7 @@ class CircuitTypeForm(BootstrapMixin, forms.ModelForm):
class Meta: class Meta:
model = CircuitType model = CircuitType
fields = [ fields = [
'name', 'slug', 'description', 'name', 'slug',
] ]
@@ -194,7 +194,7 @@ class CircuitCSVForm(forms.ModelForm):
} }
) )
status = CSVChoiceField( status = CSVChoiceField(
choices=CircuitStatusChoices, choices=CIRCUIT_STATUS_CHOICES,
required=False, required=False,
help_text='Operational status' help_text='Operational status'
) )
@@ -235,7 +235,7 @@ class CircuitBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEdit
) )
) )
status = forms.ChoiceField( status = forms.ChoiceField(
choices=add_blank_choice(CircuitStatusChoices), choices=add_blank_choice(CIRCUIT_STATUS_CHOICES),
required=False, required=False,
initial='', initial='',
widget=StaticSelect2() widget=StaticSelect2()
@@ -292,7 +292,7 @@ class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm
) )
) )
status = forms.MultipleChoiceField( status = forms.MultipleChoiceField(
choices=CircuitStatusChoices, choices=CIRCUIT_STATUS_CHOICES,
required=False, required=False,
widget=StaticSelect2Multiple() widget=StaticSelect2Multiple()
) )

View File

@@ -1,39 +0,0 @@
from django.db import migrations, models
CIRCUIT_STATUS_CHOICES = (
(0, 'deprovisioning'),
(1, 'active'),
(2, 'planned'),
(3, 'provisioning'),
(4, 'offline'),
(5, 'decommissioned')
)
def circuit_status_to_slug(apps, schema_editor):
Circuit = apps.get_model('circuits', 'Circuit')
for id, slug in CIRCUIT_STATUS_CHOICES:
Circuit.objects.filter(status=str(id)).update(status=slug)
class Migration(migrations.Migration):
atomic = False
dependencies = [
('circuits', '0015_custom_tag_models'),
]
operations = [
# Circuit.status
migrations.AlterField(
model_name='circuit',
name='status',
field=models.CharField(default='active', max_length=50),
),
migrations.RunPython(
code=circuit_status_to_slug
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 2.2.6 on 2019-12-10 18:19
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('circuits', '0016_3569_circuit_fields'),
]
operations = [
migrations.AddField(
model_name='circuittype',
name='description',
field=models.CharField(blank=True, max_length=100),
),
]

View File

@@ -3,13 +3,13 @@ from django.db import models
from django.urls import reverse from django.urls import reverse
from taggit.managers import TaggableManager from taggit.managers import TaggableManager
from dcim.constants import CONNECTION_STATUS_CHOICES from dcim.constants import CONNECTION_STATUS_CHOICES, STATUS_CLASSES
from dcim.fields import ASNField from dcim.fields import ASNField
from dcim.models import CableTermination from dcim.models import CableTermination
from extras.models import CustomFieldModel, ObjectChange, TaggedItem from extras.models import CustomFieldModel, ObjectChange, TaggedItem
from utilities.models import ChangeLoggedModel from utilities.models import ChangeLoggedModel
from utilities.utils import serialize_object from utilities.utils import serialize_object
from .choices import * from .constants import *
class Provider(ChangeLoggedModel, CustomFieldModel): class Provider(ChangeLoggedModel, CustomFieldModel):
@@ -57,12 +57,7 @@ class Provider(ChangeLoggedModel, CustomFieldModel):
tags = TaggableManager(through=TaggedItem) tags = TaggableManager(through=TaggedItem)
csv_headers = [ csv_headers = ['name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments']
'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments',
]
clone_fields = [
'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact',
]
class Meta: class Meta:
ordering = ['name'] ordering = ['name']
@@ -98,12 +93,8 @@ class CircuitType(ChangeLoggedModel):
slug = models.SlugField( slug = models.SlugField(
unique=True unique=True
) )
description = models.CharField(
max_length=100,
blank=True,
)
csv_headers = ['name', 'slug', 'description'] csv_headers = ['name', 'slug']
class Meta: class Meta:
ordering = ['name'] ordering = ['name']
@@ -118,7 +109,6 @@ class CircuitType(ChangeLoggedModel):
return ( return (
self.name, self.name,
self.slug, self.slug,
self.description,
) )
@@ -142,10 +132,9 @@ class Circuit(ChangeLoggedModel, CustomFieldModel):
on_delete=models.PROTECT, on_delete=models.PROTECT,
related_name='circuits' related_name='circuits'
) )
status = models.CharField( status = models.PositiveSmallIntegerField(
max_length=50, choices=CIRCUIT_STATUS_CHOICES,
choices=CircuitStatusChoices, default=CIRCUIT_STATUS_ACTIVE
default=CircuitStatusChoices.STATUS_ACTIVE
) )
tenant = models.ForeignKey( tenant = models.ForeignKey(
to='tenancy.Tenant', to='tenancy.Tenant',
@@ -181,18 +170,6 @@ class Circuit(ChangeLoggedModel, CustomFieldModel):
csv_headers = [ csv_headers = [
'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description', 'comments', 'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description', 'comments',
] ]
clone_fields = [
'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description',
]
STATUS_CLASS_MAP = {
CircuitStatusChoices.STATUS_DEPROVISIONING: 'warning',
CircuitStatusChoices.STATUS_ACTIVE: 'success',
CircuitStatusChoices.STATUS_PLANNED: 'info',
CircuitStatusChoices.STATUS_PROVISIONING: 'primary',
CircuitStatusChoices.STATUS_OFFLINE: 'danger',
CircuitStatusChoices.STATUS_DECOMMISSIONED: 'default',
}
class Meta: class Meta:
ordering = ['provider', 'cid'] ordering = ['provider', 'cid']
@@ -218,7 +195,7 @@ class Circuit(ChangeLoggedModel, CustomFieldModel):
) )
def get_status_class(self): def get_status_class(self):
return self.STATUS_CLASS_MAP.get(self.status) return STATUS_CLASSES[self.status]
def _get_termination(self, side): def _get_termination(self, side):
for ct in self.terminations.all(): for ct in self.terminations.all():
@@ -243,7 +220,7 @@ class CircuitTermination(CableTermination):
) )
term_side = models.CharField( term_side = models.CharField(
max_length=1, max_length=1,
choices=CircuitTerminationSideChoices, choices=TERM_SIDE_CHOICES,
verbose_name='Termination' verbose_name='Termination'
) )
site = models.ForeignKey( site = models.ForeignKey(

View File

@@ -50,14 +50,12 @@ class CircuitTypeTable(BaseTable):
name = tables.LinkColumn() name = tables.LinkColumn()
circuit_count = tables.Column(verbose_name='Circuits') circuit_count = tables.Column(verbose_name='Circuits')
actions = tables.TemplateColumn( actions = tables.TemplateColumn(
template_code=CIRCUITTYPE_ACTIONS, template_code=CIRCUITTYPE_ACTIONS, attrs={'td': {'class': 'text-right noprint'}}, verbose_name=''
attrs={'td': {'class': 'text-right noprint'}},
verbose_name=''
) )
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = CircuitType model = CircuitType
fields = ('pk', 'name', 'circuit_count', 'description', 'slug', 'actions') fields = ('pk', 'name', 'circuit_count', 'slug', 'actions')
# #

View File

@@ -1,10 +1,10 @@
from django.contrib.contenttypes.models import ContentType
from django.urls import reverse from django.urls import reverse
from rest_framework import status from rest_framework import status
from circuits.choices import * from circuits.constants import CIRCUIT_STATUS_ACTIVE, TERM_SIDE_A, TERM_SIDE_Z
from circuits.models import Circuit, CircuitTermination, CircuitType, Provider from circuits.models import Circuit, CircuitTermination, CircuitType, Provider
from dcim.models import Site from dcim.models import Device, DeviceRole, DeviceType, Interface, Manufacturer, Site
from extras.constants import GRAPH_TYPE_PROVIDER
from extras.models import Graph from extras.models import Graph
from utilities.testing import APITestCase from utilities.testing import APITestCase
@@ -28,20 +28,16 @@ class ProviderTest(APITestCase):
def test_get_provider_graphs(self): def test_get_provider_graphs(self):
provider_ct = ContentType.objects.get(app_label='circuits', model='provider')
self.graph1 = Graph.objects.create( self.graph1 = Graph.objects.create(
type=provider_ct, type=GRAPH_TYPE_PROVIDER, name='Test Graph 1',
name='Test Graph 1',
source='http://example.com/graphs.py?provider={{ obj.slug }}&foo=1' source='http://example.com/graphs.py?provider={{ obj.slug }}&foo=1'
) )
self.graph2 = Graph.objects.create( self.graph2 = Graph.objects.create(
type=provider_ct, type=GRAPH_TYPE_PROVIDER, name='Test Graph 2',
name='Test Graph 2',
source='http://example.com/graphs.py?provider={{ obj.slug }}&foo=2' source='http://example.com/graphs.py?provider={{ obj.slug }}&foo=2'
) )
self.graph3 = Graph.objects.create( self.graph3 = Graph.objects.create(
type=provider_ct, type=GRAPH_TYPE_PROVIDER, name='Test Graph 3',
name='Test Graph 3',
source='http://example.com/graphs.py?provider={{ obj.slug }}&foo=3' source='http://example.com/graphs.py?provider={{ obj.slug }}&foo=3'
) )
@@ -254,7 +250,7 @@ class CircuitTest(APITestCase):
'cid': 'TEST0004', 'cid': 'TEST0004',
'provider': self.provider1.pk, 'provider': self.provider1.pk,
'type': self.circuittype1.pk, 'type': self.circuittype1.pk,
'status': CircuitStatusChoices.STATUS_ACTIVE, 'status': CIRCUIT_STATUS_ACTIVE,
} }
url = reverse('circuits-api:circuit-list') url = reverse('circuits-api:circuit-list')
@@ -274,19 +270,19 @@ class CircuitTest(APITestCase):
'cid': 'TEST0004', 'cid': 'TEST0004',
'provider': self.provider1.pk, 'provider': self.provider1.pk,
'type': self.circuittype1.pk, 'type': self.circuittype1.pk,
'status': CircuitStatusChoices.STATUS_ACTIVE, 'status': CIRCUIT_STATUS_ACTIVE,
}, },
{ {
'cid': 'TEST0005', 'cid': 'TEST0005',
'provider': self.provider1.pk, 'provider': self.provider1.pk,
'type': self.circuittype1.pk, 'type': self.circuittype1.pk,
'status': CircuitStatusChoices.STATUS_ACTIVE, 'status': CIRCUIT_STATUS_ACTIVE,
}, },
{ {
'cid': 'TEST0006', 'cid': 'TEST0006',
'provider': self.provider1.pk, 'provider': self.provider1.pk,
'type': self.circuittype1.pk, 'type': self.circuittype1.pk,
'status': CircuitStatusChoices.STATUS_ACTIVE, 'status': CIRCUIT_STATUS_ACTIVE,
}, },
] ]
@@ -340,28 +336,16 @@ class CircuitTerminationTest(APITestCase):
self.circuit2 = Circuit.objects.create(cid='TEST0002', provider=provider, type=circuittype) self.circuit2 = Circuit.objects.create(cid='TEST0002', provider=provider, type=circuittype)
self.circuit3 = Circuit.objects.create(cid='TEST0003', provider=provider, type=circuittype) self.circuit3 = Circuit.objects.create(cid='TEST0003', provider=provider, type=circuittype)
self.circuittermination1 = CircuitTermination.objects.create( self.circuittermination1 = CircuitTermination.objects.create(
circuit=self.circuit1, circuit=self.circuit1, term_side=TERM_SIDE_A, site=self.site1, port_speed=1000000
term_side=CircuitTerminationSideChoices.SIDE_A,
site=self.site1,
port_speed=1000000
) )
self.circuittermination2 = CircuitTermination.objects.create( self.circuittermination2 = CircuitTermination.objects.create(
circuit=self.circuit1, circuit=self.circuit1, term_side=TERM_SIDE_Z, site=self.site2, port_speed=1000000
term_side=CircuitTerminationSideChoices.SIDE_Z,
site=self.site2,
port_speed=1000000
) )
self.circuittermination3 = CircuitTermination.objects.create( self.circuittermination3 = CircuitTermination.objects.create(
circuit=self.circuit2, circuit=self.circuit2, term_side=TERM_SIDE_A, site=self.site1, port_speed=1000000
term_side=CircuitTerminationSideChoices.SIDE_A,
site=self.site1,
port_speed=1000000
) )
self.circuittermination4 = CircuitTermination.objects.create( self.circuittermination4 = CircuitTermination.objects.create(
circuit=self.circuit2, circuit=self.circuit2, term_side=TERM_SIDE_Z, site=self.site2, port_speed=1000000
term_side=CircuitTerminationSideChoices.SIDE_Z,
site=self.site2,
port_speed=1000000
) )
def test_get_circuittermination(self): def test_get_circuittermination(self):
@@ -382,7 +366,7 @@ class CircuitTerminationTest(APITestCase):
data = { data = {
'circuit': self.circuit3.pk, 'circuit': self.circuit3.pk,
'term_side': CircuitTerminationSideChoices.SIDE_A, 'term_side': TERM_SIDE_A,
'site': self.site1.pk, 'site': self.site1.pk,
'port_speed': 1000000, 'port_speed': 1000000,
} }
@@ -401,15 +385,12 @@ class CircuitTerminationTest(APITestCase):
def test_update_circuittermination(self): def test_update_circuittermination(self):
circuittermination5 = CircuitTermination.objects.create( circuittermination5 = CircuitTermination.objects.create(
circuit=self.circuit3, circuit=self.circuit3, term_side=TERM_SIDE_A, site=self.site1, port_speed=1000000
term_side=CircuitTerminationSideChoices.SIDE_A,
site=self.site1,
port_speed=1000000
) )
data = { data = {
'circuit': self.circuit3.pk, 'circuit': self.circuit3.pk,
'term_side': CircuitTerminationSideChoices.SIDE_Z, 'term_side': TERM_SIDE_Z,
'site': self.site2.pk, 'site': self.site2.pk,
'port_speed': 1000000, 'port_speed': 1000000,
} }

View File

@@ -10,12 +10,7 @@ from utilities.testing import create_test_user
class ProviderTestCase(TestCase): class ProviderTestCase(TestCase):
def setUp(self): def setUp(self):
user = create_test_user( user = create_test_user(permissions=['circuits.view_provider'])
permissions=[
'circuits.view_provider',
'circuits.add_provider',
]
)
self.client = Client() self.client = Client()
self.client.force_login(user) self.client.force_login(user)
@@ -41,30 +36,11 @@ class ProviderTestCase(TestCase):
response = self.client.get(provider.get_absolute_url()) response = self.client.get(provider.get_absolute_url())
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
def test_provider_import(self):
csv_data = (
"name,slug",
"Provider 4,provider-4",
"Provider 5,provider-5",
"Provider 6,provider-6",
)
response = self.client.post(reverse('circuits:provider_import'), {'csv': '\n'.join(csv_data)})
self.assertEqual(response.status_code, 200)
self.assertEqual(Provider.objects.count(), 6)
class CircuitTypeTestCase(TestCase): class CircuitTypeTestCase(TestCase):
def setUp(self): def setUp(self):
user = create_test_user( user = create_test_user(permissions=['circuits.view_circuittype'])
permissions=[
'circuits.view_circuittype',
'circuits.add_circuittype',
]
)
self.client = Client() self.client = Client()
self.client.force_login(user) self.client.force_login(user)
@@ -81,30 +57,11 @@ class CircuitTypeTestCase(TestCase):
response = self.client.get(url) response = self.client.get(url)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
def test_circuittype_import(self):
csv_data = (
"name,slug",
"Circuit Type 4,circuit-type-4",
"Circuit Type 5,circuit-type-5",
"Circuit Type 6,circuit-type-6",
)
response = self.client.post(reverse('circuits:circuittype_import'), {'csv': '\n'.join(csv_data)})
self.assertEqual(response.status_code, 200)
self.assertEqual(CircuitType.objects.count(), 6)
class CircuitTestCase(TestCase): class CircuitTestCase(TestCase):
def setUp(self): def setUp(self):
user = create_test_user( user = create_test_user(permissions=['circuits.view_circuit'])
permissions=[
'circuits.view_circuit',
'circuits.add_circuit',
]
)
self.client = Client() self.client = Client()
self.client.force_login(user) self.client.force_login(user)
@@ -136,17 +93,3 @@ class CircuitTestCase(TestCase):
circuit = Circuit.objects.first() circuit = Circuit.objects.first()
response = self.client.get(circuit.get_absolute_url()) response = self.client.get(circuit.get_absolute_url())
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
def test_circuit_import(self):
csv_data = (
"cid,provider,type",
"Circuit 4,Provider 1,Circuit Type 1",
"Circuit 5,Provider 1,Circuit Type 1",
"Circuit 6,Provider 1,Circuit Type 1",
)
response = self.client.post(reverse('circuits:circuit_import'), {'csv': '\n'.join(csv_data)})
self.assertEqual(response.status_code, 200)
self.assertEqual(Circuit.objects.count(), 6)

View File

@@ -6,13 +6,13 @@ from django.db.models import Count, OuterRef, Subquery
from django.shortcuts import get_object_or_404, redirect, render from django.shortcuts import get_object_or_404, redirect, render
from django.views.generic import View from django.views.generic import View
from extras.models import Graph from extras.models import Graph, GRAPH_TYPE_PROVIDER
from utilities.forms import ConfirmationForm from utilities.forms import ConfirmationForm
from utilities.views import ( from utilities.views import (
BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView, BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
) )
from . import filters, forms, tables from . import filters, forms, tables
from .choices import CircuitTerminationSideChoices from .constants import TERM_SIDE_A, TERM_SIDE_Z
from .models import Circuit, CircuitTermination, CircuitType, Provider from .models import Circuit, CircuitTermination, CircuitType, Provider
@@ -36,7 +36,7 @@ class ProviderView(PermissionRequiredMixin, View):
provider = get_object_or_404(Provider, slug=slug) provider = get_object_or_404(Provider, slug=slug)
circuits = Circuit.objects.filter(provider=provider).prefetch_related('type', 'tenant', 'terminations__site') circuits = Circuit.objects.filter(provider=provider).prefetch_related('type', 'tenant', 'terminations__site')
show_graphs = Graph.objects.filter(type__model='provider').exists() show_graphs = Graph.objects.filter(type=GRAPH_TYPE_PROVIDER).exists()
return render(request, 'circuits/provider.html', { return render(request, 'circuits/provider.html', {
'provider': provider, 'provider': provider,
@@ -151,12 +151,12 @@ class CircuitView(PermissionRequiredMixin, View):
termination_a = CircuitTermination.objects.prefetch_related( termination_a = CircuitTermination.objects.prefetch_related(
'site__region', 'connected_endpoint__device' 'site__region', 'connected_endpoint__device'
).filter( ).filter(
circuit=circuit, term_side=CircuitTerminationSideChoices.SIDE_A circuit=circuit, term_side=TERM_SIDE_A
).first() ).first()
termination_z = CircuitTermination.objects.prefetch_related( termination_z = CircuitTermination.objects.prefetch_related(
'site__region', 'connected_endpoint__device' 'site__region', 'connected_endpoint__device'
).filter( ).filter(
circuit=circuit, term_side=CircuitTerminationSideChoices.SIDE_Z circuit=circuit, term_side=TERM_SIDE_Z
).first() ).first()
return render(request, 'circuits/circuit.html', { return render(request, 'circuits/circuit.html', {
@@ -212,12 +212,8 @@ class CircuitBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
def circuit_terminations_swap(request, pk): def circuit_terminations_swap(request, pk):
circuit = get_object_or_404(Circuit, pk=pk) circuit = get_object_or_404(Circuit, pk=pk)
termination_a = CircuitTermination.objects.filter( termination_a = CircuitTermination.objects.filter(circuit=circuit, term_side=TERM_SIDE_A).first()
circuit=circuit, term_side=CircuitTerminationSideChoices.SIDE_A termination_z = CircuitTermination.objects.filter(circuit=circuit, term_side=TERM_SIDE_Z).first()
).first()
termination_z = CircuitTermination.objects.filter(
circuit=circuit, term_side=CircuitTerminationSideChoices.SIDE_Z
).first()
if not termination_a and not termination_z: if not termination_a and not termination_z:
messages.error(request, "No terminations have been defined for circuit {}.".format(circuit)) messages.error(request, "No terminations have been defined for circuit {}.".format(circuit))
return redirect('circuits:circuit', pk=circuit.pk) return redirect('circuits:circuit', pk=circuit.pk)

View File

@@ -4,7 +4,6 @@ from rest_framework import serializers
from rest_framework.validators import UniqueTogetherValidator from rest_framework.validators import UniqueTogetherValidator
from taggit_serializer.serializers import TaggitSerializer, TagListSerializerField from taggit_serializer.serializers import TaggitSerializer, TagListSerializerField
from dcim.choices import *
from dcim.constants import * from dcim.constants import *
from dcim.models import ( from dcim.models import (
Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
@@ -68,7 +67,7 @@ class RegionSerializer(serializers.ModelSerializer):
class SiteSerializer(TaggitSerializer, CustomFieldModelSerializer): class SiteSerializer(TaggitSerializer, CustomFieldModelSerializer):
status = ChoiceField(choices=SiteStatusChoices, required=False) status = ChoiceField(choices=SITE_STATUS_CHOICES, required=False)
region = NestedRegionSerializer(required=False, allow_null=True) region = NestedRegionSerializer(required=False, allow_null=True)
tenant = NestedTenantSerializer(required=False, allow_null=True) tenant = NestedTenantSerializer(required=False, allow_null=True)
time_zone = TimeZoneField(required=False) time_zone = TimeZoneField(required=False)
@@ -108,18 +107,18 @@ class RackRoleSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = RackRole model = RackRole
fields = ['id', 'name', 'slug', 'color', 'description', 'rack_count'] fields = ['id', 'name', 'slug', 'color', 'rack_count']
class RackSerializer(TaggitSerializer, CustomFieldModelSerializer): class RackSerializer(TaggitSerializer, CustomFieldModelSerializer):
site = NestedSiteSerializer() site = NestedSiteSerializer()
group = NestedRackGroupSerializer(required=False, allow_null=True, default=None) group = NestedRackGroupSerializer(required=False, allow_null=True, default=None)
tenant = NestedTenantSerializer(required=False, allow_null=True) tenant = NestedTenantSerializer(required=False, allow_null=True)
status = ChoiceField(choices=RackStatusChoices, required=False) status = ChoiceField(choices=RACK_STATUS_CHOICES, required=False)
role = NestedRackRoleSerializer(required=False, allow_null=True) role = NestedRackRoleSerializer(required=False, allow_null=True)
type = ChoiceField(choices=RackTypeChoices, required=False, allow_null=True) type = ChoiceField(choices=RACK_TYPE_CHOICES, required=False, allow_null=True)
width = ChoiceField(choices=RackWidthChoices, required=False) width = ChoiceField(choices=RACK_WIDTH_CHOICES, required=False)
outer_unit = ChoiceField(choices=RackDimensionUnitChoices, required=False) outer_unit = ChoiceField(choices=RACK_DIMENSION_UNIT_CHOICES, required=False)
tags = TagListSerializerField(required=False) tags = TagListSerializerField(required=False)
device_count = serializers.IntegerField(read_only=True) device_count = serializers.IntegerField(read_only=True)
powerfeed_count = serializers.IntegerField(read_only=True) powerfeed_count = serializers.IntegerField(read_only=True)
@@ -157,7 +156,7 @@ class RackUnitSerializer(serializers.Serializer):
""" """
id = serializers.IntegerField(read_only=True) id = serializers.IntegerField(read_only=True)
name = serializers.CharField(read_only=True) name = serializers.CharField(read_only=True)
face = ChoiceField(choices=DeviceFaceChoices, read_only=True) face = serializers.IntegerField(read_only=True)
device = NestedDeviceSerializer(read_only=True) device = NestedDeviceSerializer(read_only=True)
@@ -171,31 +170,6 @@ class RackReservationSerializer(ValidatedModelSerializer):
fields = ['id', 'rack', 'units', 'created', 'user', 'tenant', 'description'] fields = ['id', 'rack', 'units', 'created', 'user', 'tenant', 'description']
class RackElevationDetailFilterSerializer(serializers.Serializer):
face = serializers.ChoiceField(
choices=DeviceFaceChoices,
default=DeviceFaceChoices.FACE_FRONT
)
render = serializers.ChoiceField(
choices=RackElevationDetailRenderChoices,
default=RackElevationDetailRenderChoices.RENDER_JSON
)
unit_width = serializers.IntegerField(
default=RACK_ELEVATION_UNIT_WIDTH_DEFAULT
)
unit_height = serializers.IntegerField(
default=RACK_ELEVATION_UNIT_HEIGHT_DEFAULT
)
exclude = serializers.IntegerField(
required=False,
default=None
)
expand_devices = serializers.BooleanField(
required=False,
default=True
)
# #
# Device types # Device types
# #
@@ -212,7 +186,7 @@ class ManufacturerSerializer(ValidatedModelSerializer):
class DeviceTypeSerializer(TaggitSerializer, CustomFieldModelSerializer): class DeviceTypeSerializer(TaggitSerializer, CustomFieldModelSerializer):
manufacturer = NestedManufacturerSerializer() manufacturer = NestedManufacturerSerializer()
subdevice_role = ChoiceField(choices=SubdeviceRoleChoices, required=False, allow_null=True) subdevice_role = ChoiceField(choices=SUBDEVICE_ROLE_CHOICES, required=False, allow_null=True)
tags = TagListSerializerField(required=False) tags = TagListSerializerField(required=False)
device_count = serializers.IntegerField(read_only=True) device_count = serializers.IntegerField(read_only=True)
@@ -226,72 +200,58 @@ class DeviceTypeSerializer(TaggitSerializer, CustomFieldModelSerializer):
class ConsolePortTemplateSerializer(ValidatedModelSerializer): class ConsolePortTemplateSerializer(ValidatedModelSerializer):
device_type = NestedDeviceTypeSerializer() device_type = NestedDeviceTypeSerializer()
type = ChoiceField(
choices=ConsolePortTypeChoices,
required=False
)
class Meta: class Meta:
model = ConsolePortTemplate model = ConsolePortTemplate
fields = ['id', 'device_type', 'name', 'type'] fields = ['id', 'device_type', 'name']
class ConsoleServerPortTemplateSerializer(ValidatedModelSerializer): class ConsoleServerPortTemplateSerializer(ValidatedModelSerializer):
device_type = NestedDeviceTypeSerializer() device_type = NestedDeviceTypeSerializer()
type = ChoiceField(
choices=ConsolePortTypeChoices,
required=False
)
class Meta: class Meta:
model = ConsoleServerPortTemplate model = ConsoleServerPortTemplate
fields = ['id', 'device_type', 'name', 'type'] fields = ['id', 'device_type', 'name']
class PowerPortTemplateSerializer(ValidatedModelSerializer): class PowerPortTemplateSerializer(ValidatedModelSerializer):
device_type = NestedDeviceTypeSerializer() device_type = NestedDeviceTypeSerializer()
type = ChoiceField(
choices=PowerPortTypeChoices,
required=False
)
class Meta: class Meta:
model = PowerPortTemplate model = PowerPortTemplate
fields = ['id', 'device_type', 'name', 'type', 'maximum_draw', 'allocated_draw'] fields = ['id', 'device_type', 'name', 'maximum_draw', 'allocated_draw']
class PowerOutletTemplateSerializer(ValidatedModelSerializer): class PowerOutletTemplateSerializer(ValidatedModelSerializer):
device_type = NestedDeviceTypeSerializer() device_type = NestedDeviceTypeSerializer()
type = ChoiceField(
choices=PowerOutletTypeChoices,
required=False
)
power_port = PowerPortTemplateSerializer( power_port = PowerPortTemplateSerializer(
required=False required=False
) )
feed_leg = ChoiceField( feed_leg = ChoiceField(
choices=PowerOutletFeedLegChoices, choices=POWERFEED_LEG_CHOICES,
required=False, required=False,
allow_null=True allow_null=True
) )
class Meta: class Meta:
model = PowerOutletTemplate model = PowerOutletTemplate
fields = ['id', 'device_type', 'name', 'type', 'power_port', 'feed_leg'] fields = ['id', 'device_type', 'name', 'power_port', 'feed_leg']
class InterfaceTemplateSerializer(ValidatedModelSerializer): class InterfaceTemplateSerializer(ValidatedModelSerializer):
device_type = NestedDeviceTypeSerializer() device_type = NestedDeviceTypeSerializer()
type = ChoiceField(choices=InterfaceTypeChoices, required=False) 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: class Meta:
model = InterfaceTemplate model = InterfaceTemplate
fields = ['id', 'device_type', 'name', 'type', 'mgmt_only'] fields = ['id', 'device_type', 'name', 'type', 'form_factor', 'mgmt_only']
class RearPortTemplateSerializer(ValidatedModelSerializer): class RearPortTemplateSerializer(ValidatedModelSerializer):
device_type = NestedDeviceTypeSerializer() device_type = NestedDeviceTypeSerializer()
type = ChoiceField(choices=PortTypeChoices) type = ChoiceField(choices=PORT_TYPE_CHOICES)
class Meta: class Meta:
model = RearPortTemplate model = RearPortTemplate
@@ -300,7 +260,7 @@ class RearPortTemplateSerializer(ValidatedModelSerializer):
class FrontPortTemplateSerializer(ValidatedModelSerializer): class FrontPortTemplateSerializer(ValidatedModelSerializer):
device_type = NestedDeviceTypeSerializer() device_type = NestedDeviceTypeSerializer()
type = ChoiceField(choices=PortTypeChoices) type = ChoiceField(choices=PORT_TYPE_CHOICES)
rear_port = NestedRearPortTemplateSerializer() rear_port = NestedRearPortTemplateSerializer()
class Meta: class Meta:
@@ -326,9 +286,7 @@ class DeviceRoleSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = DeviceRole model = DeviceRole
fields = [ fields = ['id', 'name', 'slug', 'color', 'vm_role', 'device_count', 'virtualmachine_count']
'id', 'name', 'slug', 'color', 'vm_role', 'description', 'device_count', 'virtualmachine_count',
]
class PlatformSerializer(ValidatedModelSerializer): class PlatformSerializer(ValidatedModelSerializer):
@@ -351,8 +309,8 @@ class DeviceSerializer(TaggitSerializer, CustomFieldModelSerializer):
platform = NestedPlatformSerializer(required=False, allow_null=True) platform = NestedPlatformSerializer(required=False, allow_null=True)
site = NestedSiteSerializer() site = NestedSiteSerializer()
rack = NestedRackSerializer(required=False, allow_null=True) rack = NestedRackSerializer(required=False, allow_null=True)
face = ChoiceField(choices=DeviceFaceChoices, required=False, allow_null=True) face = ChoiceField(choices=RACK_FACE_CHOICES, required=False, allow_null=True)
status = ChoiceField(choices=DeviceStatusChoices, required=False) status = ChoiceField(choices=DEVICE_STATUS_CHOICES, required=False)
primary_ip = NestedIPAddressSerializer(read_only=True) primary_ip = NestedIPAddressSerializer(read_only=True)
primary_ip4 = NestedIPAddressSerializer(required=False, allow_null=True) primary_ip4 = NestedIPAddressSerializer(required=False, allow_null=True)
primary_ip6 = NestedIPAddressSerializer(required=False, allow_null=True) primary_ip6 = NestedIPAddressSerializer(required=False, allow_null=True)
@@ -414,49 +372,37 @@ class DeviceWithConfigContextSerializer(DeviceSerializer):
class ConsoleServerPortSerializer(TaggitSerializer, ConnectedEndpointSerializer): class ConsoleServerPortSerializer(TaggitSerializer, ConnectedEndpointSerializer):
device = NestedDeviceSerializer() device = NestedDeviceSerializer()
type = ChoiceField(
choices=ConsolePortTypeChoices,
required=False
)
cable = NestedCableSerializer(read_only=True) cable = NestedCableSerializer(read_only=True)
tags = TagListSerializerField(required=False) tags = TagListSerializerField(required=False)
class Meta: class Meta:
model = ConsoleServerPort model = ConsoleServerPort
fields = [ fields = [
'id', 'device', 'name', 'type', 'description', 'connected_endpoint_type', 'connected_endpoint', 'id', 'device', 'name', 'description', 'connected_endpoint_type', 'connected_endpoint', 'connection_status',
'connection_status', 'cable', 'tags', 'cable', 'tags',
] ]
class ConsolePortSerializer(TaggitSerializer, ConnectedEndpointSerializer): class ConsolePortSerializer(TaggitSerializer, ConnectedEndpointSerializer):
device = NestedDeviceSerializer() device = NestedDeviceSerializer()
type = ChoiceField(
choices=ConsolePortTypeChoices,
required=False
)
cable = NestedCableSerializer(read_only=True) cable = NestedCableSerializer(read_only=True)
tags = TagListSerializerField(required=False) tags = TagListSerializerField(required=False)
class Meta: class Meta:
model = ConsolePort model = ConsolePort
fields = [ fields = [
'id', 'device', 'name', 'type', 'description', 'connected_endpoint_type', 'connected_endpoint', 'id', 'device', 'name', 'description', 'connected_endpoint_type', 'connected_endpoint', 'connection_status',
'connection_status', 'cable', 'tags', 'cable', 'tags',
] ]
class PowerOutletSerializer(TaggitSerializer, ConnectedEndpointSerializer): class PowerOutletSerializer(TaggitSerializer, ConnectedEndpointSerializer):
device = NestedDeviceSerializer() device = NestedDeviceSerializer()
type = ChoiceField(
choices=PowerOutletTypeChoices,
required=False
)
power_port = NestedPowerPortSerializer( power_port = NestedPowerPortSerializer(
required=False required=False
) )
feed_leg = ChoiceField( feed_leg = ChoiceField(
choices=PowerOutletFeedLegChoices, choices=POWERFEED_LEG_CHOICES,
required=False, required=False,
allow_null=True allow_null=True
) )
@@ -470,33 +416,31 @@ class PowerOutletSerializer(TaggitSerializer, ConnectedEndpointSerializer):
class Meta: class Meta:
model = PowerOutlet model = PowerOutlet
fields = [ fields = [
'id', 'device', 'name', 'type', 'power_port', 'feed_leg', 'description', 'connected_endpoint_type', 'id', 'device', 'name', 'power_port', 'feed_leg', 'description', 'connected_endpoint_type',
'connected_endpoint', 'connection_status', 'cable', 'tags', 'connected_endpoint', 'connection_status', 'cable', 'tags',
] ]
class PowerPortSerializer(TaggitSerializer, ConnectedEndpointSerializer): class PowerPortSerializer(TaggitSerializer, ConnectedEndpointSerializer):
device = NestedDeviceSerializer() device = NestedDeviceSerializer()
type = ChoiceField(
choices=PowerPortTypeChoices,
required=False
)
cable = NestedCableSerializer(read_only=True) cable = NestedCableSerializer(read_only=True)
tags = TagListSerializerField(required=False) tags = TagListSerializerField(required=False)
class Meta: class Meta:
model = PowerPort model = PowerPort
fields = [ fields = [
'id', 'device', 'name', 'type', 'maximum_draw', 'allocated_draw', 'description', 'connected_endpoint_type', 'id', 'device', 'name', 'maximum_draw', 'allocated_draw', 'description', 'connected_endpoint_type',
'connected_endpoint', 'connection_status', 'cable', 'tags', 'connected_endpoint', 'connection_status', 'cable', 'tags',
] ]
class InterfaceSerializer(TaggitSerializer, ConnectedEndpointSerializer): class InterfaceSerializer(TaggitSerializer, ConnectedEndpointSerializer):
device = NestedDeviceSerializer() device = NestedDeviceSerializer()
type = ChoiceField(choices=InterfaceTypeChoices, required=False) 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) lag = NestedInterfaceSerializer(required=False, allow_null=True)
mode = ChoiceField(choices=InterfaceModeChoices, required=False, allow_null=True) mode = ChoiceField(choices=IFACE_MODE_CHOICES, required=False, allow_null=True)
untagged_vlan = NestedVLANSerializer(required=False, allow_null=True) untagged_vlan = NestedVLANSerializer(required=False, allow_null=True)
tagged_vlans = SerializedPKRelatedField( tagged_vlans = SerializedPKRelatedField(
queryset=VLAN.objects.all(), queryset=VLAN.objects.all(),
@@ -510,9 +454,9 @@ class InterfaceSerializer(TaggitSerializer, ConnectedEndpointSerializer):
class Meta: class Meta:
model = Interface model = Interface
fields = [ fields = [
'id', 'device', 'name', 'type', 'enabled', 'lag', 'mtu', 'mac_address', 'mgmt_only', 'description', 'id', 'device', 'name', 'type', 'form_factor', 'enabled', 'lag', 'mtu', 'mac_address', 'mgmt_only',
'connected_endpoint_type', 'connected_endpoint', 'connection_status', 'cable', 'mode', 'untagged_vlan', 'description', 'connected_endpoint_type', 'connected_endpoint', 'connection_status', 'cable', 'mode',
'tagged_vlans', 'tags', 'count_ipaddresses', 'untagged_vlan', 'tagged_vlans', 'tags', 'count_ipaddresses',
] ]
# TODO: This validation should be handled by Interface.clean() # TODO: This validation should be handled by Interface.clean()
@@ -538,7 +482,7 @@ class InterfaceSerializer(TaggitSerializer, ConnectedEndpointSerializer):
class RearPortSerializer(TaggitSerializer, ValidatedModelSerializer): class RearPortSerializer(TaggitSerializer, ValidatedModelSerializer):
device = NestedDeviceSerializer() device = NestedDeviceSerializer()
type = ChoiceField(choices=PortTypeChoices) type = ChoiceField(choices=PORT_TYPE_CHOICES)
cable = NestedCableSerializer(read_only=True) cable = NestedCableSerializer(read_only=True)
tags = TagListSerializerField(required=False) tags = TagListSerializerField(required=False)
@@ -560,7 +504,7 @@ class FrontPortRearPortSerializer(WritableNestedSerializer):
class FrontPortSerializer(TaggitSerializer, ValidatedModelSerializer): class FrontPortSerializer(TaggitSerializer, ValidatedModelSerializer):
device = NestedDeviceSerializer() device = NestedDeviceSerializer()
type = ChoiceField(choices=PortTypeChoices) type = ChoiceField(choices=PORT_TYPE_CHOICES)
rear_port = FrontPortRearPortSerializer() rear_port = FrontPortRearPortSerializer()
cable = NestedCableSerializer(read_only=True) cable = NestedCableSerializer(read_only=True)
tags = TagListSerializerField(required=False) tags = TagListSerializerField(required=False)
@@ -612,8 +556,8 @@ class CableSerializer(ValidatedModelSerializer):
) )
termination_a = serializers.SerializerMethodField(read_only=True) termination_a = serializers.SerializerMethodField(read_only=True)
termination_b = serializers.SerializerMethodField(read_only=True) termination_b = serializers.SerializerMethodField(read_only=True)
status = ChoiceField(choices=CableStatusChoices, required=False) status = ChoiceField(choices=CONNECTION_STATUS_CHOICES, required=False)
length_unit = ChoiceField(choices=CableLengthUnitChoices, required=False, allow_null=True) length_unit = ChoiceField(choices=CABLE_LENGTH_UNIT_CHOICES, required=False, allow_null=True)
class Meta: class Meta:
model = Cable model = Cable
@@ -718,20 +662,20 @@ class PowerFeedSerializer(TaggitSerializer, CustomFieldModelSerializer):
default=None default=None
) )
type = ChoiceField( type = ChoiceField(
choices=PowerFeedTypeChoices, choices=POWERFEED_TYPE_CHOICES,
default=PowerFeedTypeChoices.TYPE_PRIMARY default=POWERFEED_TYPE_PRIMARY
) )
status = ChoiceField( status = ChoiceField(
choices=PowerFeedStatusChoices, choices=POWERFEED_STATUS_CHOICES,
default=PowerFeedStatusChoices.STATUS_ACTIVE default=POWERFEED_STATUS_ACTIVE
) )
supply = ChoiceField( supply = ChoiceField(
choices=PowerFeedSupplyChoices, choices=POWERFEED_SUPPLY_CHOICES,
default=PowerFeedSupplyChoices.SUPPLY_AC default=POWERFEED_SUPPLY_AC
) )
phase = ChoiceField( phase = ChoiceField(
choices=PowerFeedPhaseChoices, choices=POWERFEED_PHASE_CHOICES,
default=PowerFeedPhaseChoices.PHASE_SINGLE default=POWERFEED_PHASE_SINGLE
) )
tags = TagListSerializerField( tags = TagListSerializerField(
required=False required=False

View File

@@ -2,8 +2,8 @@ from collections import OrderedDict
from django.conf import settings from django.conf import settings
from django.db.models import Count, F from django.db.models import Count, F
from django.http import HttpResponseForbidden, HttpResponseBadRequest, HttpResponse from django.http import HttpResponseForbidden
from django.shortcuts import get_object_or_404, reverse from django.shortcuts import get_object_or_404
from drf_yasg import openapi from drf_yasg import openapi
from drf_yasg.openapi import Parameter from drf_yasg.openapi import Parameter
from drf_yasg.utils import swagger_auto_schema from drf_yasg.utils import swagger_auto_schema
@@ -13,7 +13,7 @@ from rest_framework.response import Response
from rest_framework.viewsets import GenericViewSet, ViewSet from rest_framework.viewsets import GenericViewSet, ViewSet
from circuits.models import Circuit from circuits.models import Circuit
from dcim import constants, filters from dcim import filters
from dcim.models import ( from dcim.models import (
Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate, DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate,
@@ -23,12 +23,12 @@ from dcim.models import (
) )
from extras.api.serializers import RenderedGraphSerializer from extras.api.serializers import RenderedGraphSerializer
from extras.api.views import CustomFieldModelViewSet from extras.api.views import CustomFieldModelViewSet
from extras.constants import GRAPH_TYPE_DEVICE, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE
from extras.models import Graph from extras.models import Graph
from ipam.models import Prefix, VLAN from ipam.models import Prefix, VLAN
from utilities.api import ( from utilities.api import (
get_serializer_for_model, IsAuthenticatedOrLoginNotRequired, FieldChoicesViewSet, ModelViewSet, ServiceUnavailable, get_serializer_for_model, IsAuthenticatedOrLoginNotRequired, FieldChoicesViewSet, ModelViewSet, ServiceUnavailable,
) )
from utilities.custom_inspectors import NullablePaginatorInspector
from utilities.utils import get_subquery from utilities.utils import get_subquery
from virtualization.models import VirtualMachine from virtualization.models import VirtualMachine
from . import serializers from . import serializers
@@ -42,20 +42,16 @@ from .exceptions import MissingFilterException
class DCIMFieldChoicesViewSet(FieldChoicesViewSet): class DCIMFieldChoicesViewSet(FieldChoicesViewSet):
fields = ( fields = (
(Cable, ['length_unit', 'status', 'termination_a_type', 'termination_b_type', 'type']), (Cable, ['length_unit', 'status', 'termination_a_type', 'termination_b_type', 'type']),
(ConsolePort, ['type', 'connection_status']), (ConsolePort, ['connection_status']),
(ConsolePortTemplate, ['type']),
(ConsoleServerPort, ['type']),
(ConsoleServerPortTemplate, ['type']),
(Device, ['face', 'status']), (Device, ['face', 'status']),
(DeviceType, ['subdevice_role']), (DeviceType, ['subdevice_role']),
(FrontPort, ['type']), (FrontPort, ['type']),
(FrontPortTemplate, ['type']), (FrontPortTemplate, ['type']),
(Interface, ['type', 'mode']), (Interface, ['type', 'mode']),
(InterfaceTemplate, ['type']), (InterfaceTemplate, ['type']),
(PowerOutlet, ['type', 'feed_leg']), (PowerOutlet, ['feed_leg']),
(PowerOutletTemplate, ['type', 'feed_leg']), (PowerOutletTemplate, ['feed_leg']),
(PowerPort, ['type', 'connection_status']), (PowerPort, ['connection_status']),
(PowerPortTemplate, ['type']),
(Rack, ['outer_unit', 'status', 'type', 'width']), (Rack, ['outer_unit', 'status', 'type', 'width']),
(RearPort, ['type']), (RearPort, ['type']),
(RearPortTemplate, ['type']), (RearPortTemplate, ['type']),
@@ -133,7 +129,7 @@ class SiteViewSet(CustomFieldModelViewSet):
A convenience method for rendering graphs for a particular site. A convenience method for rendering graphs for a particular site.
""" """
site = get_object_or_404(Site, pk=pk) site = get_object_or_404(Site, pk=pk)
queryset = Graph.objects.filter(type__model='site') queryset = Graph.objects.filter(type=GRAPH_TYPE_SITE)
serializer = RenderedGraphSerializer(queryset, many=True, context={'graphed_object': site}) serializer = RenderedGraphSerializer(queryset, many=True, context={'graphed_object': site})
return Response(serializer.data) return Response(serializer.data)
@@ -176,15 +172,13 @@ class RackViewSet(CustomFieldModelViewSet):
serializer_class = serializers.RackSerializer serializer_class = serializers.RackSerializer
filterset_class = filters.RackFilter filterset_class = filters.RackFilter
@swagger_auto_schema(deprecated=True)
@action(detail=True) @action(detail=True)
def units(self, request, pk=None): def units(self, request, pk=None):
""" """
List rack units (by rack) List rack units (by rack)
""" """
# TODO: Remove this action detail route in v2.8
rack = get_object_or_404(Rack, pk=pk) rack = get_object_or_404(Rack, pk=pk)
face = request.GET.get('face', 'front') face = request.GET.get('face', 0)
exclude_pk = request.GET.get('exclude', None) exclude_pk = request.GET.get('exclude', None)
if exclude_pk is not None: if exclude_pk is not None:
try: try:
@@ -203,39 +197,6 @@ class RackViewSet(CustomFieldModelViewSet):
rack_units = serializers.RackUnitSerializer(page, many=True, context={'request': request}) rack_units = serializers.RackUnitSerializer(page, many=True, context={'request': request})
return self.get_paginated_response(rack_units.data) return self.get_paginated_response(rack_units.data)
@swagger_auto_schema(
responses={200: serializers.RackUnitSerializer(many=True)},
query_serializer=serializers.RackElevationDetailFilterSerializer
)
@action(detail=True)
def elevation(self, request, pk=None):
"""
Rack elevation representing the list of rack units. Also supports rendering the elevation as an SVG.
"""
rack = get_object_or_404(Rack, pk=pk)
serializer = serializers.RackElevationDetailFilterSerializer(data=request.GET)
if not serializer.is_valid():
return Response(serializer.errors, 400)
data = serializer.validated_data
if data['render'] == 'svg':
# Render and return the elevation as an SVG drawing with the correct content type
drawing = rack.get_elevation_svg(data['face'], data['unit_width'], data['unit_height'])
return HttpResponse(drawing.tostring(), content_type='image/svg+xml')
else:
# Return a JSON representation of the rack units in the elevation
elevation = rack.get_rack_units(
face=data['face'],
exclude=data['exclude'],
expand_devices=data['expand_devices']
)
page = self.paginate_queryset(elevation)
if page is not None:
rack_units = serializers.RackUnitSerializer(page, many=True, context={'request': request})
return self.get_paginated_response(rack_units.data)
# #
# Rack reservations # Rack reservations
@@ -392,7 +353,7 @@ class DeviceViewSet(CustomFieldModelViewSet):
A convenience method for rendering graphs for a particular Device. A convenience method for rendering graphs for a particular Device.
""" """
device = get_object_or_404(Device, pk=pk) device = get_object_or_404(Device, pk=pk)
queryset = Graph.objects.filter(type__model='device') queryset = Graph.objects.filter(type=GRAPH_TYPE_DEVICE)
serializer = RenderedGraphSerializer(queryset, many=True, context={'graphed_object': device}) serializer = RenderedGraphSerializer(queryset, many=True, context={'graphed_object': device})
return Response(serializer.data) return Response(serializer.data)
@@ -514,7 +475,7 @@ class InterfaceViewSet(CableTraceMixin, ModelViewSet):
A convenience method for rendering graphs for a particular interface. A convenience method for rendering graphs for a particular interface.
""" """
interface = get_object_or_404(Interface, pk=pk) interface = get_object_or_404(Interface, pk=pk)
queryset = Graph.objects.filter(type__model='interface') queryset = Graph.objects.filter(type=GRAPH_TYPE_INTERFACE)
serializer = RenderedGraphSerializer(queryset, many=True, context={'graphed_object': interface}) serializer = RenderedGraphSerializer(queryset, many=True, context={'graphed_object': interface})
return Response(serializer.data) return Response(serializer.data)

File diff suppressed because it is too large Load Diff

View File

@@ -1,25 +1,379 @@
from .choices import InterfaceTypeChoices
# Rack types
RACK_TYPE_2POST = 100
RACK_TYPE_4POST = 200
RACK_TYPE_CABINET = 300
RACK_TYPE_WALLFRAME = 1000
RACK_TYPE_WALLCABINET = 1100
RACK_TYPE_CHOICES = (
(RACK_TYPE_2POST, '2-post frame'),
(RACK_TYPE_4POST, '4-post frame'),
(RACK_TYPE_CABINET, '4-post cabinet'),
(RACK_TYPE_WALLFRAME, 'Wall-mounted frame'),
(RACK_TYPE_WALLCABINET, 'Wall-mounted cabinet'),
)
# # Rack widths
# Interface type groups RACK_WIDTH_19IN = 19
# RACK_WIDTH_23IN = 23
RACK_WIDTH_CHOICES = (
(RACK_WIDTH_19IN, '19 inches'),
(RACK_WIDTH_23IN, '23 inches'),
)
# Rack faces
RACK_FACE_FRONT = 0
RACK_FACE_REAR = 1
RACK_FACE_CHOICES = [
[RACK_FACE_FRONT, 'Front'],
[RACK_FACE_REAR, 'Rear'],
]
# Rack statuses
RACK_STATUS_RESERVED = 0
RACK_STATUS_AVAILABLE = 1
RACK_STATUS_PLANNED = 2
RACK_STATUS_ACTIVE = 3
RACK_STATUS_DEPRECATED = 4
RACK_STATUS_CHOICES = [
[RACK_STATUS_ACTIVE, 'Active'],
[RACK_STATUS_PLANNED, 'Planned'],
[RACK_STATUS_RESERVED, 'Reserved'],
[RACK_STATUS_AVAILABLE, 'Available'],
[RACK_STATUS_DEPRECATED, 'Deprecated'],
]
# Device rack position
DEVICE_POSITION_CHOICES = [
# Rack.u_height is limited to 100
(i, 'Unit {}'.format(i)) for i in range(1, 101)
]
# Parent/child device roles
SUBDEVICE_ROLE_PARENT = True
SUBDEVICE_ROLE_CHILD = False
SUBDEVICE_ROLE_CHOICES = (
(None, 'None'),
(SUBDEVICE_ROLE_PARENT, 'Parent'),
(SUBDEVICE_ROLE_CHILD, 'Child'),
)
# Interface types
# Virtual
IFACE_TYPE_VIRTUAL = 0
IFACE_TYPE_LAG = 200
# Ethernet
IFACE_TYPE_100ME_FIXED = 800
IFACE_TYPE_1GE_FIXED = 1000
IFACE_TYPE_1GE_GBIC = 1050
IFACE_TYPE_1GE_SFP = 1100
IFACE_TYPE_2GE_FIXED = 1120
IFACE_TYPE_5GE_FIXED = 1130
IFACE_TYPE_10GE_FIXED = 1150
IFACE_TYPE_10GE_CX4 = 1170
IFACE_TYPE_10GE_SFP_PLUS = 1200
IFACE_TYPE_10GE_XFP = 1300
IFACE_TYPE_10GE_XENPAK = 1310
IFACE_TYPE_10GE_X2 = 1320
IFACE_TYPE_25GE_SFP28 = 1350
IFACE_TYPE_40GE_QSFP_PLUS = 1400
IFACE_TYPE_50GE_QSFP28 = 1420
IFACE_TYPE_100GE_CFP = 1500
IFACE_TYPE_100GE_CFP2 = 1510
IFACE_TYPE_100GE_CFP4 = 1520
IFACE_TYPE_100GE_CPAK = 1550
IFACE_TYPE_100GE_QSFP28 = 1600
IFACE_TYPE_200GE_CFP2 = 1650
IFACE_TYPE_200GE_QSFP56 = 1700
IFACE_TYPE_400GE_QSFP_DD = 1750
IFACE_TYPE_400GE_OSFP = 1800
# Wireless
IFACE_TYPE_80211A = 2600
IFACE_TYPE_80211G = 2610
IFACE_TYPE_80211N = 2620
IFACE_TYPE_80211AC = 2630
IFACE_TYPE_80211AD = 2640
# Cellular
IFACE_TYPE_GSM = 2810
IFACE_TYPE_CDMA = 2820
IFACE_TYPE_LTE = 2830
# SONET
IFACE_TYPE_SONET_OC3 = 6100
IFACE_TYPE_SONET_OC12 = 6200
IFACE_TYPE_SONET_OC48 = 6300
IFACE_TYPE_SONET_OC192 = 6400
IFACE_TYPE_SONET_OC768 = 6500
IFACE_TYPE_SONET_OC1920 = 6600
IFACE_TYPE_SONET_OC3840 = 6700
# Fibrechannel
IFACE_TYPE_1GFC_SFP = 3010
IFACE_TYPE_2GFC_SFP = 3020
IFACE_TYPE_4GFC_SFP = 3040
IFACE_TYPE_8GFC_SFP_PLUS = 3080
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
# Serial
IFACE_TYPE_T1 = 4000
IFACE_TYPE_E1 = 4010
IFACE_TYPE_T3 = 4040
IFACE_TYPE_E3 = 4050
# Stacking
IFACE_TYPE_STACKWISE = 5000
IFACE_TYPE_STACKWISE_PLUS = 5050
IFACE_TYPE_FLEXSTACK = 5100
IFACE_TYPE_FLEXSTACK_PLUS = 5150
IFACE_TYPE_JUNIPER_VCP = 5200
IFACE_TYPE_SUMMITSTACK = 5300
IFACE_TYPE_SUMMITSTACK128 = 5310
IFACE_TYPE_SUMMITSTACK256 = 5320
IFACE_TYPE_SUMMITSTACK512 = 5330
# Other
IFACE_TYPE_OTHER = 32767
IFACE_TYPE_CHOICES = [
[
'Virtual interfaces',
[
[IFACE_TYPE_VIRTUAL, 'Virtual'],
[IFACE_TYPE_LAG, 'Link Aggregation Group (LAG)'],
],
],
[
'Ethernet (fixed)',
[
[IFACE_TYPE_100ME_FIXED, '100BASE-TX (10/100ME)'],
[IFACE_TYPE_1GE_FIXED, '1000BASE-T (1GE)'],
[IFACE_TYPE_2GE_FIXED, '2.5GBASE-T (2.5GE)'],
[IFACE_TYPE_5GE_FIXED, '5GBASE-T (5GE)'],
[IFACE_TYPE_10GE_FIXED, '10GBASE-T (10GE)'],
[IFACE_TYPE_10GE_CX4, '10GBASE-CX4 (10GE)'],
]
],
[
'Ethernet (modular)',
[
[IFACE_TYPE_1GE_GBIC, 'GBIC (1GE)'],
[IFACE_TYPE_1GE_SFP, 'SFP (1GE)'],
[IFACE_TYPE_10GE_SFP_PLUS, 'SFP+ (10GE)'],
[IFACE_TYPE_10GE_XFP, 'XFP (10GE)'],
[IFACE_TYPE_10GE_XENPAK, 'XENPAK (10GE)'],
[IFACE_TYPE_10GE_X2, 'X2 (10GE)'],
[IFACE_TYPE_25GE_SFP28, 'SFP28 (25GE)'],
[IFACE_TYPE_40GE_QSFP_PLUS, 'QSFP+ (40GE)'],
[IFACE_TYPE_50GE_QSFP28, 'QSFP28 (50GE)'],
[IFACE_TYPE_100GE_CFP, 'CFP (100GE)'],
[IFACE_TYPE_100GE_CFP2, 'CFP2 (100GE)'],
[IFACE_TYPE_200GE_CFP2, 'CFP2 (200GE)'],
[IFACE_TYPE_100GE_CFP4, 'CFP4 (100GE)'],
[IFACE_TYPE_100GE_CPAK, 'Cisco CPAK (100GE)'],
[IFACE_TYPE_100GE_QSFP28, 'QSFP28 (100GE)'],
[IFACE_TYPE_200GE_QSFP56, 'QSFP56 (200GE)'],
[IFACE_TYPE_400GE_QSFP_DD, 'QSFP-DD (400GE)'],
[IFACE_TYPE_400GE_OSFP, 'OSFP (400GE)'],
]
],
[
'Wireless',
[
[IFACE_TYPE_80211A, 'IEEE 802.11a'],
[IFACE_TYPE_80211G, 'IEEE 802.11b/g'],
[IFACE_TYPE_80211N, 'IEEE 802.11n'],
[IFACE_TYPE_80211AC, 'IEEE 802.11ac'],
[IFACE_TYPE_80211AD, 'IEEE 802.11ad'],
]
],
[
'Cellular',
[
[IFACE_TYPE_GSM, 'GSM'],
[IFACE_TYPE_CDMA, 'CDMA'],
[IFACE_TYPE_LTE, 'LTE'],
]
],
[
'SONET',
[
[IFACE_TYPE_SONET_OC3, 'OC-3/STM-1'],
[IFACE_TYPE_SONET_OC12, 'OC-12/STM-4'],
[IFACE_TYPE_SONET_OC48, 'OC-48/STM-16'],
[IFACE_TYPE_SONET_OC192, 'OC-192/STM-64'],
[IFACE_TYPE_SONET_OC768, 'OC-768/STM-256'],
[IFACE_TYPE_SONET_OC1920, 'OC-1920/STM-640'],
[IFACE_TYPE_SONET_OC3840, 'OC-3840/STM-1234'],
]
],
[
'FibreChannel',
[
[IFACE_TYPE_1GFC_SFP, 'SFP (1GFC)'],
[IFACE_TYPE_2GFC_SFP, 'SFP (2GFC)'],
[IFACE_TYPE_4GFC_SFP, 'SFP (4GFC)'],
[IFACE_TYPE_8GFC_SFP_PLUS, 'SFP+ (8GFC)'],
[IFACE_TYPE_16GFC_SFP_PLUS, 'SFP+ (16GFC)'],
[IFACE_TYPE_32GFC_SFP28, 'SFP28 (32GFC)'],
[IFACE_TYPE_128GFC_QSFP28, 'QSFP28 (128GFC)'],
]
],
[
'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)'],
]
],
[
'Serial',
[
[IFACE_TYPE_T1, 'T1 (1.544 Mbps)'],
[IFACE_TYPE_E1, 'E1 (2.048 Mbps)'],
[IFACE_TYPE_T3, 'T3 (45 Mbps)'],
[IFACE_TYPE_E3, 'E3 (34 Mbps)'],
]
],
[
'Stacking',
[
[IFACE_TYPE_STACKWISE, 'Cisco StackWise'],
[IFACE_TYPE_STACKWISE_PLUS, 'Cisco StackWise Plus'],
[IFACE_TYPE_FLEXSTACK, 'Cisco FlexStack'],
[IFACE_TYPE_FLEXSTACK_PLUS, 'Cisco FlexStack Plus'],
[IFACE_TYPE_JUNIPER_VCP, 'Juniper VCP'],
[IFACE_TYPE_SUMMITSTACK, 'Extreme SummitStack'],
[IFACE_TYPE_SUMMITSTACK128, 'Extreme SummitStack-128'],
[IFACE_TYPE_SUMMITSTACK256, 'Extreme SummitStack-256'],
[IFACE_TYPE_SUMMITSTACK512, 'Extreme SummitStack-512'],
]
],
[
'Other',
[
[IFACE_TYPE_OTHER, 'Other'],
]
],
]
VIRTUAL_IFACE_TYPES = [ VIRTUAL_IFACE_TYPES = [
InterfaceTypeChoices.TYPE_VIRTUAL, IFACE_TYPE_VIRTUAL,
InterfaceTypeChoices.TYPE_LAG, IFACE_TYPE_LAG,
] ]
WIRELESS_IFACE_TYPES = [ WIRELESS_IFACE_TYPES = [
InterfaceTypeChoices.TYPE_80211A, IFACE_TYPE_80211A,
InterfaceTypeChoices.TYPE_80211G, IFACE_TYPE_80211G,
InterfaceTypeChoices.TYPE_80211N, IFACE_TYPE_80211N,
InterfaceTypeChoices.TYPE_80211AC, IFACE_TYPE_80211AC,
InterfaceTypeChoices.TYPE_80211AD, IFACE_TYPE_80211AD,
] ]
NONCONNECTABLE_IFACE_TYPES = VIRTUAL_IFACE_TYPES + WIRELESS_IFACE_TYPES NONCONNECTABLE_IFACE_TYPES = VIRTUAL_IFACE_TYPES + WIRELESS_IFACE_TYPES
IFACE_MODE_ACCESS = 100
IFACE_MODE_TAGGED = 200
IFACE_MODE_TAGGED_ALL = 300
IFACE_MODE_CHOICES = [
[IFACE_MODE_ACCESS, 'Access'],
[IFACE_MODE_TAGGED, 'Tagged'],
[IFACE_MODE_TAGGED_ALL, 'Tagged All'],
]
# Pass-through port types
PORT_TYPE_8P8C = 1000
PORT_TYPE_110_PUNCH = 1100
PORT_TYPE_BNC = 1200
PORT_TYPE_ST = 2000
PORT_TYPE_SC = 2100
PORT_TYPE_SC_APC = 2110
PORT_TYPE_FC = 2200
PORT_TYPE_LC = 2300
PORT_TYPE_LC_APC = 2310
PORT_TYPE_MTRJ = 2400
PORT_TYPE_MPO = 2500
PORT_TYPE_LSH = 2600
PORT_TYPE_LSH_APC = 2610
PORT_TYPE_CHOICES = [
[
'Copper',
[
[PORT_TYPE_8P8C, '8P8C'],
[PORT_TYPE_110_PUNCH, '110 Punch'],
[PORT_TYPE_BNC, 'BNC'],
],
],
[
'Fiber Optic',
[
[PORT_TYPE_FC, 'FC'],
[PORT_TYPE_LC, 'LC'],
[PORT_TYPE_LC_APC, 'LC/APC'],
[PORT_TYPE_LSH, 'LSH'],
[PORT_TYPE_LSH_APC, 'LSH/APC'],
[PORT_TYPE_MPO, 'MPO'],
[PORT_TYPE_MTRJ, 'MTRJ'],
[PORT_TYPE_SC, 'SC'],
[PORT_TYPE_SC_APC, 'SC/APC'],
[PORT_TYPE_ST, 'ST'],
]
]
]
# Device statuses
DEVICE_STATUS_OFFLINE = 0
DEVICE_STATUS_ACTIVE = 1
DEVICE_STATUS_PLANNED = 2
DEVICE_STATUS_STAGED = 3
DEVICE_STATUS_FAILED = 4
DEVICE_STATUS_INVENTORY = 5
DEVICE_STATUS_DECOMMISSIONING = 6
DEVICE_STATUS_CHOICES = [
[DEVICE_STATUS_ACTIVE, 'Active'],
[DEVICE_STATUS_OFFLINE, 'Offline'],
[DEVICE_STATUS_PLANNED, 'Planned'],
[DEVICE_STATUS_STAGED, 'Staged'],
[DEVICE_STATUS_FAILED, 'Failed'],
[DEVICE_STATUS_INVENTORY, 'Inventory'],
[DEVICE_STATUS_DECOMMISSIONING, 'Decommissioning'],
]
# Site statuses
SITE_STATUS_ACTIVE = 1
SITE_STATUS_PLANNED = 2
SITE_STATUS_RETIRED = 4
SITE_STATUS_CHOICES = [
[SITE_STATUS_ACTIVE, 'Active'],
[SITE_STATUS_PLANNED, 'Planned'],
[SITE_STATUS_RETIRED, 'Retired'],
]
# Bootstrap CSS classes for device/rack statuses
STATUS_CLASSES = {
0: 'warning',
1: 'success',
2: 'info',
3: 'primary',
4: 'danger',
5: 'default',
6: 'warning',
}
# Console/power/interface connection statuses # Console/power/interface connection statuses
CONNECTION_STATUS_PLANNED = False CONNECTION_STATUS_PLANNED = False
CONNECTION_STATUS_CONNECTED = True CONNECTION_STATUS_CONNECTED = True
@@ -30,10 +384,59 @@ CONNECTION_STATUS_CHOICES = [
# Cable endpoint types # Cable endpoint types
CABLE_TERMINATION_TYPES = [ CABLE_TERMINATION_TYPES = [
'consoleport', 'consoleserverport', 'interface', 'poweroutlet', 'powerport', 'frontport', 'rearport', 'consoleport', 'consoleserverport', 'interface', 'poweroutlet', 'powerport', 'frontport', 'rearport', 'circuittermination',
'circuittermination', 'powerfeed',
] ]
# Cable types
CABLE_TYPE_CAT3 = 1300
CABLE_TYPE_CAT5 = 1500
CABLE_TYPE_CAT5E = 1510
CABLE_TYPE_CAT6 = 1600
CABLE_TYPE_CAT6A = 1610
CABLE_TYPE_CAT7 = 1700
CABLE_TYPE_DAC_ACTIVE = 1800
CABLE_TYPE_DAC_PASSIVE = 1810
CABLE_TYPE_COAXIAL = 1900
CABLE_TYPE_MMF = 3000
CABLE_TYPE_MMF_OM1 = 3010
CABLE_TYPE_MMF_OM2 = 3020
CABLE_TYPE_MMF_OM3 = 3030
CABLE_TYPE_MMF_OM4 = 3040
CABLE_TYPE_SMF = 3500
CABLE_TYPE_SMF_OS1 = 3510
CABLE_TYPE_SMF_OS2 = 3520
CABLE_TYPE_AOC = 3800
CABLE_TYPE_POWER = 5000
CABLE_TYPE_CHOICES = (
(
'Copper', (
(CABLE_TYPE_CAT3, 'CAT3'),
(CABLE_TYPE_CAT5, 'CAT5'),
(CABLE_TYPE_CAT5E, 'CAT5e'),
(CABLE_TYPE_CAT6, 'CAT6'),
(CABLE_TYPE_CAT6A, 'CAT6a'),
(CABLE_TYPE_CAT7, 'CAT7'),
(CABLE_TYPE_DAC_ACTIVE, 'Direct Attach Copper (Active)'),
(CABLE_TYPE_DAC_PASSIVE, 'Direct Attach Copper (Passive)'),
(CABLE_TYPE_COAXIAL, 'Coaxial'),
),
),
(
'Fiber', (
(CABLE_TYPE_MMF, 'Multimode Fiber'),
(CABLE_TYPE_MMF_OM1, 'Multimode Fiber (OM1)'),
(CABLE_TYPE_MMF_OM2, 'Multimode Fiber (OM2)'),
(CABLE_TYPE_MMF_OM3, 'Multimode Fiber (OM3)'),
(CABLE_TYPE_MMF_OM4, 'Multimode Fiber (OM4)'),
(CABLE_TYPE_SMF, 'Singlemode Fiber'),
(CABLE_TYPE_SMF_OS1, 'Singlemode Fiber (OS1)'),
(CABLE_TYPE_SMF_OS2, 'Singlemode Fiber (OS2)'),
(CABLE_TYPE_AOC, 'Active Optical Cabling (AOC)'),
),
),
(CABLE_TYPE_POWER, 'Power'),
)
CABLE_TERMINATION_TYPE_CHOICES = { CABLE_TERMINATION_TYPE_CHOICES = {
# (API endpoint, human-friendly name) # (API endpoint, human-friendly name)
'consoleport': ('console-ports', 'Console port'), 'consoleport': ('console-ports', 'Console port'),
@@ -56,68 +459,56 @@ COMPATIBLE_TERMINATION_TYPES = {
'circuittermination': ['interface', 'frontport', 'rearport'], 'circuittermination': ['interface', 'frontport', 'rearport'],
} }
LENGTH_UNIT_METER = 1200
LENGTH_UNIT_CENTIMETER = 1100
LENGTH_UNIT_MILLIMETER = 1000
LENGTH_UNIT_FOOT = 2100
LENGTH_UNIT_INCH = 2000
CABLE_LENGTH_UNIT_CHOICES = (
(LENGTH_UNIT_METER, 'Meters'),
(LENGTH_UNIT_CENTIMETER, 'Centimeters'),
(LENGTH_UNIT_FOOT, 'Feet'),
(LENGTH_UNIT_INCH, 'Inches'),
)
RACK_DIMENSION_UNIT_CHOICES = (
(LENGTH_UNIT_MILLIMETER, 'Millimeters'),
(LENGTH_UNIT_INCH, 'Inches'),
)
RACK_ELEVATION_STYLE = """ # Power feeds
* { POWERFEED_TYPE_PRIMARY = 1
font-family: sans-serif; POWERFEED_TYPE_REDUNDANT = 2
font-size: 13px; POWERFEED_TYPE_CHOICES = (
} (POWERFEED_TYPE_PRIMARY, 'Primary'),
rect { (POWERFEED_TYPE_REDUNDANT, 'Redundant'),
box-sizing: border-box; )
} POWERFEED_SUPPLY_AC = 1
text { POWERFEED_SUPPLY_DC = 2
text-anchor: middle; POWERFEED_SUPPLY_CHOICES = (
dominant-baseline: middle; (POWERFEED_SUPPLY_AC, 'AC'),
} (POWERFEED_SUPPLY_DC, 'DC'),
.rack { )
background-color: #f0f0f0; POWERFEED_PHASE_SINGLE = 1
fill: none; POWERFEED_PHASE_3PHASE = 3
stroke: black; POWERFEED_PHASE_CHOICES = (
stroke-width: 3px; (POWERFEED_PHASE_SINGLE, 'Single phase'),
} (POWERFEED_PHASE_3PHASE, 'Three-phase'),
.slot { )
fill: #f7f7f7; POWERFEED_STATUS_OFFLINE = 0
stroke: #a0a0a0; POWERFEED_STATUS_ACTIVE = 1
} POWERFEED_STATUS_PLANNED = 2
.slot:hover { POWERFEED_STATUS_FAILED = 4
fill: #fff; POWERFEED_STATUS_CHOICES = (
} (POWERFEED_STATUS_ACTIVE, 'Active'),
.slot+.add-device { (POWERFEED_STATUS_OFFLINE, 'Offline'),
fill: none; (POWERFEED_STATUS_PLANNED, 'Planned'),
} (POWERFEED_STATUS_FAILED, 'Failed'),
.slot:hover+.add-device { )
fill: blue; POWERFEED_LEG_A = 1
} POWERFEED_LEG_B = 2
.add-device:hover { POWERFEED_LEG_C = 3
fill: blue; POWERFEED_LEG_CHOICES = (
} (POWERFEED_LEG_A, 'A'),
.add-device:hover+.slot { (POWERFEED_LEG_B, 'B'),
fill: #fff; (POWERFEED_LEG_C, 'C'),
} )
.reserved {
fill: url(#reserved);
}
.reserved:hover {
fill: url(#reserved);
}
.occupied {
fill: url(#occupied);
}
.occupied:hover {
fill: url(#occupied);
}
.blocked {
fill: url(#blocked);
}
.blocked:hover {
fill: url(#blocked);
}
.blocked:hover+.add-device {
fill: none;
}
"""
# Rack Elevation SVG Size
RACK_ELEVATION_UNIT_WIDTH_DEFAULT = 230
RACK_ELEVATION_UNIT_HEIGHT_DEFAULT = 20

View File

@@ -22,7 +22,7 @@ class MACAddressField(models.Field):
def python_type(self): def python_type(self):
return EUI return EUI
def from_db_value(self, value, expression, connection): def from_db_value(self, value, expression, connection, context):
return self.to_python(value) return self.to_python(value)
def to_python(self, value): def to_python(self, value):

View File

@@ -2,16 +2,15 @@ import django_filters
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.db.models import Q from django.db.models import Q
from extras.filters import CustomFieldFilterSet, LocalConfigContextFilter, CreatedUpdatedFilterSet from extras.filters import CustomFieldFilterSet, LocalConfigContextFilter
from tenancy.filtersets import TenancyFilterSet from tenancy.filtersets import TenancyFilterSet
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.constants import COLOR_CHOICES from utilities.constants import COLOR_CHOICES
from utilities.filters import ( from utilities.filters import (
MultiValueCharFilter, MultiValueMACAddressFilter, MultiValueNumberFilter, NameSlugSearchFilterSet, NumericInFilter, MultiValueMACAddressFilter, MultiValueNumberFilter, NameSlugSearchFilterSet, NumericInFilter, TagFilter,
TagFilter, TreeNodeMultipleChoiceFilter, TreeNodeMultipleChoiceFilter,
) )
from virtualization.models import Cluster from virtualization.models import Cluster
from .choices import *
from .constants import * from .constants import *
from .models import ( from .models import (
Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
@@ -39,7 +38,7 @@ class RegionFilter(NameSlugSearchFilterSet):
fields = ['id', 'name', 'slug'] fields = ['id', 'name', 'slug']
class SiteFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet): class SiteFilter(TenancyFilterSet, CustomFieldFilterSet):
id__in = NumericInFilter( id__in = NumericInFilter(
field_name='id', field_name='id',
lookup_expr='in' lookup_expr='in'
@@ -49,7 +48,7 @@ class SiteFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet
label='Search', label='Search',
) )
status = django_filters.MultipleChoiceFilter( status = django_filters.MultipleChoiceFilter(
choices=SiteStatusChoices, choices=SITE_STATUS_CHOICES,
null_value=None null_value=None
) )
region_id = TreeNodeMultipleChoiceFilter( region_id = TreeNodeMultipleChoiceFilter(
@@ -117,7 +116,7 @@ class RackRoleFilter(NameSlugSearchFilterSet):
fields = ['id', 'name', 'slug', 'color'] fields = ['id', 'name', 'slug', 'color']
class RackFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet): class RackFilter(TenancyFilterSet, CustomFieldFilterSet):
id__in = NumericInFilter( id__in = NumericInFilter(
field_name='id', field_name='id',
lookup_expr='in' lookup_expr='in'
@@ -147,7 +146,7 @@ class RackFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet
label='Group', label='Group',
) )
status = django_filters.MultipleChoiceFilter( status = django_filters.MultipleChoiceFilter(
choices=RackStatusChoices, choices=RACK_STATUS_CHOICES,
null_value=None null_value=None
) )
role_id = django_filters.ModelMultipleChoiceFilter( role_id = django_filters.ModelMultipleChoiceFilter(
@@ -252,7 +251,7 @@ class ManufacturerFilter(NameSlugSearchFilterSet):
fields = ['id', 'name', 'slug'] fields = ['id', 'name', 'slug']
class DeviceTypeFilter(CustomFieldFilterSet, CreatedUpdatedFilterSet): class DeviceTypeFilter(CustomFieldFilterSet):
id__in = NumericInFilter( id__in = NumericInFilter(
field_name='id', field_name='id',
lookup_expr='in' lookup_expr='in'
@@ -347,28 +346,28 @@ class ConsolePortTemplateFilter(DeviceTypeComponentFilterSet):
class Meta: class Meta:
model = ConsolePortTemplate model = ConsolePortTemplate
fields = ['id', 'name', 'type'] fields = ['id', 'name']
class ConsoleServerPortTemplateFilter(DeviceTypeComponentFilterSet): class ConsoleServerPortTemplateFilter(DeviceTypeComponentFilterSet):
class Meta: class Meta:
model = ConsoleServerPortTemplate model = ConsoleServerPortTemplate
fields = ['id', 'name', 'type'] fields = ['id', 'name']
class PowerPortTemplateFilter(DeviceTypeComponentFilterSet): class PowerPortTemplateFilter(DeviceTypeComponentFilterSet):
class Meta: class Meta:
model = PowerPortTemplate model = PowerPortTemplate
fields = ['id', 'name', 'type', 'maximum_draw', 'allocated_draw'] fields = ['id', 'name', 'maximum_draw', 'allocated_draw']
class PowerOutletTemplateFilter(DeviceTypeComponentFilterSet): class PowerOutletTemplateFilter(DeviceTypeComponentFilterSet):
class Meta: class Meta:
model = PowerOutletTemplate model = PowerOutletTemplate
fields = ['id', 'name', 'type', 'feed_leg'] fields = ['id', 'name', 'feed_leg']
class InterfaceTemplateFilter(DeviceTypeComponentFilterSet): class InterfaceTemplateFilter(DeviceTypeComponentFilterSet):
@@ -424,7 +423,7 @@ class PlatformFilter(NameSlugSearchFilterSet):
fields = ['id', 'name', 'slug', 'napalm_driver'] fields = ['id', 'name', 'slug', 'napalm_driver']
class DeviceFilter(LocalConfigContextFilter, TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet): class DeviceFilter(LocalConfigContextFilter, TenancyFilterSet, CustomFieldFilterSet):
id__in = NumericInFilter( id__in = NumericInFilter(
field_name='id', field_name='id',
lookup_expr='in' lookup_expr='in'
@@ -511,7 +510,7 @@ class DeviceFilter(LocalConfigContextFilter, TenancyFilterSet, CustomFieldFilter
label='Device model (slug)', label='Device model (slug)',
) )
status = django_filters.MultipleChoiceFilter( status = django_filters.MultipleChoiceFilter(
choices=DeviceStatusChoices, choices=DEVICE_STATUS_CHOICES,
null_value=None null_value=None
) )
is_full_depth = django_filters.BooleanFilter( is_full_depth = django_filters.BooleanFilter(
@@ -621,26 +620,6 @@ class DeviceComponentFilterSet(django_filters.FilterSet):
method='search', method='search',
label='Search', label='Search',
) )
region_id = django_filters.ModelMultipleChoiceFilter(
field_name='device__site__region',
queryset=Region.objects.all(),
label='Region (ID)',
)
region = django_filters.ModelMultipleChoiceFilter(
field_name='device__site__region__in',
queryset=Region.objects.all(),
label='Region name (slug)',
)
site_id = django_filters.ModelMultipleChoiceFilter(
field_name='device__site',
queryset=Site.objects.all(),
label='Site (ID)',
)
site = django_filters.ModelMultipleChoiceFilter(
field_name='device__site__slug',
queryset=Site.objects.all(),
label='Site name (slug)',
)
device_id = django_filters.ModelMultipleChoiceFilter( device_id = django_filters.ModelMultipleChoiceFilter(
queryset=Device.objects.all(), queryset=Device.objects.all(),
label='Device (ID)', label='Device (ID)',
@@ -662,10 +641,6 @@ class DeviceComponentFilterSet(django_filters.FilterSet):
class ConsolePortFilter(DeviceComponentFilterSet): class ConsolePortFilter(DeviceComponentFilterSet):
type = django_filters.MultipleChoiceFilter(
choices=ConsolePortTypeChoices,
null_value=None
)
cabled = django_filters.BooleanFilter( cabled = django_filters.BooleanFilter(
field_name='cable', field_name='cable',
lookup_expr='isnull', lookup_expr='isnull',
@@ -678,10 +653,6 @@ class ConsolePortFilter(DeviceComponentFilterSet):
class ConsoleServerPortFilter(DeviceComponentFilterSet): class ConsoleServerPortFilter(DeviceComponentFilterSet):
type = django_filters.MultipleChoiceFilter(
choices=ConsolePortTypeChoices,
null_value=None
)
cabled = django_filters.BooleanFilter( cabled = django_filters.BooleanFilter(
field_name='cable', field_name='cable',
lookup_expr='isnull', lookup_expr='isnull',
@@ -694,10 +665,6 @@ class ConsoleServerPortFilter(DeviceComponentFilterSet):
class PowerPortFilter(DeviceComponentFilterSet): class PowerPortFilter(DeviceComponentFilterSet):
type = django_filters.MultipleChoiceFilter(
choices=PowerPortTypeChoices,
null_value=None
)
cabled = django_filters.BooleanFilter( cabled = django_filters.BooleanFilter(
field_name='cable', field_name='cable',
lookup_expr='isnull', lookup_expr='isnull',
@@ -710,10 +677,6 @@ class PowerPortFilter(DeviceComponentFilterSet):
class PowerOutletFilter(DeviceComponentFilterSet): class PowerOutletFilter(DeviceComponentFilterSet):
type = django_filters.MultipleChoiceFilter(
choices=PowerOutletTypeChoices,
null_value=None
)
cabled = django_filters.BooleanFilter( cabled = django_filters.BooleanFilter(
field_name='cable', field_name='cable',
lookup_expr='isnull', lookup_expr='isnull',
@@ -733,28 +696,7 @@ class InterfaceFilter(django_filters.FilterSet):
method='search', method='search',
label='Search', label='Search',
) )
region_id = django_filters.ModelMultipleChoiceFilter( device = django_filters.CharFilter(
field_name='device__site__region',
queryset=Region.objects.all(),
label='Region (ID)',
)
region = django_filters.ModelMultipleChoiceFilter(
field_name='device__site__region__in',
queryset=Region.objects.all(),
label='Region name (slug)',
)
site_id = django_filters.ModelMultipleChoiceFilter(
field_name='device__site',
queryset=Site.objects.all(),
label='Site (ID)',
)
site = django_filters.ModelMultipleChoiceFilter(
field_name='device__site__slug',
to_field_name='slug',
queryset=Site.objects.all(),
label='Site name (slug)',
)
device = MultiValueCharFilter(
method='filter_device', method='filter_device',
field_name='name', field_name='name',
label='Device', label='Device',
@@ -789,7 +731,7 @@ class InterfaceFilter(django_filters.FilterSet):
label='Assigned VID' label='Assigned VID'
) )
type = django_filters.MultipleChoiceFilter( type = django_filters.MultipleChoiceFilter(
choices=InterfaceTypeChoices, choices=IFACE_TYPE_CHOICES,
null_value=None null_value=None
) )
@@ -807,10 +749,8 @@ class InterfaceFilter(django_filters.FilterSet):
def filter_device(self, queryset, name, value): def filter_device(self, queryset, name, value):
try: try:
devices = Device.objects.filter(**{'{}__in'.format(name): value}) device = Device.objects.get(**{name: value})
vc_interface_ids = [] vc_interface_ids = device.vc_interfaces.values_list('id', flat=True)
for device in devices:
vc_interface_ids.extend(device.vc_interfaces.values_list('id', flat=True))
return queryset.filter(pk__in=vc_interface_ids) return queryset.filter(pk__in=vc_interface_ids)
except Device.DoesNotExist: except Device.DoesNotExist:
return queryset.none() return queryset.none()
@@ -982,10 +922,10 @@ class CableFilter(django_filters.FilterSet):
label='Search', label='Search',
) )
type = django_filters.MultipleChoiceFilter( type = django_filters.MultipleChoiceFilter(
choices=CableTypeChoices choices=CABLE_TYPE_CHOICES
) )
status = django_filters.MultipleChoiceFilter( status = django_filters.MultipleChoiceFilter(
choices=CableStatusChoices choices=CONNECTION_STATUS_CHOICES
) )
color = django_filters.MultipleChoiceFilter( color = django_filters.MultipleChoiceFilter(
choices=COLOR_CHOICES choices=COLOR_CHOICES
@@ -1156,7 +1096,7 @@ class PowerPanelFilter(django_filters.FilterSet):
return queryset.filter(qs_filter) return queryset.filter(qs_filter)
class PowerFeedFilter(CustomFieldFilterSet, CreatedUpdatedFilterSet): class PowerFeedFilter(CustomFieldFilterSet):
id__in = NumericInFilter( id__in = NumericInFilter(
field_name='id', field_name='id',
lookup_expr='in' lookup_expr='in'

View File

@@ -1910,7 +1910,7 @@
"site": 1, "site": 1,
"rack": 1, "rack": 1,
"position": 1, "position": 1,
"face": "front", "face": 0,
"status": true, "status": true,
"primary_ip4": 1, "primary_ip4": 1,
"primary_ip6": null, "primary_ip6": null,
@@ -1931,7 +1931,7 @@
"site": 1, "site": 1,
"rack": 1, "rack": 1,
"position": 17, "position": 17,
"face": "rear", "face": 0,
"status": true, "status": true,
"primary_ip4": 5, "primary_ip4": 5,
"primary_ip6": null, "primary_ip6": null,
@@ -1952,7 +1952,7 @@
"site": 1, "site": 1,
"rack": 1, "rack": 1,
"position": 33, "position": 33,
"face": "rear", "face": 0,
"status": true, "status": true,
"primary_ip4": null, "primary_ip4": null,
"primary_ip6": null, "primary_ip6": null,
@@ -1973,7 +1973,7 @@
"site": 1, "site": 1,
"rack": 1, "rack": 1,
"position": 34, "position": 34,
"face": "rear", "face": 0,
"status": true, "status": true,
"primary_ip4": null, "primary_ip4": null,
"primary_ip6": null, "primary_ip6": null,
@@ -1994,7 +1994,7 @@
"site": 1, "site": 1,
"rack": 2, "rack": 2,
"position": 34, "position": 34,
"face": "rear", "face": 0,
"status": true, "status": true,
"primary_ip4": null, "primary_ip4": null,
"primary_ip6": null, "primary_ip6": null,
@@ -2015,7 +2015,7 @@
"site": 1, "site": 1,
"rack": 2, "rack": 2,
"position": 33, "position": 33,
"face": "rear", "face": 0,
"status": true, "status": true,
"primary_ip4": null, "primary_ip4": null,
"primary_ip6": null, "primary_ip6": null,
@@ -2036,7 +2036,7 @@
"site": 1, "site": 1,
"rack": 2, "rack": 2,
"position": 1, "position": 1,
"face": "rear", "face": 0,
"status": true, "status": true,
"primary_ip4": 3, "primary_ip4": 3,
"primary_ip6": null, "primary_ip6": null,
@@ -2057,7 +2057,7 @@
"site": 1, "site": 1,
"rack": 2, "rack": 2,
"position": 17, "position": 17,
"face": "rear", "face": 0,
"status": true, "status": true,
"primary_ip4": 19, "primary_ip4": 19,
"primary_ip6": null, "primary_ip6": null,
@@ -2078,7 +2078,7 @@
"site": 1, "site": 1,
"rack": 1, "rack": 1,
"position": 42, "position": 42,
"face": "rear", "face": 0,
"status": true, "status": true,
"primary_ip4": null, "primary_ip4": null,
"primary_ip6": null, "primary_ip6": null,
@@ -2099,7 +2099,7 @@
"site": 1, "site": 1,
"rack": 1, "rack": 1,
"position": null, "position": null,
"face": "", "face": null,
"status": true, "status": true,
"primary_ip4": null, "primary_ip4": null,
"primary_ip6": null, "primary_ip6": null,
@@ -2120,7 +2120,7 @@
"site": 1, "site": 1,
"rack": 2, "rack": 2,
"position": null, "position": null,
"face": "", "face": null,
"status": true, "status": true,
"primary_ip4": null, "primary_ip4": null,
"primary_ip6": null, "primary_ip6": null,

View File

@@ -0,0 +1,195 @@
[
{
"model": "dcim.devicerole",
"pk": 1,
"fields": {
"name": "Console Server",
"slug": "console-server",
"color": "009688"
}
},
{
"model": "dcim.devicerole",
"pk": 2,
"fields": {
"name": "Core Switch",
"slug": "core-switch",
"color": "2196f3"
}
},
{
"model": "dcim.devicerole",
"pk": 3,
"fields": {
"name": "Distribution Switch",
"slug": "distribution-switch",
"color": "2196f3"
}
},
{
"model": "dcim.devicerole",
"pk": 4,
"fields": {
"name": "Access Switch",
"slug": "access-switch",
"color": "2196f3"
}
},
{
"model": "dcim.devicerole",
"pk": 5,
"fields": {
"name": "Management Switch",
"slug": "management-switch",
"color": "ff9800"
}
},
{
"model": "dcim.devicerole",
"pk": 6,
"fields": {
"name": "Firewall",
"slug": "firewall",
"color": "f44336"
}
},
{
"model": "dcim.devicerole",
"pk": 7,
"fields": {
"name": "Router",
"slug": "router",
"color": "9c27b0"
}
},
{
"model": "dcim.devicerole",
"pk": 8,
"fields": {
"name": "Server",
"slug": "server",
"color": "9e9e9e"
}
},
{
"model": "dcim.devicerole",
"pk": 9,
"fields": {
"name": "PDU",
"slug": "pdu",
"color": "607d8b"
}
},
{
"model": "dcim.manufacturer",
"pk": 1,
"fields": {
"name": "APC",
"slug": "apc"
}
},
{
"model": "dcim.manufacturer",
"pk": 2,
"fields": {
"name": "Cisco",
"slug": "cisco"
}
},
{
"model": "dcim.manufacturer",
"pk": 3,
"fields": {
"name": "Dell",
"slug": "dell"
}
},
{
"model": "dcim.manufacturer",
"pk": 4,
"fields": {
"name": "HP",
"slug": "hp"
}
},
{
"model": "dcim.manufacturer",
"pk": 5,
"fields": {
"name": "Juniper",
"slug": "juniper"
}
},
{
"model": "dcim.manufacturer",
"pk": 6,
"fields": {
"name": "Arista",
"slug": "arista"
}
},
{
"model": "dcim.manufacturer",
"pk": 7,
"fields": {
"name": "Opengear",
"slug": "opengear"
}
},
{
"model": "dcim.manufacturer",
"pk": 8,
"fields": {
"name": "Super Micro",
"slug": "super-micro"
}
},
{
"model": "dcim.platform",
"pk": 1,
"fields": {
"name": "Cisco IOS",
"slug": "cisco-ios"
}
},
{
"model": "dcim.platform",
"pk": 2,
"fields": {
"name": "Cisco NX-OS",
"slug": "cisco-nx-os"
}
},
{
"model": "dcim.platform",
"pk": 3,
"fields": {
"name": "Juniper Junos",
"slug": "juniper-junos"
}
},
{
"model": "dcim.platform",
"pk": 4,
"fields": {
"name": "Arista EOS",
"slug": "arista-eos"
}
},
{
"model": "dcim.platform",
"pk": 5,
"fields": {
"name": "Linux",
"slug": "linux"
}
},
{
"model": "dcim.platform",
"pk": 6,
"fields": {
"name": "Opengear",
"slug": "opengear"
}
}
]

File diff suppressed because it is too large Load Diff

View File

@@ -174,8 +174,8 @@ class Migration(migrations.Migration):
('length', models.PositiveSmallIntegerField(blank=True, null=True)), ('length', models.PositiveSmallIntegerField(blank=True, null=True)),
('length_unit', models.PositiveSmallIntegerField(blank=True, null=True)), ('length_unit', models.PositiveSmallIntegerField(blank=True, null=True)),
('_abs_length', models.DecimalField(blank=True, decimal_places=4, max_digits=10, null=True)), ('_abs_length', models.DecimalField(blank=True, decimal_places=4, max_digits=10, null=True)),
('termination_a_type', models.ForeignKey(limit_choices_to={'model__in': ['consoleport', 'consoleserverport', 'interface', 'poweroutlet', 'powerport', 'frontport', 'rearport', 'circuittermination', 'powerfeed']}, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.ContentType')), ('termination_a_type', models.ForeignKey(limit_choices_to={'model__in': ['consoleport', 'consoleserverport', 'interface', 'poweroutlet', 'powerport', 'frontport', 'rearport', 'circuittermination']}, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.ContentType')),
('termination_b_type', models.ForeignKey(limit_choices_to={'model__in': ['consoleport', 'consoleserverport', 'interface', 'poweroutlet', 'powerport', 'frontport', 'rearport', 'circuittermination', 'powerfeed']}, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.ContentType')), ('termination_b_type', models.ForeignKey(limit_choices_to={'model__in': ['consoleport', 'consoleserverport', 'interface', 'poweroutlet', 'powerport', 'frontport', 'rearport', 'circuittermination']}, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.ContentType')),
], ],
), ),
migrations.AlterUniqueTogether( migrations.AlterUniqueTogether(

View File

@@ -1,33 +0,0 @@
# 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),
),
]

View File

@@ -1,33 +0,0 @@
# 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),
),
]

View File

@@ -1,35 +0,0 @@
from django.db import migrations, models
SITE_STATUS_CHOICES = (
(1, 'active'),
(2, 'planned'),
(4, 'retired'),
)
def site_status_to_slug(apps, schema_editor):
Site = apps.get_model('dcim', 'Site')
for id, slug in SITE_STATUS_CHOICES:
Site.objects.filter(status=str(id)).update(status=slug)
class Migration(migrations.Migration):
atomic = False
dependencies = [
('dcim', '0077_power_types'),
]
operations = [
# Site.status
migrations.AlterField(
model_name='site',
name='status',
field=models.CharField(default='active', max_length=50),
),
migrations.RunPython(
code=site_status_to_slug
),
]

View File

@@ -1,92 +0,0 @@
from django.db import migrations, models
RACK_TYPE_CHOICES = (
(100, '2-post-frame'),
(200, '4-post-frame'),
(300, '4-post-cabinet'),
(1000, 'wall-frame'),
(1100, 'wall-cabinet'),
)
RACK_STATUS_CHOICES = (
(0, 'reserved'),
(1, 'available'),
(2, 'planned'),
(3, 'active'),
(4, 'deprecated'),
)
RACK_DIMENSION_CHOICES = (
(1000, 'mm'),
(2000, 'in'),
)
def rack_type_to_slug(apps, schema_editor):
Rack = apps.get_model('dcim', 'Rack')
for id, slug in RACK_TYPE_CHOICES:
Rack.objects.filter(type=str(id)).update(type=slug)
def rack_status_to_slug(apps, schema_editor):
Rack = apps.get_model('dcim', 'Rack')
for id, slug in RACK_STATUS_CHOICES:
Rack.objects.filter(status=str(id)).update(status=slug)
def rack_outer_unit_to_slug(apps, schema_editor):
Rack = apps.get_model('dcim', 'Rack')
for id, slug in RACK_DIMENSION_CHOICES:
Rack.objects.filter(status=str(id)).update(status=slug)
class Migration(migrations.Migration):
atomic = False
dependencies = [
('dcim', '0078_3569_site_fields'),
]
operations = [
# Rack.type
migrations.AlterField(
model_name='rack',
name='type',
field=models.CharField(blank=True, default='', max_length=50),
),
migrations.RunPython(
code=rack_type_to_slug
),
migrations.AlterField(
model_name='rack',
name='type',
field=models.CharField(blank=True, max_length=50),
),
# Rack.status
migrations.AlterField(
model_name='rack',
name='status',
field=models.CharField(default='active', max_length=50),
),
migrations.RunPython(
code=rack_status_to_slug
),
# Rack.outer_unit
migrations.AlterField(
model_name='rack',
name='outer_unit',
field=models.CharField(blank=True, default='', max_length=50),
),
migrations.RunPython(
code=rack_outer_unit_to_slug
),
migrations.AlterField(
model_name='rack',
name='outer_unit',
field=models.CharField(blank=True, max_length=50),
),
]

View File

@@ -1,39 +0,0 @@
from django.db import migrations, models
SUBDEVICE_ROLE_CHOICES = (
('true', 'parent'),
('false', 'child'),
)
def devicetype_subdevicerole_to_slug(apps, schema_editor):
DeviceType = apps.get_model('dcim', 'DeviceType')
for boolean, slug in SUBDEVICE_ROLE_CHOICES:
DeviceType.objects.filter(subdevice_role=boolean).update(subdevice_role=slug)
class Migration(migrations.Migration):
atomic = False
dependencies = [
('dcim', '0079_3569_rack_fields'),
]
operations = [
# DeviceType.subdevice_role
migrations.AlterField(
model_name='devicetype',
name='subdevice_role',
field=models.CharField(blank=True, default='', max_length=50),
),
migrations.RunPython(
code=devicetype_subdevicerole_to_slug
),
migrations.AlterField(
model_name='devicetype',
name='subdevice_role',
field=models.CharField(blank=True, max_length=50),
),
]

View File

@@ -1,65 +0,0 @@
from django.db import migrations, models
DEVICE_FACE_CHOICES = (
(0, 'front'),
(1, 'rear'),
)
DEVICE_STATUS_CHOICES = (
(0, 'offline'),
(1, 'active'),
(2, 'planned'),
(3, 'staged'),
(4, 'failed'),
(5, 'inventory'),
(6, 'decommissioning'),
)
def device_face_to_slug(apps, schema_editor):
Device = apps.get_model('dcim', 'Device')
for id, slug in DEVICE_FACE_CHOICES:
Device.objects.filter(face=str(id)).update(face=slug)
def device_status_to_slug(apps, schema_editor):
Device = apps.get_model('dcim', 'Device')
for id, slug in DEVICE_STATUS_CHOICES:
Device.objects.filter(status=str(id)).update(status=slug)
class Migration(migrations.Migration):
atomic = False
dependencies = [
('dcim', '0080_3569_devicetype_fields'),
]
operations = [
# Device.face
migrations.AlterField(
model_name='device',
name='face',
field=models.CharField(blank=True, default='', max_length=50),
),
migrations.RunPython(
code=device_face_to_slug
),
migrations.AlterField(
model_name='device',
name='face',
field=models.CharField(blank=True, max_length=50),
),
# Device.status
migrations.AlterField(
model_name='device',
name='status',
field=models.CharField(default='active', max_length=50),
),
migrations.RunPython(
code=device_status_to_slug
),
]

View File

@@ -1,147 +0,0 @@
from django.db import migrations, models
INTERFACE_TYPE_CHOICES = (
(0, 'virtual'),
(200, 'lag'),
(800, '100base-tx'),
(1000, '1000base-t'),
(1050, '1000base-x-gbic'),
(1100, '1000base-x-sfp'),
(1120, '2.5gbase-t'),
(1130, '5gbase-t'),
(1150, '10gbase-t'),
(1170, '10gbase-cx4'),
(1200, '10gbase-x-sfpp'),
(1300, '10gbase-x-xfp'),
(1310, '10gbase-x-xenpak'),
(1320, '10gbase-x-x2'),
(1350, '25gbase-x-sfp28'),
(1400, '40gbase-x-qsfpp'),
(1420, '50gbase-x-sfp28'),
(1500, '100gbase-x-cfp'),
(1510, '100gbase-x-cfp2'),
(1520, '100gbase-x-cfp4'),
(1550, '100gbase-x-cpak'),
(1600, '100gbase-x-qsfp28'),
(1650, '200gbase-x-cfp2'),
(1700, '200gbase-x-qsfp56'),
(1750, '400gbase-x-qsfpdd'),
(1800, '400gbase-x-osfp'),
(2600, 'ieee802.11a'),
(2610, 'ieee802.11g'),
(2620, 'ieee802.11n'),
(2630, 'ieee802.11ac'),
(2640, 'ieee802.11ad'),
(2810, 'gsm'),
(2820, 'cdma'),
(2830, 'lte'),
(6100, 'sonet-oc3'),
(6200, 'sonet-oc12'),
(6300, 'sonet-oc48'),
(6400, 'sonet-oc192'),
(6500, 'sonet-oc768'),
(6600, 'sonet-oc1920'),
(6700, 'sonet-oc3840'),
(3010, '1gfc-sfp'),
(3020, '2gfc-sfp'),
(3040, '4gfc-sfp'),
(3080, '8gfc-sfpp'),
(3160, '16gfc-sfpp'),
(3320, '32gfc-sfp28'),
(3400, '128gfc-sfp28'),
(7010, 'inifiband-sdr'),
(7020, 'inifiband-ddr'),
(7030, 'inifiband-qdr'),
(7040, 'inifiband-fdr10'),
(7050, 'inifiband-fdr'),
(7060, 'inifiband-edr'),
(7070, 'inifiband-hdr'),
(7080, 'inifiband-ndr'),
(7090, 'inifiband-xdr'),
(4000, 't1'),
(4010, 'e1'),
(4040, 't3'),
(4050, 'e3'),
(5000, 'cisco-stackwise'),
(5050, 'cisco-stackwise-plus'),
(5100, 'cisco-flexstack'),
(5150, 'cisco-flexstack-plus'),
(5200, 'juniper-vcp'),
(5300, 'extreme-summitstack'),
(5310, 'extreme-summitstack-128'),
(5320, 'extreme-summitstack-256'),
(5330, 'extreme-summitstack-512'),
)
INTERFACE_MODE_CHOICES = (
(100, 'access'),
(200, 'tagged'),
(300, 'tagged-all'),
)
def interfacetemplate_type_to_slug(apps, schema_editor):
InterfaceTemplate = apps.get_model('dcim', 'InterfaceTemplate')
for id, slug in INTERFACE_TYPE_CHOICES:
InterfaceTemplate.objects.filter(type=id).update(type=slug)
def interface_type_to_slug(apps, schema_editor):
Interface = apps.get_model('dcim', 'Interface')
for id, slug in INTERFACE_TYPE_CHOICES:
Interface.objects.filter(type=id).update(type=slug)
def interface_mode_to_slug(apps, schema_editor):
Interface = apps.get_model('dcim', 'Interface')
for id, slug in INTERFACE_MODE_CHOICES:
Interface.objects.filter(mode=id).update(mode=slug)
class Migration(migrations.Migration):
atomic = False
dependencies = [
('dcim', '0081_3569_device_fields'),
]
operations = [
# InterfaceTemplate.type
migrations.AlterField(
model_name='interfacetemplate',
name='type',
field=models.CharField(max_length=50),
),
migrations.RunPython(
code=interfacetemplate_type_to_slug
),
# Interface.type
migrations.AlterField(
model_name='interface',
name='type',
field=models.CharField(max_length=50),
),
migrations.RunPython(
code=interface_type_to_slug
),
# Interface.mode
migrations.AlterField(
model_name='interface',
name='mode',
field=models.CharField(blank=True, default='', max_length=50),
),
migrations.RunPython(
code=interface_mode_to_slug
),
migrations.AlterField(
model_name='interface',
name='mode',
field=models.CharField(blank=True, max_length=50),
),
]

View File

@@ -1,93 +0,0 @@
from django.db import migrations, models
PORT_TYPE_CHOICES = (
(1000, '8p8c'),
(1100, '110-punch'),
(1200, 'bnc'),
(2000, 'st'),
(2100, 'sc'),
(2110, 'sc-apc'),
(2200, 'fc'),
(2300, 'lc'),
(2310, 'lc-apc'),
(2400, 'mtrj'),
(2500, 'mpo'),
(2600, 'lsh'),
(2610, 'lsh-apc'),
)
def frontporttemplate_type_to_slug(apps, schema_editor):
FrontPortTemplate = apps.get_model('dcim', 'FrontPortTemplate')
for id, slug in PORT_TYPE_CHOICES:
FrontPortTemplate.objects.filter(type=id).update(type=slug)
def rearporttemplate_type_to_slug(apps, schema_editor):
RearPortTemplate = apps.get_model('dcim', 'RearPortTemplate')
for id, slug in PORT_TYPE_CHOICES:
RearPortTemplate.objects.filter(type=id).update(type=slug)
def frontport_type_to_slug(apps, schema_editor):
FrontPort = apps.get_model('dcim', 'FrontPort')
for id, slug in PORT_TYPE_CHOICES:
FrontPort.objects.filter(type=id).update(type=slug)
def rearport_type_to_slug(apps, schema_editor):
RearPort = apps.get_model('dcim', 'RearPort')
for id, slug in PORT_TYPE_CHOICES:
RearPort.objects.filter(type=id).update(type=slug)
class Migration(migrations.Migration):
atomic = False
dependencies = [
('dcim', '0082_3569_interface_fields'),
]
operations = [
# FrontPortTemplate.type
migrations.AlterField(
model_name='frontporttemplate',
name='type',
field=models.CharField(max_length=50),
),
migrations.RunPython(
code=frontporttemplate_type_to_slug
),
# RearPortTemplate.type
migrations.AlterField(
model_name='rearporttemplate',
name='type',
field=models.CharField(max_length=50),
),
migrations.RunPython(
code=rearporttemplate_type_to_slug
),
# FrontPort.type
migrations.AlterField(
model_name='frontport',
name='type',
field=models.CharField(max_length=50),
),
migrations.RunPython(
code=frontport_type_to_slug
),
# RearPort.type
migrations.AlterField(
model_name='rearport',
name='type',
field=models.CharField(max_length=50),
),
migrations.RunPython(
code=rearport_type_to_slug
),
]

View File

@@ -1,106 +0,0 @@
from django.db import migrations, models
CABLE_TYPE_CHOICES = (
(1300, 'cat3'),
(1500, 'cat5'),
(1510, 'cat5e'),
(1600, 'cat6'),
(1610, 'cat6a'),
(1700, 'cat7'),
(1800, 'dac-active'),
(1810, 'dac-passive'),
(1900, 'coaxial'),
(3000, 'mmf'),
(3010, 'mmf-om1'),
(3020, 'mmf-om2'),
(3030, 'mmf-om3'),
(3040, 'mmf-om4'),
(3500, 'smf'),
(3510, 'smf-os1'),
(3520, 'smf-os2'),
(3800, 'aoc'),
(5000, 'power'),
)
CABLE_STATUS_CHOICES = (
(True, 'connected'),
(False, 'planned'),
)
CABLE_LENGTH_UNIT_CHOICES = (
(1200, 'm'),
(1100, 'cm'),
(2100, 'ft'),
(2000, 'in'),
)
def cable_type_to_slug(apps, schema_editor):
Cable = apps.get_model('dcim', 'Cable')
for id, slug in CABLE_TYPE_CHOICES:
Cable.objects.filter(type=id).update(type=slug)
def cable_status_to_slug(apps, schema_editor):
Cable = apps.get_model('dcim', 'Cable')
for bool, slug in CABLE_STATUS_CHOICES:
Cable.objects.filter(status=str(bool)).update(status=slug)
def cable_length_unit_to_slug(apps, schema_editor):
Cable = apps.get_model('dcim', 'Cable')
for id, slug in CABLE_LENGTH_UNIT_CHOICES:
Cable.objects.filter(length_unit=id).update(length_unit=slug)
class Migration(migrations.Migration):
atomic = False
dependencies = [
('dcim', '0082_3569_port_fields'),
]
operations = [
# Cable.type
migrations.AlterField(
model_name='cable',
name='type',
field=models.CharField(blank=True, default='', max_length=50),
),
migrations.RunPython(
code=cable_type_to_slug
),
migrations.AlterField(
model_name='cable',
name='type',
field=models.CharField(blank=True, max_length=50),
),
# Cable.status
migrations.AlterField(
model_name='cable',
name='status',
field=models.CharField(default='connected', max_length=50),
),
migrations.RunPython(
code=cable_status_to_slug
),
# Cable.length_unit
migrations.AlterField(
model_name='cable',
name='length_unit',
field=models.CharField(blank=True, default='', max_length=50),
),
migrations.RunPython(
code=cable_length_unit_to_slug
),
migrations.AlterField(
model_name='cable',
name='length_unit',
field=models.CharField(blank=True, max_length=50),
),
]

View File

@@ -1,100 +0,0 @@
from django.db import migrations, models
POWERFEED_STATUS_CHOICES = (
(0, 'offline'),
(1, 'active'),
(2, 'planned'),
(4, 'failed'),
)
POWERFEED_TYPE_CHOICES = (
(1, 'primary'),
(2, 'redundant'),
)
POWERFEED_SUPPLY_CHOICES = (
(1, 'ac'),
(2, 'dc'),
)
POWERFEED_PHASE_CHOICES = (
(1, 'single-phase'),
(3, 'three-phase'),
)
def powerfeed_status_to_slug(apps, schema_editor):
PowerFeed = apps.get_model('dcim', 'PowerFeed')
for id, slug in POWERFEED_STATUS_CHOICES:
PowerFeed.objects.filter(status=id).update(status=slug)
def powerfeed_type_to_slug(apps, schema_editor):
PowerFeed = apps.get_model('dcim', 'PowerFeed')
for id, slug in POWERFEED_TYPE_CHOICES:
PowerFeed.objects.filter(type=id).update(type=slug)
def powerfeed_supply_to_slug(apps, schema_editor):
PowerFeed = apps.get_model('dcim', 'PowerFeed')
for id, slug in POWERFEED_SUPPLY_CHOICES:
PowerFeed.objects.filter(supply=id).update(supply=slug)
def powerfeed_phase_to_slug(apps, schema_editor):
PowerFeed = apps.get_model('dcim', 'PowerFeed')
for id, slug in POWERFEED_PHASE_CHOICES:
PowerFeed.objects.filter(phase=id).update(phase=slug)
class Migration(migrations.Migration):
atomic = False
dependencies = [
('dcim', '0083_3569_cable_fields'),
]
operations = [
# PowerFeed.status
migrations.AlterField(
model_name='powerfeed',
name='status',
field=models.CharField(default='active', max_length=50),
),
migrations.RunPython(
code=powerfeed_status_to_slug
),
# PowerFeed.type
migrations.AlterField(
model_name='powerfeed',
name='type',
field=models.CharField(default='primary', max_length=50),
),
migrations.RunPython(
code=powerfeed_type_to_slug
),
# PowerFeed.supply
migrations.AlterField(
model_name='powerfeed',
name='supply',
field=models.CharField(default='ac', max_length=50),
),
migrations.RunPython(
code=powerfeed_supply_to_slug
),
# PowerFeed.phase
migrations.AlterField(
model_name='powerfeed',
name='phase',
field=models.CharField(default='single-phase', max_length=50),
),
migrations.RunPython(
code=powerfeed_phase_to_slug
),
]

View File

@@ -1,62 +0,0 @@
from django.db import migrations, models
POWEROUTLET_FEED_LEG_CHOICES_CHOICES = (
(1, 'A'),
(2, 'B'),
(3, 'C'),
)
def poweroutlettemplate_feed_leg_to_slug(apps, schema_editor):
PowerOutletTemplate = apps.get_model('dcim', 'PowerOutletTemplate')
for id, slug in POWEROUTLET_FEED_LEG_CHOICES_CHOICES:
PowerOutletTemplate.objects.filter(feed_leg=id).update(feed_leg=slug)
def poweroutlet_feed_leg_to_slug(apps, schema_editor):
PowerOutlet = apps.get_model('dcim', 'PowerOutlet')
for id, slug in POWEROUTLET_FEED_LEG_CHOICES_CHOICES:
PowerOutlet.objects.filter(feed_leg=id).update(feed_leg=slug)
class Migration(migrations.Migration):
atomic = False
dependencies = [
('dcim', '0084_3569_powerfeed_fields'),
]
operations = [
# PowerOutletTemplate.feed_leg
migrations.AlterField(
model_name='poweroutlettemplate',
name='feed_leg',
field=models.CharField(blank=True, default='', max_length=50),
),
migrations.RunPython(
code=poweroutlettemplate_feed_leg_to_slug
),
migrations.AlterField(
model_name='poweroutlettemplate',
name='feed_leg',
field=models.CharField(blank=True, max_length=50),
),
# PowerOutlet.feed_leg
migrations.AlterField(
model_name='poweroutlet',
name='feed_leg',
field=models.CharField(blank=True, default='', max_length=50),
),
migrations.RunPython(
code=poweroutlet_feed_leg_to_slug
),
migrations.AlterField(
model_name='poweroutlet',
name='feed_leg',
field=models.CharField(blank=True, max_length=50),
),
]

View File

@@ -1,23 +0,0 @@
# Generated by Django 2.2.6 on 2019-12-09 15:49
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('tenancy', '0006_custom_tag_models'),
('dcim', '0085_3569_poweroutlet_fields'),
]
operations = [
migrations.AlterField(
model_name='device',
name='name',
field=models.CharField(blank=True, max_length=64, null=True),
),
migrations.AlterUniqueTogether(
name='device',
unique_together={('rack', 'position', 'face'), ('virtual_chassis', 'vc_position'), ('site', 'tenant', 'name')},
),
]

View File

@@ -1,23 +0,0 @@
# Generated by Django 2.2.6 on 2019-12-10 17:15
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dcim', '0086_device_name_nonunique'),
]
operations = [
migrations.AddField(
model_name='devicerole',
name='description',
field=models.CharField(blank=True, max_length=100),
),
migrations.AddField(
model_name='rackrole',
name='description',
field=models.CharField(blank=True, max_length=100),
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 2.2.8 on 2019-12-12 02:09
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dcim', '0087_role_descriptions'),
]
operations = [
migrations.AlterField(
model_name='powerfeed',
name='available_power',
field=models.PositiveIntegerField(default=0, editable=False),
),
]

File diff suppressed because it is too large Load Diff

View File

@@ -45,7 +45,7 @@ def update_connected_endpoints(instance, **kwargs):
# Check if this Cable has formed a complete path. If so, update both endpoints. # Check if this Cable has formed a complete path. If so, update both endpoints.
endpoint_a, endpoint_b, path_status = instance.get_path_endpoints() endpoint_a, endpoint_b, path_status = instance.get_path_endpoints()
if getattr(endpoint_a, 'is_path_endpoint', False) and getattr(endpoint_b, 'is_path_endpoint', False): if endpoint_a is not None and endpoint_b is not None:
endpoint_a.connected_endpoint = endpoint_b endpoint_a.connected_endpoint = endpoint_b
endpoint_a.connection_status = path_status endpoint_a.connection_status = path_status
endpoint_a.save() endpoint_a.save()

View File

@@ -156,6 +156,10 @@ DEVICE_PRIMARY_IP = """
{{ record.primary_ip4.address.ip|default:"" }} {{ record.primary_ip4.address.ip|default:"" }}
""" """
SUBDEVICE_ROLE_TEMPLATE = """
{% if record.subdevice_role == True %}Parent{% elif record.subdevice_role == False %}Child{% else %}&mdash;{% endif %}
"""
DEVICETYPE_INSTANCES_TEMPLATE = """ DEVICETYPE_INSTANCES_TEMPLATE = """
<a href="{% url 'dcim:device_list' %}?manufacturer_id={{ record.manufacturer_id }}&device_type_id={{ record.pk }}">{{ record.instance_count }}</a> <a href="{% url 'dcim:device_list' %}?manufacturer_id={{ record.manufacturer_id }}&device_type_id={{ record.pk }}">{{ record.instance_count }}</a>
""" """
@@ -177,10 +181,8 @@ VIRTUALCHASSIS_ACTIONS = """
CABLE_TERMINATION_PARENT = """ CABLE_TERMINATION_PARENT = """
{% if value.device %} {% if value.device %}
<a href="{{ value.device.get_absolute_url }}">{{ value.device }}</a> <a href="{{ value.device.get_absolute_url }}">{{ value.device }}</a>
{% elif value.circuit %} {% else %}
<a href="{{ value.circuit.get_absolute_url }}">{{ value.circuit }}</a> <a href="{{ value.circuit.get_absolute_url }}">{{ value.circuit }}</a>
{% elif value.power_panel %}
<a href="{{ value.power_panel.get_absolute_url }}">{{ value.power_panel }}</a>
{% endif %} {% endif %}
""" """
@@ -272,17 +274,16 @@ class RackGroupTable(BaseTable):
class RackRoleTable(BaseTable): class RackRoleTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
name = tables.LinkColumn(verbose_name='Name')
rack_count = tables.Column(verbose_name='Racks') rack_count = tables.Column(verbose_name='Racks')
color = tables.TemplateColumn(COLOR_LABEL) color = tables.TemplateColumn(COLOR_LABEL, verbose_name='Color')
actions = tables.TemplateColumn( slug = tables.Column(verbose_name='Slug')
template_code=RACKROLE_ACTIONS, actions = tables.TemplateColumn(template_code=RACKROLE_ACTIONS, attrs={'td': {'class': 'text-right noprint'}},
attrs={'td': {'class': 'text-right noprint'}}, verbose_name='')
verbose_name=''
)
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = RackRole model = RackRole
fields = ('pk', 'name', 'rack_count', 'color', 'description', 'slug', 'actions') fields = ('pk', 'name', 'rack_count', 'color', 'slug', 'actions')
# #
@@ -390,6 +391,10 @@ class DeviceTypeTable(BaseTable):
verbose_name='Device Type' verbose_name='Device Type'
) )
is_full_depth = BooleanColumn(verbose_name='Full Depth') is_full_depth = BooleanColumn(verbose_name='Full Depth')
subdevice_role = tables.TemplateColumn(
template_code=SUBDEVICE_ROLE_TEMPLATE,
verbose_name='Subdevice Role'
)
instance_count = tables.TemplateColumn( instance_count = tables.TemplateColumn(
template_code=DEVICETYPE_INSTANCES_TEMPLATE, template_code=DEVICETYPE_INSTANCES_TEMPLATE,
verbose_name='Instances' verbose_name='Instances'
@@ -417,19 +422,10 @@ class ConsolePortTemplateTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = ConsolePortTemplate model = ConsolePortTemplate
fields = ('pk', 'name', 'type', 'actions') fields = ('pk', 'name', 'actions')
empty_text = "None" empty_text = "None"
class ConsolePortImportTable(BaseTable):
device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device')
class Meta(BaseTable.Meta):
model = ConsolePort
fields = ('device', 'name', 'description')
empty_text = False
class ConsoleServerPortTemplateTable(BaseTable): class ConsoleServerPortTemplateTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
actions = tables.TemplateColumn( actions = tables.TemplateColumn(
@@ -444,15 +440,6 @@ class ConsoleServerPortTemplateTable(BaseTable):
empty_text = "None" empty_text = "None"
class ConsoleServerPortImportTable(BaseTable):
device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device')
class Meta(BaseTable.Meta):
model = ConsoleServerPort
fields = ('device', 'name', 'description')
empty_text = False
class PowerPortTemplateTable(BaseTable): class PowerPortTemplateTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
actions = tables.TemplateColumn( actions = tables.TemplateColumn(
@@ -463,19 +450,10 @@ class PowerPortTemplateTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = PowerPortTemplate model = PowerPortTemplate
fields = ('pk', 'name', 'type', 'maximum_draw', 'allocated_draw', 'actions') fields = ('pk', 'name', 'maximum_draw', 'allocated_draw', 'actions')
empty_text = "None" empty_text = "None"
class PowerPortImportTable(BaseTable):
device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device')
class Meta(BaseTable.Meta):
model = PowerPort
fields = ('device', 'name', 'description', 'maximum_draw', 'allocated_draw')
empty_text = False
class PowerOutletTemplateTable(BaseTable): class PowerOutletTemplateTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
actions = tables.TemplateColumn( actions = tables.TemplateColumn(
@@ -486,19 +464,10 @@ class PowerOutletTemplateTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = PowerOutletTemplate model = PowerOutletTemplate
fields = ('pk', 'name', 'type', 'power_port', 'feed_leg', 'actions') fields = ('pk', 'name', 'power_port', 'feed_leg', 'actions')
empty_text = "None" empty_text = "None"
class PowerOutletImportTable(BaseTable):
device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device')
class Meta(BaseTable.Meta):
model = PowerOutlet
fields = ('device', 'name', 'description', 'power_port', 'feed_leg')
empty_text = False
class InterfaceTemplateTable(BaseTable): class InterfaceTemplateTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
mgmt_only = tables.TemplateColumn("{% if value %}OOB Management{% endif %}") mgmt_only = tables.TemplateColumn("{% if value %}OOB Management{% endif %}")
@@ -514,16 +483,6 @@ class InterfaceTemplateTable(BaseTable):
empty_text = "None" empty_text = "None"
class InterfaceImportTable(BaseTable):
device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device')
virtual_machine = tables.LinkColumn('virtualization:virtualmachine', args=[Accessor('virtual_machine.pk')], verbose_name='Virtual Machine')
class Meta(BaseTable.Meta):
model = Interface
fields = ('device', 'virtual_machine', 'name', 'description', 'lag', 'type', 'enabled', 'mac_address', 'mtu', 'mgmt_only', 'mode')
empty_text = False
class FrontPortTemplateTable(BaseTable): class FrontPortTemplateTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
rear_port_position = tables.Column( rear_port_position = tables.Column(
@@ -541,15 +500,6 @@ class FrontPortTemplateTable(BaseTable):
empty_text = "None" empty_text = "None"
class FrontPortImportTable(BaseTable):
device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device')
class Meta(BaseTable.Meta):
model = FrontPort
fields = ('device', 'name', 'description', 'type', 'rear_port', 'rear_port_position')
empty_text = False
class RearPortTemplateTable(BaseTable): class RearPortTemplateTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
actions = tables.TemplateColumn( actions = tables.TemplateColumn(
@@ -564,15 +514,6 @@ class RearPortTemplateTable(BaseTable):
empty_text = "None" empty_text = "None"
class RearPortImportTable(BaseTable):
device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device')
class Meta(BaseTable.Meta):
model = RearPort
fields = ('device', 'name', 'description', 'type', 'position')
empty_text = False
class DeviceBayTemplateTable(BaseTable): class DeviceBayTemplateTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
actions = tables.TemplateColumn( actions = tables.TemplateColumn(
@@ -615,7 +556,7 @@ class DeviceRoleTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = DeviceRole model = DeviceRole
fields = ('pk', 'name', 'device_count', 'vm_count', 'color', 'vm_role', 'description', 'slug', 'actions') fields = ('pk', 'name', 'device_count', 'vm_count', 'color', 'vm_role', 'slug', 'actions')
# #
@@ -702,28 +643,11 @@ class DeviceImportTable(BaseTable):
# Device components # Device components
# #
class DeviceComponentDetailTable(BaseTable):
pk = ToggleColumn()
cable = tables.LinkColumn()
class Meta(BaseTable.Meta):
order_by = ('device', 'name')
fields = ('pk', 'device', 'name', 'type', 'description', 'cable')
sequence = ('pk', 'device', 'name', 'type', 'description', 'cable')
class ConsolePortTable(BaseTable): class ConsolePortTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = ConsolePort model = ConsolePort
fields = ('name', 'type') fields = ('name',)
class ConsolePortDetailTable(DeviceComponentDetailTable):
device = tables.LinkColumn()
class Meta(DeviceComponentDetailTable.Meta, ConsolePortTable.Meta):
pass
class ConsoleServerPortTable(BaseTable): class ConsoleServerPortTable(BaseTable):
@@ -733,39 +657,18 @@ class ConsoleServerPortTable(BaseTable):
fields = ('name', 'description') fields = ('name', 'description')
class ConsoleServerPortDetailTable(DeviceComponentDetailTable):
device = tables.LinkColumn()
class Meta(DeviceComponentDetailTable.Meta, ConsoleServerPortTable.Meta):
pass
class PowerPortTable(BaseTable): class PowerPortTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = PowerPort model = PowerPort
fields = ('name', 'type') fields = ('name',)
class PowerPortDetailTable(DeviceComponentDetailTable):
device = tables.LinkColumn()
class Meta(DeviceComponentDetailTable.Meta, PowerPortTable.Meta):
pass
class PowerOutletTable(BaseTable): class PowerOutletTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = PowerOutlet model = PowerOutlet
fields = ('name', 'type', 'description') fields = ('name', 'description')
class PowerOutletDetailTable(DeviceComponentDetailTable):
device = tables.LinkColumn()
class Meta(DeviceComponentDetailTable.Meta, PowerOutletTable.Meta):
pass
class InterfaceTable(BaseTable): class InterfaceTable(BaseTable):
@@ -775,15 +678,6 @@ class InterfaceTable(BaseTable):
fields = ('name', 'type', 'lag', 'enabled', 'mgmt_only', 'description') fields = ('name', 'type', 'lag', 'enabled', 'mgmt_only', 'description')
class InterfaceDetailTable(DeviceComponentDetailTable):
parent = tables.LinkColumn(order_by=('device', 'virtual_machine'))
class Meta(InterfaceTable.Meta):
order_by = ('parent', 'name')
fields = ('pk', 'parent', 'name', 'type', 'description', 'cable')
sequence = ('pk', 'parent', 'name', 'type', 'description', 'cable')
class FrontPortTable(BaseTable): class FrontPortTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
@@ -792,13 +686,6 @@ class FrontPortTable(BaseTable):
empty_text = "None" empty_text = "None"
class FrontPortDetailTable(DeviceComponentDetailTable):
device = tables.LinkColumn()
class Meta(DeviceComponentDetailTable.Meta, FrontPortTable.Meta):
pass
class RearPortTable(BaseTable): class RearPortTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
@@ -807,13 +694,6 @@ class RearPortTable(BaseTable):
empty_text = "None" empty_text = "None"
class RearPortDetailTable(DeviceComponentDetailTable):
device = tables.LinkColumn()
class Meta(DeviceComponentDetailTable.Meta, RearPortTable.Meta):
pass
class DeviceBayTable(BaseTable): class DeviceBayTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
@@ -821,26 +701,6 @@ class DeviceBayTable(BaseTable):
fields = ('name',) fields = ('name',)
class DeviceBayDetailTable(DeviceComponentDetailTable):
device = tables.LinkColumn()
installed_device = tables.LinkColumn()
class Meta(DeviceBayTable.Meta):
fields = ('pk', 'name', 'device', 'installed_device')
sequence = ('pk', 'name', 'device', 'installed_device')
exclude = ('cable',)
class DeviceBayImportTable(BaseTable):
device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device')
installed_device = tables.LinkColumn('dcim:device', args=[Accessor('installed_device.pk')], verbose_name='Installed Device')
class Meta(BaseTable.Meta):
model = DeviceBay
fields = ('device', 'name', 'installed_device', 'description')
empty_text = False
# #
# Cables # Cables
# #
@@ -858,7 +718,7 @@ class CableTable(BaseTable):
orderable=False, orderable=False,
verbose_name='Termination A' verbose_name='Termination A'
) )
termination_a = tables.LinkColumn( termination_a = tables.Column(
accessor=Accessor('termination_a'), accessor=Accessor('termination_a'),
orderable=False, orderable=False,
verbose_name='' verbose_name=''
@@ -869,7 +729,7 @@ class CableTable(BaseTable):
orderable=False, orderable=False,
verbose_name='Termination B' verbose_name='Termination B'
) )
termination_b = tables.LinkColumn( termination_b = tables.Column(
accessor=Accessor('termination_b'), accessor=Accessor('termination_b'),
orderable=False, orderable=False,
verbose_name='' verbose_name=''

View File

@@ -1,10 +1,8 @@
from django.contrib.contenttypes.models import ContentType
from django.urls import reverse from django.urls import reverse
from netaddr import IPNetwork from netaddr import IPNetwork
from rest_framework import status from rest_framework import status
from circuits.models import Circuit, CircuitTermination, CircuitType, Provider from circuits.models import Circuit, CircuitTermination, CircuitType, Provider
from dcim.choices import *
from dcim.constants import * from dcim.constants import *
from dcim.models import ( from dcim.models import (
Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
@@ -13,7 +11,7 @@ from dcim.models import (
Rack, RackGroup, RackReservation, RackRole, RearPort, Region, Site, VirtualChassis, Rack, RackGroup, RackReservation, RackRole, RearPort, Region, Site, VirtualChassis,
) )
from ipam.models import IPAddress, VLAN from ipam.models import IPAddress, VLAN
from extras.models import Graph from extras.models import Graph, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE
from utilities.testing import APITestCase from utilities.testing import APITestCase
from virtualization.models import Cluster, ClusterType from virtualization.models import Cluster, ClusterType
@@ -140,20 +138,16 @@ class SiteTest(APITestCase):
def test_get_site_graphs(self): def test_get_site_graphs(self):
site_ct = ContentType.objects.get_for_model(Site)
self.graph1 = Graph.objects.create( self.graph1 = Graph.objects.create(
type=site_ct, type=GRAPH_TYPE_SITE, name='Test Graph 1',
name='Test Graph 1',
source='http://example.com/graphs.py?site={{ obj.slug }}&foo=1' source='http://example.com/graphs.py?site={{ obj.slug }}&foo=1'
) )
self.graph2 = Graph.objects.create( self.graph2 = Graph.objects.create(
type=site_ct, type=GRAPH_TYPE_SITE, name='Test Graph 2',
name='Test Graph 2',
source='http://example.com/graphs.py?site={{ obj.slug }}&foo=2' source='http://example.com/graphs.py?site={{ obj.slug }}&foo=2'
) )
self.graph3 = Graph.objects.create( self.graph3 = Graph.objects.create(
type=site_ct, type=GRAPH_TYPE_SITE, name='Test Graph 3',
name='Test Graph 3',
source='http://example.com/graphs.py?site={{ obj.slug }}&foo=3' source='http://example.com/graphs.py?site={{ obj.slug }}&foo=3'
) )
@@ -186,7 +180,7 @@ class SiteTest(APITestCase):
'name': 'Test Site 4', 'name': 'Test Site 4',
'slug': 'test-site-4', 'slug': 'test-site-4',
'region': self.region1.pk, 'region': self.region1.pk,
'status': SiteStatusChoices.STATUS_ACTIVE, 'status': SITE_STATUS_ACTIVE,
} }
url = reverse('dcim-api:site-list') url = reverse('dcim-api:site-list')
@@ -206,19 +200,19 @@ class SiteTest(APITestCase):
'name': 'Test Site 4', 'name': 'Test Site 4',
'slug': 'test-site-4', 'slug': 'test-site-4',
'region': self.region1.pk, 'region': self.region1.pk,
'status': SiteStatusChoices.STATUS_ACTIVE, 'status': SITE_STATUS_ACTIVE,
}, },
{ {
'name': 'Test Site 5', 'name': 'Test Site 5',
'slug': 'test-site-5', 'slug': 'test-site-5',
'region': self.region1.pk, 'region': self.region1.pk,
'status': SiteStatusChoices.STATUS_ACTIVE, 'status': SITE_STATUS_ACTIVE,
}, },
{ {
'name': 'Test Site 6', 'name': 'Test Site 6',
'slug': 'test-site-6', 'slug': 'test-site-6',
'region': self.region1.pk, 'region': self.region1.pk,
'status': SiteStatusChoices.STATUS_ACTIVE, 'status': SITE_STATUS_ACTIVE,
}, },
] ]
@@ -2422,20 +2416,16 @@ class InterfaceTest(APITestCase):
def test_get_interface_graphs(self): def test_get_interface_graphs(self):
interface_ct = ContentType.objects.get_for_model(Interface)
self.graph1 = Graph.objects.create( self.graph1 = Graph.objects.create(
type=interface_ct, type=GRAPH_TYPE_INTERFACE, name='Test Graph 1',
name='Test Graph 1',
source='http://example.com/graphs.py?interface={{ obj.name }}&foo=1' source='http://example.com/graphs.py?interface={{ obj.name }}&foo=1'
) )
self.graph2 = Graph.objects.create( self.graph2 = Graph.objects.create(
type=interface_ct, type=GRAPH_TYPE_INTERFACE, name='Test Graph 2',
name='Test Graph 2',
source='http://example.com/graphs.py?interface={{ obj.name }}&foo=2' source='http://example.com/graphs.py?interface={{ obj.name }}&foo=2'
) )
self.graph3 = Graph.objects.create( self.graph3 = Graph.objects.create(
type=interface_ct, type=GRAPH_TYPE_INTERFACE, name='Test Graph 3',
name='Test Graph 3',
source='http://example.com/graphs.py?interface={{ obj.name }}&foo=3' source='http://example.com/graphs.py?interface={{ obj.name }}&foo=3'
) )
@@ -2483,7 +2473,7 @@ class InterfaceTest(APITestCase):
data = { data = {
'device': self.device.pk, 'device': self.device.pk,
'name': 'Test Interface 4', 'name': 'Test Interface 4',
'mode': InterfaceModeChoices.MODE_TAGGED, 'mode': IFACE_MODE_TAGGED,
'untagged_vlan': self.vlan3.id, 'untagged_vlan': self.vlan3.id,
'tagged_vlans': [self.vlan1.id, self.vlan2.id], 'tagged_vlans': [self.vlan1.id, self.vlan2.id],
} }
@@ -2530,21 +2520,21 @@ class InterfaceTest(APITestCase):
{ {
'device': self.device.pk, 'device': self.device.pk,
'name': 'Test Interface 4', 'name': 'Test Interface 4',
'mode': InterfaceModeChoices.MODE_TAGGED, 'mode': IFACE_MODE_TAGGED,
'untagged_vlan': self.vlan2.id, 'untagged_vlan': self.vlan2.id,
'tagged_vlans': [self.vlan1.id], 'tagged_vlans': [self.vlan1.id],
}, },
{ {
'device': self.device.pk, 'device': self.device.pk,
'name': 'Test Interface 5', 'name': 'Test Interface 5',
'mode': InterfaceModeChoices.MODE_TAGGED, 'mode': IFACE_MODE_TAGGED,
'untagged_vlan': self.vlan2.id, 'untagged_vlan': self.vlan2.id,
'tagged_vlans': [self.vlan1.id], 'tagged_vlans': [self.vlan1.id],
}, },
{ {
'device': self.device.pk, 'device': self.device.pk,
'name': 'Test Interface 6', 'name': 'Test Interface 6',
'mode': InterfaceModeChoices.MODE_TAGGED, 'mode': IFACE_MODE_TAGGED,
'untagged_vlan': self.vlan2.id, 'untagged_vlan': self.vlan2.id,
'tagged_vlans': [self.vlan1.id], 'tagged_vlans': [self.vlan1.id],
}, },
@@ -2563,7 +2553,7 @@ class InterfaceTest(APITestCase):
def test_update_interface(self): def test_update_interface(self):
lag_interface = Interface.objects.create( lag_interface = Interface.objects.create(
device=self.device, name='Test LAG Interface', type=InterfaceTypeChoices.TYPE_LAG device=self.device, name='Test LAG Interface', type=IFACE_TYPE_LAG
) )
data = { data = {
@@ -2600,11 +2590,11 @@ class DeviceBayTest(APITestCase):
manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1') manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
self.devicetype1 = DeviceType.objects.create( self.devicetype1 = DeviceType.objects.create(
manufacturer=manufacturer, model='Parent Device Type', slug='parent-device-type', manufacturer=manufacturer, model='Parent Device Type', slug='parent-device-type',
subdevice_role=SubdeviceRoleChoices.ROLE_PARENT subdevice_role=SUBDEVICE_ROLE_PARENT
) )
self.devicetype2 = DeviceType.objects.create( self.devicetype2 = DeviceType.objects.create(
manufacturer=manufacturer, model='Child Device Type', slug='child-device-type', manufacturer=manufacturer, model='Child Device Type', slug='child-device-type',
subdevice_role=SubdeviceRoleChoices.ROLE_CHILD subdevice_role=SUBDEVICE_ROLE_CHILD
) )
devicerole = DeviceRole.objects.create( devicerole = DeviceRole.objects.create(
name='Test Device Role 1', slug='test-device-role-1', color='ff0000' name='Test Device Role 1', slug='test-device-role-1', color='ff0000'
@@ -2851,7 +2841,7 @@ class CableTest(APITestCase):
) )
for device in [self.device1, self.device2]: for device in [self.device1, self.device2]:
for i in range(0, 10): for i in range(0, 10):
Interface(device=device, type=InterfaceTypeChoices.TYPE_1GE_FIXED, name='eth{}'.format(i)).save() Interface(device=device, type=IFACE_TYPE_1GE_FIXED, name='eth{}'.format(i)).save()
self.cable1 = Cable( self.cable1 = Cable(
termination_a=self.device1.interfaces.get(name='eth0'), termination_a=self.device1.interfaces.get(name='eth0'),
@@ -2895,7 +2885,7 @@ class CableTest(APITestCase):
'termination_a_id': interface_a.pk, 'termination_a_id': interface_a.pk,
'termination_b_type': 'dcim.interface', 'termination_b_type': 'dcim.interface',
'termination_b_id': interface_b.pk, 'termination_b_id': interface_b.pk,
'status': CableStatusChoices.STATUS_PLANNED, 'status': CONNECTION_STATUS_PLANNED,
'label': 'Test Cable 4', 'label': 'Test Cable 4',
} }
@@ -2949,7 +2939,7 @@ class CableTest(APITestCase):
data = { data = {
'label': 'Test Cable X', 'label': 'Test Cable X',
'status': CableStatusChoices.STATUS_CONNECTED, 'status': CONNECTION_STATUS_CONNECTED,
} }
url = reverse('dcim-api:cable-detail', kwargs={'pk': self.cable1.pk}) url = reverse('dcim-api:cable-detail', kwargs={'pk': self.cable1.pk})
@@ -3043,16 +3033,16 @@ class ConnectionTest(APITestCase):
device=self.device2, name='Test Console Server Port 1' device=self.device2, name='Test Console Server Port 1'
) )
rearport1 = RearPort.objects.create( rearport1 = RearPort.objects.create(
device=self.panel1, name='Test Rear Port 1', type=PortTypeChoices.TYPE_8P8C device=self.panel1, name='Test Rear Port 1', type=PORT_TYPE_8P8C
) )
frontport1 = FrontPort.objects.create( frontport1 = FrontPort.objects.create(
device=self.panel1, name='Test Front Port 1', type=PortTypeChoices.TYPE_8P8C, rear_port=rearport1 device=self.panel1, name='Test Front Port 1', type=PORT_TYPE_8P8C, rear_port=rearport1
) )
rearport2 = RearPort.objects.create( rearport2 = RearPort.objects.create(
device=self.panel2, name='Test Rear Port 2', type=PortTypeChoices.TYPE_8P8C device=self.panel2, name='Test Rear Port 2', type=PORT_TYPE_8P8C
) )
frontport2 = FrontPort.objects.create( frontport2 = FrontPort.objects.create(
device=self.panel2, name='Test Front Port 2', type=PortTypeChoices.TYPE_8P8C, rear_port=rearport2 device=self.panel2, name='Test Front Port 2', type=PORT_TYPE_8P8C, rear_port=rearport2
) )
url = reverse('dcim-api:cable-list') url = reverse('dcim-api:cable-list')
@@ -3171,16 +3161,16 @@ class ConnectionTest(APITestCase):
device=self.device2, name='Test Interface 2' device=self.device2, name='Test Interface 2'
) )
rearport1 = RearPort.objects.create( rearport1 = RearPort.objects.create(
device=self.panel1, name='Test Rear Port 1', type=PortTypeChoices.TYPE_8P8C device=self.panel1, name='Test Rear Port 1', type=PORT_TYPE_8P8C
) )
frontport1 = FrontPort.objects.create( frontport1 = FrontPort.objects.create(
device=self.panel1, name='Test Front Port 1', type=PortTypeChoices.TYPE_8P8C, rear_port=rearport1 device=self.panel1, name='Test Front Port 1', type=PORT_TYPE_8P8C, rear_port=rearport1
) )
rearport2 = RearPort.objects.create( rearport2 = RearPort.objects.create(
device=self.panel2, name='Test Rear Port 2', type=PortTypeChoices.TYPE_8P8C device=self.panel2, name='Test Rear Port 2', type=PORT_TYPE_8P8C
) )
frontport2 = FrontPort.objects.create( frontport2 = FrontPort.objects.create(
device=self.panel2, name='Test Front Port 2', type=PortTypeChoices.TYPE_8P8C, rear_port=rearport2 device=self.panel2, name='Test Front Port 2', type=PORT_TYPE_8P8C, rear_port=rearport2
) )
url = reverse('dcim-api:cable-list') url = reverse('dcim-api:cable-list')
@@ -3282,16 +3272,16 @@ class ConnectionTest(APITestCase):
circuit=circuit, term_side='A', site=self.site, port_speed=10000 circuit=circuit, term_side='A', site=self.site, port_speed=10000
) )
rearport1 = RearPort.objects.create( rearport1 = RearPort.objects.create(
device=self.panel1, name='Test Rear Port 1', type=PortTypeChoices.TYPE_8P8C device=self.panel1, name='Test Rear Port 1', type=PORT_TYPE_8P8C
) )
frontport1 = FrontPort.objects.create( frontport1 = FrontPort.objects.create(
device=self.panel1, name='Test Front Port 1', type=PortTypeChoices.TYPE_8P8C, rear_port=rearport1 device=self.panel1, name='Test Front Port 1', type=PORT_TYPE_8P8C, rear_port=rearport1
) )
rearport2 = RearPort.objects.create( rearport2 = RearPort.objects.create(
device=self.panel2, name='Test Rear Port 2', type=PortTypeChoices.TYPE_8P8C device=self.panel2, name='Test Rear Port 2', type=PORT_TYPE_8P8C
) )
frontport2 = FrontPort.objects.create( frontport2 = FrontPort.objects.create(
device=self.panel2, name='Test Front Port 2', type=PortTypeChoices.TYPE_8P8C, rear_port=rearport2 device=self.panel2, name='Test Front Port 2', type=PORT_TYPE_8P8C, rear_port=rearport2
) )
url = reverse('dcim-api:cable-list') url = reverse('dcim-api:cable-list')
@@ -3420,23 +3410,23 @@ class VirtualChassisTest(APITestCase):
device_type=device_type, device_role=device_role, name='StackSwitch9', site=site device_type=device_type, device_role=device_role, name='StackSwitch9', site=site
) )
for i in range(0, 13): for i in range(0, 13):
Interface.objects.create(device=self.device1, name='1/{}'.format(i), type=InterfaceTypeChoices.TYPE_1GE_FIXED) Interface.objects.create(device=self.device1, name='1/{}'.format(i), type=IFACE_TYPE_1GE_FIXED)
for i in range(0, 13): for i in range(0, 13):
Interface.objects.create(device=self.device2, name='2/{}'.format(i), type=InterfaceTypeChoices.TYPE_1GE_FIXED) Interface.objects.create(device=self.device2, name='2/{}'.format(i), type=IFACE_TYPE_1GE_FIXED)
for i in range(0, 13): for i in range(0, 13):
Interface.objects.create(device=self.device3, name='3/{}'.format(i), type=InterfaceTypeChoices.TYPE_1GE_FIXED) Interface.objects.create(device=self.device3, name='3/{}'.format(i), type=IFACE_TYPE_1GE_FIXED)
for i in range(0, 13): for i in range(0, 13):
Interface.objects.create(device=self.device4, name='1/{}'.format(i), type=InterfaceTypeChoices.TYPE_1GE_FIXED) Interface.objects.create(device=self.device4, name='1/{}'.format(i), type=IFACE_TYPE_1GE_FIXED)
for i in range(0, 13): for i in range(0, 13):
Interface.objects.create(device=self.device5, name='2/{}'.format(i), type=InterfaceTypeChoices.TYPE_1GE_FIXED) Interface.objects.create(device=self.device5, name='2/{}'.format(i), type=IFACE_TYPE_1GE_FIXED)
for i in range(0, 13): for i in range(0, 13):
Interface.objects.create(device=self.device6, name='3/{}'.format(i), type=InterfaceTypeChoices.TYPE_1GE_FIXED) Interface.objects.create(device=self.device6, name='3/{}'.format(i), type=IFACE_TYPE_1GE_FIXED)
for i in range(0, 13): for i in range(0, 13):
Interface.objects.create(device=self.device7, name='1/{}'.format(i), type=InterfaceTypeChoices.TYPE_1GE_FIXED) Interface.objects.create(device=self.device7, name='1/{}'.format(i), type=IFACE_TYPE_1GE_FIXED)
for i in range(0, 13): for i in range(0, 13):
Interface.objects.create(device=self.device8, name='2/{}'.format(i), type=InterfaceTypeChoices.TYPE_1GE_FIXED) Interface.objects.create(device=self.device8, name='2/{}'.format(i), type=IFACE_TYPE_1GE_FIXED)
for i in range(0, 13): for i in range(0, 13):
Interface.objects.create(device=self.device9, name='3/{}'.format(i), type=InterfaceTypeChoices.TYPE_1GE_FIXED) Interface.objects.create(device=self.device9, name='3/{}'.format(i), type=IFACE_TYPE_1GE_FIXED)
# Create two VirtualChassis with three members each # Create two VirtualChassis with three members each
self.vc1 = VirtualChassis.objects.create(master=self.device1, domain='test-domain-1') self.vc1 = VirtualChassis.objects.create(master=self.device1, domain='test-domain-1')
@@ -3688,22 +3678,22 @@ class PowerFeedTest(APITestCase):
site=self.site1, rack_group=self.rackgroup1, name='Test Power Panel 2' site=self.site1, rack_group=self.rackgroup1, name='Test Power Panel 2'
) )
self.powerfeed1 = PowerFeed.objects.create( self.powerfeed1 = PowerFeed.objects.create(
power_panel=self.powerpanel1, rack=self.rack1, name='Test Power Feed 1A', type=PowerFeedTypeChoices.TYPE_PRIMARY power_panel=self.powerpanel1, rack=self.rack1, name='Test Power Feed 1A', type=POWERFEED_TYPE_PRIMARY
) )
self.powerfeed2 = PowerFeed.objects.create( self.powerfeed2 = PowerFeed.objects.create(
power_panel=self.powerpanel2, rack=self.rack1, name='Test Power Feed 1B', type=PowerFeedTypeChoices.TYPE_REDUNDANT power_panel=self.powerpanel2, rack=self.rack1, name='Test Power Feed 1B', type=POWERFEED_TYPE_REDUNDANT
) )
self.powerfeed3 = PowerFeed.objects.create( self.powerfeed3 = PowerFeed.objects.create(
power_panel=self.powerpanel1, rack=self.rack2, name='Test Power Feed 2A', type=PowerFeedTypeChoices.TYPE_PRIMARY power_panel=self.powerpanel1, rack=self.rack2, name='Test Power Feed 2A', type=POWERFEED_TYPE_PRIMARY
) )
self.powerfeed4 = PowerFeed.objects.create( self.powerfeed4 = PowerFeed.objects.create(
power_panel=self.powerpanel2, rack=self.rack2, name='Test Power Feed 2B', type=PowerFeedTypeChoices.TYPE_REDUNDANT power_panel=self.powerpanel2, rack=self.rack2, name='Test Power Feed 2B', type=POWERFEED_TYPE_REDUNDANT
) )
self.powerfeed5 = PowerFeed.objects.create( self.powerfeed5 = PowerFeed.objects.create(
power_panel=self.powerpanel1, rack=self.rack3, name='Test Power Feed 3A', type=PowerFeedTypeChoices.TYPE_PRIMARY power_panel=self.powerpanel1, rack=self.rack3, name='Test Power Feed 3A', type=POWERFEED_TYPE_PRIMARY
) )
self.powerfeed6 = PowerFeed.objects.create( self.powerfeed6 = PowerFeed.objects.create(
power_panel=self.powerpanel2, rack=self.rack3, name='Test Power Feed 3B', type=PowerFeedTypeChoices.TYPE_REDUNDANT power_panel=self.powerpanel2, rack=self.rack3, name='Test Power Feed 3B', type=POWERFEED_TYPE_REDUNDANT
) )
def test_get_powerfeed(self): def test_get_powerfeed(self):
@@ -3736,7 +3726,7 @@ class PowerFeedTest(APITestCase):
'name': 'Test Power Feed 4A', 'name': 'Test Power Feed 4A',
'power_panel': self.powerpanel1.pk, 'power_panel': self.powerpanel1.pk,
'rack': self.rack4.pk, 'rack': self.rack4.pk,
'type': PowerFeedTypeChoices.TYPE_PRIMARY, 'type': POWERFEED_TYPE_PRIMARY,
} }
url = reverse('dcim-api:powerfeed-list') url = reverse('dcim-api:powerfeed-list')
@@ -3756,13 +3746,13 @@ class PowerFeedTest(APITestCase):
'name': 'Test Power Feed 4A', 'name': 'Test Power Feed 4A',
'power_panel': self.powerpanel1.pk, 'power_panel': self.powerpanel1.pk,
'rack': self.rack4.pk, 'rack': self.rack4.pk,
'type': PowerFeedTypeChoices.TYPE_PRIMARY, 'type': POWERFEED_TYPE_PRIMARY,
}, },
{ {
'name': 'Test Power Feed 4B', 'name': 'Test Power Feed 4B',
'power_panel': self.powerpanel1.pk, 'power_panel': self.powerpanel1.pk,
'rack': self.rack4.pk, 'rack': self.rack4.pk,
'type': PowerFeedTypeChoices.TYPE_REDUNDANT, 'type': POWERFEED_TYPE_REDUNDANT,
}, },
] ]
@@ -3779,7 +3769,7 @@ class PowerFeedTest(APITestCase):
data = { data = {
'name': 'Test Power Feed X', 'name': 'Test Power Feed X',
'rack': self.rack4.pk, 'rack': self.rack4.pk,
'type': PowerFeedTypeChoices.TYPE_REDUNDANT, 'type': POWERFEED_TYPE_REDUNDANT,
} }
url = reverse('dcim-api:powerfeed-detail', kwargs={'pk': self.powerfeed1.pk}) url = reverse('dcim-api:powerfeed-detail', kwargs={'pk': self.powerfeed1.pk})

View File

@@ -21,10 +21,10 @@ class DeviceTestCase(TestCase):
'device_type': get_id(DeviceType, 'qfx5100-48s'), 'device_type': get_id(DeviceType, 'qfx5100-48s'),
'site': get_id(Site, 'test1'), 'site': get_id(Site, 'test1'),
'rack': '1', 'rack': '1',
'face': DeviceFaceChoices.FACE_FRONT, 'face': RACK_FACE_FRONT,
'position': 41, 'position': 41,
'platform': get_id(Platform, 'juniper-junos'), 'platform': get_id(Platform, 'juniper-junos'),
'status': DeviceStatusChoices.STATUS_ACTIVE, 'status': DEVICE_STATUS_ACTIVE,
}) })
self.assertTrue(test.is_valid(), test.fields['position'].choices) self.assertTrue(test.is_valid(), test.fields['position'].choices)
self.assertTrue(test.save()) self.assertTrue(test.save())
@@ -38,10 +38,10 @@ class DeviceTestCase(TestCase):
'device_type': get_id(DeviceType, 'qfx5100-48s'), 'device_type': get_id(DeviceType, 'qfx5100-48s'),
'site': get_id(Site, 'test1'), 'site': get_id(Site, 'test1'),
'rack': '1', 'rack': '1',
'face': DeviceFaceChoices.FACE_FRONT, 'face': RACK_FACE_FRONT,
'position': 1, 'position': 1,
'platform': get_id(Platform, 'juniper-junos'), 'platform': get_id(Platform, 'juniper-junos'),
'status': DeviceStatusChoices.STATUS_ACTIVE, 'status': DEVICE_STATUS_ACTIVE,
}) })
self.assertFalse(test.is_valid()) self.assertFalse(test.is_valid())
@@ -54,10 +54,10 @@ class DeviceTestCase(TestCase):
'device_type': get_id(DeviceType, 'cwg-24vym415c9'), 'device_type': get_id(DeviceType, 'cwg-24vym415c9'),
'site': get_id(Site, 'test1'), 'site': get_id(Site, 'test1'),
'rack': '1', 'rack': '1',
'face': '', 'face': None,
'position': None, 'position': None,
'platform': None, 'platform': None,
'status': DeviceStatusChoices.STATUS_ACTIVE, 'status': DEVICE_STATUS_ACTIVE,
}) })
self.assertTrue(test.is_valid()) self.assertTrue(test.is_valid())
self.assertTrue(test.save()) self.assertTrue(test.save())
@@ -71,10 +71,10 @@ class DeviceTestCase(TestCase):
'device_type': get_id(DeviceType, 'cwg-24vym415c9'), 'device_type': get_id(DeviceType, 'cwg-24vym415c9'),
'site': get_id(Site, 'test1'), 'site': get_id(Site, 'test1'),
'rack': '1', 'rack': '1',
'face': DeviceFaceChoices.FACE_REAR, 'face': RACK_FACE_REAR,
'position': None, 'position': None,
'platform': None, 'platform': None,
'status': DeviceStatusChoices.STATUS_ACTIVE, 'status': DEVICE_STATUS_ACTIVE,
}) })
self.assertTrue(test.is_valid()) self.assertTrue(test.is_valid())
self.assertTrue(test.save()) self.assertTrue(test.save())

View File

@@ -1,7 +1,6 @@
from django.test import TestCase from django.test import TestCase
from dcim.models import * from dcim.models import *
from tenancy.models import Tenant
class RackTestCase(TestCase): class RackTestCase(TestCase):
@@ -88,7 +87,7 @@ class RackTestCase(TestCase):
site=self.site1, site=self.site1,
rack=rack1, rack=rack1,
position=43, position=43,
face=DeviceFaceChoices.FACE_FRONT, face=RACK_FACE_FRONT,
) )
device1.save() device1.save()
@@ -118,7 +117,7 @@ class RackTestCase(TestCase):
site=self.site1, site=self.site1,
rack=self.rack, rack=self.rack,
position=10, position=10,
face=DeviceFaceChoices.FACE_REAR, face=RACK_FACE_REAR,
) )
device1.save() device1.save()
@@ -126,14 +125,14 @@ class RackTestCase(TestCase):
self.assertEqual(list(self.rack.units), list(reversed(range(1, 43)))) self.assertEqual(list(self.rack.units), list(reversed(range(1, 43))))
# Validate inventory (front face) # Validate inventory (front face)
rack1_inventory_front = self.rack.get_rack_units(face=DeviceFaceChoices.FACE_FRONT) rack1_inventory_front = self.rack.get_front_elevation()
self.assertEqual(rack1_inventory_front[-10]['device'], device1) self.assertEqual(rack1_inventory_front[-10]['device'], device1)
del(rack1_inventory_front[-10]) del(rack1_inventory_front[-10])
for u in rack1_inventory_front: for u in rack1_inventory_front:
self.assertIsNone(u['device']) self.assertIsNone(u['device'])
# Validate inventory (rear face) # Validate inventory (rear face)
rack1_inventory_rear = self.rack.get_rack_units(face=DeviceFaceChoices.FACE_REAR) rack1_inventory_rear = self.rack.get_rear_elevation()
self.assertEqual(rack1_inventory_rear[-10]['device'], device1) self.assertEqual(rack1_inventory_rear[-10]['device'], device1)
del(rack1_inventory_rear[-10]) del(rack1_inventory_rear[-10])
for u in rack1_inventory_rear: for u in rack1_inventory_rear:
@@ -147,7 +146,7 @@ class RackTestCase(TestCase):
site=self.site1, site=self.site1,
rack=self.rack, rack=self.rack,
position=None, position=None,
face='', face=None,
) )
self.assertTrue(pdu) self.assertTrue(pdu)
@@ -188,20 +187,20 @@ class DeviceTestCase(TestCase):
device_type=self.device_type, device_type=self.device_type,
name='Power Outlet 1', name='Power Outlet 1',
power_port=ppt, power_port=ppt,
feed_leg=PowerOutletFeedLegChoices.FEED_LEG_A feed_leg=POWERFEED_LEG_A
).save() ).save()
InterfaceTemplate( InterfaceTemplate(
device_type=self.device_type, device_type=self.device_type,
name='Interface 1', name='Interface 1',
type=InterfaceTypeChoices.TYPE_1GE_FIXED, type=IFACE_TYPE_1GE_FIXED,
mgmt_only=True mgmt_only=True
).save() ).save()
rpt = RearPortTemplate( rpt = RearPortTemplate(
device_type=self.device_type, device_type=self.device_type,
name='Rear Port 1', name='Rear Port 1',
type=PortTypeChoices.TYPE_8P8C, type=PORT_TYPE_8P8C,
positions=8 positions=8
) )
rpt.save() rpt.save()
@@ -209,7 +208,7 @@ class DeviceTestCase(TestCase):
FrontPortTemplate( FrontPortTemplate(
device_type=self.device_type, device_type=self.device_type,
name='Front Port 1', name='Front Port 1',
type=PortTypeChoices.TYPE_8P8C, type=PORT_TYPE_8P8C,
rear_port=rpt, rear_port=rpt,
rear_port_position=2 rear_port_position=2
).save() ).save()
@@ -252,27 +251,27 @@ class DeviceTestCase(TestCase):
device=d, device=d,
name='Power Outlet 1', name='Power Outlet 1',
power_port=pp, power_port=pp,
feed_leg=PowerOutletFeedLegChoices.FEED_LEG_A feed_leg=POWERFEED_LEG_A
) )
Interface.objects.get( Interface.objects.get(
device=d, device=d,
name='Interface 1', name='Interface 1',
type=InterfaceTypeChoices.TYPE_1GE_FIXED, type=IFACE_TYPE_1GE_FIXED,
mgmt_only=True mgmt_only=True
) )
rp = RearPort.objects.get( rp = RearPort.objects.get(
device=d, device=d,
name='Rear Port 1', name='Rear Port 1',
type=PortTypeChoices.TYPE_8P8C, type=PORT_TYPE_8P8C,
positions=8 positions=8
) )
FrontPort.objects.get( FrontPort.objects.get(
device=d, device=d,
name='Front Port 1', name='Front Port 1',
type=PortTypeChoices.TYPE_8P8C, type=PORT_TYPE_8P8C,
rear_port=rp, rear_port=rp,
rear_port_position=2 rear_port_position=2
) )
@@ -282,42 +281,6 @@ class DeviceTestCase(TestCase):
name='Device Bay 1' name='Device Bay 1'
) )
def test_device_duplicate_name_per_site(self):
device1 = Device(
site=self.site,
device_type=self.device_type,
device_role=self.device_role,
name='Test Device 1'
)
device1.save()
device2 = Device(
site=device1.site,
device_type=device1.device_type,
device_role=device1.device_role,
name=device1.name
)
# Two devices assigned to the same Site and no Tenant should fail validation
with self.assertRaises(ValidationError):
device2.full_clean()
tenant = Tenant.objects.create(name='Test Tenant 1', slug='test-tenant-1')
device1.tenant = tenant
device1.save()
device2.tenant = tenant
# Two devices assigned to the same Site and the same Tenant should fail validation
with self.assertRaises(ValidationError):
device2.full_clean()
device2.tenant = None
# Two devices assigned to the same Site and different Tenants should pass validation
device2.full_clean()
device2.save()
class CableTestCase(TestCase): class CableTestCase(TestCase):
@@ -416,7 +379,7 @@ class CableTestCase(TestCase):
""" """
A cable cannot terminate to a virtual interface A cable cannot terminate to a virtual interface
""" """
virtual_interface = Interface(device=self.device1, name="V1", type=InterfaceTypeChoices.TYPE_VIRTUAL) virtual_interface = Interface(device=self.device1, name="V1", type=IFACE_TYPE_VIRTUAL)
cable = Cable(termination_a=self.interface2, termination_b=virtual_interface) cable = Cable(termination_a=self.interface2, termination_b=virtual_interface)
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
cable.clean() cable.clean()
@@ -425,7 +388,7 @@ class CableTestCase(TestCase):
""" """
A cable cannot terminate to a wireless interface A cable cannot terminate to a wireless interface
""" """
wireless_interface = Interface(device=self.device1, name="W1", type=InterfaceTypeChoices.TYPE_80211A) wireless_interface = Interface(device=self.device1, name="W1", type=IFACE_TYPE_80211A)
cable = Cable(termination_a=self.interface2, termination_b=wireless_interface) cable = Cable(termination_a=self.interface2, termination_b=wireless_interface)
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
cable.clean() cable.clean()
@@ -458,16 +421,16 @@ class CablePathTestCase(TestCase):
device_type=devicetype, device_role=devicerole, name='Test Panel 2', site=site device_type=devicetype, device_role=devicerole, name='Test Panel 2', site=site
) )
self.rear_port1 = RearPort.objects.create( self.rear_port1 = RearPort.objects.create(
device=self.panel1, name='Rear Port 1', type=PortTypeChoices.TYPE_8P8C device=self.panel1, name='Rear Port 1', type=PORT_TYPE_8P8C
) )
self.front_port1 = FrontPort.objects.create( self.front_port1 = FrontPort.objects.create(
device=self.panel1, name='Front Port 1', type=PortTypeChoices.TYPE_8P8C, rear_port=self.rear_port1 device=self.panel1, name='Front Port 1', type=PORT_TYPE_8P8C, rear_port=self.rear_port1
) )
self.rear_port2 = RearPort.objects.create( self.rear_port2 = RearPort.objects.create(
device=self.panel2, name='Rear Port 2', type=PortTypeChoices.TYPE_8P8C device=self.panel2, name='Rear Port 2', type=PORT_TYPE_8P8C
) )
self.front_port2 = FrontPort.objects.create( self.front_port2 = FrontPort.objects.create(
device=self.panel2, name='Front Port 2', type=PortTypeChoices.TYPE_8P8C, rear_port=self.rear_port2 device=self.panel2, name='Front Port 2', type=PORT_TYPE_8P8C, rear_port=self.rear_port2
) )
def test_path_completion(self): def test_path_completion(self):
@@ -487,11 +450,7 @@ class CablePathTestCase(TestCase):
self.assertIsNone(interface1.connection_status) self.assertIsNone(interface1.connection_status)
# Third segment # Third segment
cable3 = Cable( cable3 = Cable(termination_a=self.front_port2, termination_b=self.interface2, status=CONNECTION_STATUS_PLANNED)
termination_a=self.front_port2,
termination_b=self.interface2,
status=CableStatusChoices.STATUS_PLANNED
)
cable3.save() cable3.save()
interface1 = Interface.objects.get(pk=self.interface1.pk) interface1 = Interface.objects.get(pk=self.interface1.pk)
self.assertEqual(interface1.connected_endpoint, self.interface2) self.assertEqual(interface1.connected_endpoint, self.interface2)

File diff suppressed because it is too large Load Diff

View File

@@ -82,7 +82,7 @@ urlpatterns = [
# Device types # Device types
path(r'device-types/', views.DeviceTypeListView.as_view(), name='devicetype_list'), 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/add/', views.DeviceTypeCreateView.as_view(), name='devicetype_add'),
path(r'device-types/import/', views.DeviceTypeImportView.as_view(), name='devicetype_import'), path(r'device-types/import/', views.DeviceTypeBulkImportView.as_view(), name='devicetype_import'),
path(r'device-types/edit/', views.DeviceTypeBulkEditView.as_view(), name='devicetype_bulk_edit'), 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/delete/', views.DeviceTypeBulkDeleteView.as_view(), name='devicetype_bulk_delete'),
path(r'device-types/<int:pk>/', views.DeviceTypeView.as_view(), name='devicetype'), path(r'device-types/<int:pk>/', views.DeviceTypeView.as_view(), name='devicetype'),
@@ -171,58 +171,49 @@ urlpatterns = [
path(r'devices/console-ports/add/', views.DeviceBulkAddConsolePortView.as_view(), name='device_bulk_add_consoleport'), path(r'devices/console-ports/add/', views.DeviceBulkAddConsolePortView.as_view(), name='device_bulk_add_consoleport'),
path(r'devices/<int:pk>/console-ports/add/', views.ConsolePortCreateView.as_view(), name='consoleport_add'), path(r'devices/<int:pk>/console-ports/add/', views.ConsolePortCreateView.as_view(), name='consoleport_add'),
path(r'devices/<int:pk>/console-ports/delete/', views.ConsolePortBulkDeleteView.as_view(), name='consoleport_bulk_delete'), path(r'devices/<int:pk>/console-ports/delete/', views.ConsolePortBulkDeleteView.as_view(), name='consoleport_bulk_delete'),
path(r'console-ports/', views.ConsolePortListView.as_view(), name='consoleport_list'),
path(r'console-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='consoleport_connect', kwargs={'termination_a_type': ConsolePort}), path(r'console-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='consoleport_connect', kwargs={'termination_a_type': ConsolePort}),
path(r'console-ports/<int:pk>/edit/', views.ConsolePortEditView.as_view(), name='consoleport_edit'), path(r'console-ports/<int:pk>/edit/', views.ConsolePortEditView.as_view(), name='consoleport_edit'),
path(r'console-ports/<int:pk>/delete/', views.ConsolePortDeleteView.as_view(), name='consoleport_delete'), path(r'console-ports/<int:pk>/delete/', views.ConsolePortDeleteView.as_view(), name='consoleport_delete'),
path(r'console-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='consoleport_trace', kwargs={'model': ConsolePort}), path(r'console-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='consoleport_trace', kwargs={'model': ConsolePort}),
path(r'console-ports/import/', views.ConsolePortBulkImportView.as_view(), name='consoleport_import'),
# Console server ports # Console server ports
path(r'devices/console-server-ports/add/', views.DeviceBulkAddConsoleServerPortView.as_view(), name='device_bulk_add_consoleserverport'), path(r'devices/console-server-ports/add/', views.DeviceBulkAddConsoleServerPortView.as_view(), name='device_bulk_add_consoleserverport'),
path(r'devices/<int:pk>/console-server-ports/add/', views.ConsoleServerPortCreateView.as_view(), name='consoleserverport_add'), path(r'devices/<int:pk>/console-server-ports/add/', views.ConsoleServerPortCreateView.as_view(), name='consoleserverport_add'),
path(r'devices/<int:pk>/console-server-ports/edit/', views.ConsoleServerPortBulkEditView.as_view(), name='consoleserverport_bulk_edit'), path(r'devices/<int:pk>/console-server-ports/edit/', views.ConsoleServerPortBulkEditView.as_view(), name='consoleserverport_bulk_edit'),
path(r'devices/<int:pk>/console-server-ports/delete/', views.ConsoleServerPortBulkDeleteView.as_view(), name='consoleserverport_bulk_delete'), path(r'devices/<int:pk>/console-server-ports/delete/', views.ConsoleServerPortBulkDeleteView.as_view(), name='consoleserverport_bulk_delete'),
path(r'console-server-ports/', views.ConsoleServerPortListView.as_view(), name='consoleserverport_list'),
path(r'console-server-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='consoleserverport_connect', kwargs={'termination_a_type': ConsoleServerPort}), path(r'console-server-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='consoleserverport_connect', kwargs={'termination_a_type': ConsoleServerPort}),
path(r'console-server-ports/<int:pk>/edit/', views.ConsoleServerPortEditView.as_view(), name='consoleserverport_edit'), path(r'console-server-ports/<int:pk>/edit/', views.ConsoleServerPortEditView.as_view(), name='consoleserverport_edit'),
path(r'console-server-ports/<int:pk>/delete/', views.ConsoleServerPortDeleteView.as_view(), name='consoleserverport_delete'), path(r'console-server-ports/<int:pk>/delete/', views.ConsoleServerPortDeleteView.as_view(), name='consoleserverport_delete'),
path(r'console-server-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='consoleserverport_trace', kwargs={'model': ConsoleServerPort}), path(r'console-server-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='consoleserverport_trace', kwargs={'model': ConsoleServerPort}),
path(r'console-server-ports/rename/', views.ConsoleServerPortBulkRenameView.as_view(), name='consoleserverport_bulk_rename'), path(r'console-server-ports/rename/', views.ConsoleServerPortBulkRenameView.as_view(), name='consoleserverport_bulk_rename'),
path(r'console-server-ports/disconnect/', views.ConsoleServerPortBulkDisconnectView.as_view(), name='consoleserverport_bulk_disconnect'), path(r'console-server-ports/disconnect/', views.ConsoleServerPortBulkDisconnectView.as_view(), name='consoleserverport_bulk_disconnect'),
path(r'console-server-ports/import/', views.ConsoleServerPortBulkImportView.as_view(), name='consoleserverport_import'),
# Power ports # Power ports
path(r'devices/power-ports/add/', views.DeviceBulkAddPowerPortView.as_view(), name='device_bulk_add_powerport'), path(r'devices/power-ports/add/', views.DeviceBulkAddPowerPortView.as_view(), name='device_bulk_add_powerport'),
path(r'devices/<int:pk>/power-ports/add/', views.PowerPortCreateView.as_view(), name='powerport_add'), path(r'devices/<int:pk>/power-ports/add/', views.PowerPortCreateView.as_view(), name='powerport_add'),
path(r'devices/<int:pk>/power-ports/delete/', views.PowerPortBulkDeleteView.as_view(), name='powerport_bulk_delete'), path(r'devices/<int:pk>/power-ports/delete/', views.PowerPortBulkDeleteView.as_view(), name='powerport_bulk_delete'),
path(r'power-ports/', views.PowerPortListView.as_view(), name='powerport_list'),
path(r'power-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='powerport_connect', kwargs={'termination_a_type': PowerPort}), path(r'power-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='powerport_connect', kwargs={'termination_a_type': PowerPort}),
path(r'power-ports/<int:pk>/edit/', views.PowerPortEditView.as_view(), name='powerport_edit'), path(r'power-ports/<int:pk>/edit/', views.PowerPortEditView.as_view(), name='powerport_edit'),
path(r'power-ports/<int:pk>/delete/', views.PowerPortDeleteView.as_view(), name='powerport_delete'), path(r'power-ports/<int:pk>/delete/', views.PowerPortDeleteView.as_view(), name='powerport_delete'),
path(r'power-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='powerport_trace', kwargs={'model': PowerPort}), path(r'power-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='powerport_trace', kwargs={'model': PowerPort}),
path(r'power-ports/import/', views.PowerPortBulkImportView.as_view(), name='powerport_import'),
# Power outlets # Power outlets
path(r'devices/power-outlets/add/', views.DeviceBulkAddPowerOutletView.as_view(), name='device_bulk_add_poweroutlet'), path(r'devices/power-outlets/add/', views.DeviceBulkAddPowerOutletView.as_view(), name='device_bulk_add_poweroutlet'),
path(r'devices/<int:pk>/power-outlets/add/', views.PowerOutletCreateView.as_view(), name='poweroutlet_add'), path(r'devices/<int:pk>/power-outlets/add/', views.PowerOutletCreateView.as_view(), name='poweroutlet_add'),
path(r'devices/<int:pk>/power-outlets/edit/', views.PowerOutletBulkEditView.as_view(), name='poweroutlet_bulk_edit'), path(r'devices/<int:pk>/power-outlets/edit/', views.PowerOutletBulkEditView.as_view(), name='poweroutlet_bulk_edit'),
path(r'devices/<int:pk>/power-outlets/delete/', views.PowerOutletBulkDeleteView.as_view(), name='poweroutlet_bulk_delete'), path(r'devices/<int:pk>/power-outlets/delete/', views.PowerOutletBulkDeleteView.as_view(), name='poweroutlet_bulk_delete'),
path(r'power-outlets/', views.PowerOutletListView.as_view(), name='poweroutlet_list'),
path(r'power-outlets/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='poweroutlet_connect', kwargs={'termination_a_type': PowerOutlet}), path(r'power-outlets/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='poweroutlet_connect', kwargs={'termination_a_type': PowerOutlet}),
path(r'power-outlets/<int:pk>/edit/', views.PowerOutletEditView.as_view(), name='poweroutlet_edit'), path(r'power-outlets/<int:pk>/edit/', views.PowerOutletEditView.as_view(), name='poweroutlet_edit'),
path(r'power-outlets/<int:pk>/delete/', views.PowerOutletDeleteView.as_view(), name='poweroutlet_delete'), path(r'power-outlets/<int:pk>/delete/', views.PowerOutletDeleteView.as_view(), name='poweroutlet_delete'),
path(r'power-outlets/<int:pk>/trace/', views.CableTraceView.as_view(), name='poweroutlet_trace', kwargs={'model': PowerOutlet}), path(r'power-outlets/<int:pk>/trace/', views.CableTraceView.as_view(), name='poweroutlet_trace', kwargs={'model': PowerOutlet}),
path(r'power-outlets/rename/', views.PowerOutletBulkRenameView.as_view(), name='poweroutlet_bulk_rename'), path(r'power-outlets/rename/', views.PowerOutletBulkRenameView.as_view(), name='poweroutlet_bulk_rename'),
path(r'power-outlets/disconnect/', views.PowerOutletBulkDisconnectView.as_view(), name='poweroutlet_bulk_disconnect'), path(r'power-outlets/disconnect/', views.PowerOutletBulkDisconnectView.as_view(), name='poweroutlet_bulk_disconnect'),
path(r'power-outlets/import/', views.PowerOutletBulkImportView.as_view(), name='poweroutlet_import'),
# Interfaces # Interfaces
path(r'devices/interfaces/add/', views.DeviceBulkAddInterfaceView.as_view(), name='device_bulk_add_interface'), path(r'devices/interfaces/add/', views.DeviceBulkAddInterfaceView.as_view(), name='device_bulk_add_interface'),
path(r'devices/<int:pk>/interfaces/add/', views.InterfaceCreateView.as_view(), name='interface_add'), path(r'devices/<int:pk>/interfaces/add/', views.InterfaceCreateView.as_view(), name='interface_add'),
path(r'devices/<int:pk>/interfaces/edit/', views.InterfaceBulkEditView.as_view(), name='interface_bulk_edit'), path(r'devices/<int:pk>/interfaces/edit/', views.InterfaceBulkEditView.as_view(), name='interface_bulk_edit'),
path(r'devices/<int:pk>/interfaces/delete/', views.InterfaceBulkDeleteView.as_view(), name='interface_bulk_delete'), path(r'devices/<int:pk>/interfaces/delete/', views.InterfaceBulkDeleteView.as_view(), name='interface_bulk_delete'),
path(r'interfaces/', views.InterfaceListView.as_view(), name='interface_list'),
path(r'interfaces/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='interface_connect', kwargs={'termination_a_type': Interface}), path(r'interfaces/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='interface_connect', kwargs={'termination_a_type': Interface}),
path(r'interfaces/<int:pk>/', views.InterfaceView.as_view(), name='interface'), path(r'interfaces/<int:pk>/', views.InterfaceView.as_view(), name='interface'),
path(r'interfaces/<int:pk>/edit/', views.InterfaceEditView.as_view(), name='interface_edit'), path(r'interfaces/<int:pk>/edit/', views.InterfaceEditView.as_view(), name='interface_edit'),
@@ -231,47 +222,40 @@ urlpatterns = [
path(r'interfaces/<int:pk>/trace/', views.CableTraceView.as_view(), name='interface_trace', kwargs={'model': Interface}), path(r'interfaces/<int:pk>/trace/', views.CableTraceView.as_view(), name='interface_trace', kwargs={'model': Interface}),
path(r'interfaces/rename/', views.InterfaceBulkRenameView.as_view(), name='interface_bulk_rename'), path(r'interfaces/rename/', views.InterfaceBulkRenameView.as_view(), name='interface_bulk_rename'),
path(r'interfaces/disconnect/', views.InterfaceBulkDisconnectView.as_view(), name='interface_bulk_disconnect'), path(r'interfaces/disconnect/', views.InterfaceBulkDisconnectView.as_view(), name='interface_bulk_disconnect'),
path(r'interfaces/import/', views.InterfaceBulkImportView.as_view(), name='interface_import'),
# Front ports # Front ports
# path(r'devices/front-ports/add/', views.DeviceBulkAddFrontPortView.as_view(), name='device_bulk_add_frontport'), # path(r'devices/front-ports/add/', views.DeviceBulkAddFrontPortView.as_view(), name='device_bulk_add_frontport'),
path(r'devices/<int:pk>/front-ports/add/', views.FrontPortCreateView.as_view(), name='frontport_add'), path(r'devices/<int:pk>/front-ports/add/', views.FrontPortCreateView.as_view(), name='frontport_add'),
path(r'devices/<int:pk>/front-ports/edit/', views.FrontPortBulkEditView.as_view(), name='frontport_bulk_edit'), path(r'devices/<int:pk>/front-ports/edit/', views.FrontPortBulkEditView.as_view(), name='frontport_bulk_edit'),
path(r'devices/<int:pk>/front-ports/delete/', views.FrontPortBulkDeleteView.as_view(), name='frontport_bulk_delete'), path(r'devices/<int:pk>/front-ports/delete/', views.FrontPortBulkDeleteView.as_view(), name='frontport_bulk_delete'),
path(r'front-ports/', views.FrontPortListView.as_view(), name='frontport_list'),
path(r'front-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='frontport_connect', kwargs={'termination_a_type': FrontPort}), path(r'front-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='frontport_connect', kwargs={'termination_a_type': FrontPort}),
path(r'front-ports/<int:pk>/edit/', views.FrontPortEditView.as_view(), name='frontport_edit'), path(r'front-ports/<int:pk>/edit/', views.FrontPortEditView.as_view(), name='frontport_edit'),
path(r'front-ports/<int:pk>/delete/', views.FrontPortDeleteView.as_view(), name='frontport_delete'), path(r'front-ports/<int:pk>/delete/', views.FrontPortDeleteView.as_view(), name='frontport_delete'),
path(r'front-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='frontport_trace', kwargs={'model': FrontPort}), path(r'front-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='frontport_trace', kwargs={'model': FrontPort}),
path(r'front-ports/rename/', views.FrontPortBulkRenameView.as_view(), name='frontport_bulk_rename'), path(r'front-ports/rename/', views.FrontPortBulkRenameView.as_view(), name='frontport_bulk_rename'),
path(r'front-ports/disconnect/', views.FrontPortBulkDisconnectView.as_view(), name='frontport_bulk_disconnect'), path(r'front-ports/disconnect/', views.FrontPortBulkDisconnectView.as_view(), name='frontport_bulk_disconnect'),
path(r'front-ports/import/', views.FrontPortBulkImportView.as_view(), name='frontport_import'),
# Rear ports # Rear ports
# path(r'devices/rear-ports/add/', views.DeviceBulkAddRearPortView.as_view(), name='device_bulk_add_rearport'), # path(r'devices/rear-ports/add/', views.DeviceBulkAddRearPortView.as_view(), name='device_bulk_add_rearport'),
path(r'devices/<int:pk>/rear-ports/add/', views.RearPortCreateView.as_view(), name='rearport_add'), path(r'devices/<int:pk>/rear-ports/add/', views.RearPortCreateView.as_view(), name='rearport_add'),
path(r'devices/<int:pk>/rear-ports/edit/', views.RearPortBulkEditView.as_view(), name='rearport_bulk_edit'), path(r'devices/<int:pk>/rear-ports/edit/', views.RearPortBulkEditView.as_view(), name='rearport_bulk_edit'),
path(r'devices/<int:pk>/rear-ports/delete/', views.RearPortBulkDeleteView.as_view(), name='rearport_bulk_delete'), path(r'devices/<int:pk>/rear-ports/delete/', views.RearPortBulkDeleteView.as_view(), name='rearport_bulk_delete'),
path(r'rear-ports/', views.RearPortListView.as_view(), name='rearport_list'),
path(r'rear-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='rearport_connect', kwargs={'termination_a_type': RearPort}), path(r'rear-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='rearport_connect', kwargs={'termination_a_type': RearPort}),
path(r'rear-ports/<int:pk>/edit/', views.RearPortEditView.as_view(), name='rearport_edit'), path(r'rear-ports/<int:pk>/edit/', views.RearPortEditView.as_view(), name='rearport_edit'),
path(r'rear-ports/<int:pk>/delete/', views.RearPortDeleteView.as_view(), name='rearport_delete'), path(r'rear-ports/<int:pk>/delete/', views.RearPortDeleteView.as_view(), name='rearport_delete'),
path(r'rear-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='rearport_trace', kwargs={'model': RearPort}), path(r'rear-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='rearport_trace', kwargs={'model': RearPort}),
path(r'rear-ports/rename/', views.RearPortBulkRenameView.as_view(), name='rearport_bulk_rename'), path(r'rear-ports/rename/', views.RearPortBulkRenameView.as_view(), name='rearport_bulk_rename'),
path(r'rear-ports/disconnect/', views.RearPortBulkDisconnectView.as_view(), name='rearport_bulk_disconnect'), path(r'rear-ports/disconnect/', views.RearPortBulkDisconnectView.as_view(), name='rearport_bulk_disconnect'),
path(r'rear-ports/import/', views.RearPortBulkImportView.as_view(), name='rearport_import'),
# Device bays # Device bays
path(r'devices/device-bays/add/', views.DeviceBulkAddDeviceBayView.as_view(), name='device_bulk_add_devicebay'), path(r'devices/device-bays/add/', views.DeviceBulkAddDeviceBayView.as_view(), name='device_bulk_add_devicebay'),
path(r'devices/<int:pk>/bays/add/', views.DeviceBayCreateView.as_view(), name='devicebay_add'), path(r'devices/<int:pk>/bays/add/', views.DeviceBayCreateView.as_view(), name='devicebay_add'),
path(r'devices/<int:pk>/bays/delete/', views.DeviceBayBulkDeleteView.as_view(), name='devicebay_bulk_delete'), path(r'devices/<int:pk>/bays/delete/', views.DeviceBayBulkDeleteView.as_view(), name='devicebay_bulk_delete'),
path(r'device-bays/', views.DeviceBayListView.as_view(), name='devicebay_list'),
path(r'device-bays/<int:pk>/edit/', views.DeviceBayEditView.as_view(), name='devicebay_edit'), path(r'device-bays/<int:pk>/edit/', views.DeviceBayEditView.as_view(), name='devicebay_edit'),
path(r'device-bays/<int:pk>/delete/', views.DeviceBayDeleteView.as_view(), name='devicebay_delete'), path(r'device-bays/<int:pk>/delete/', views.DeviceBayDeleteView.as_view(), name='devicebay_delete'),
path(r'device-bays/<int:pk>/populate/', views.DeviceBayPopulateView.as_view(), name='devicebay_populate'), path(r'device-bays/<int:pk>/populate/', views.DeviceBayPopulateView.as_view(), name='devicebay_populate'),
path(r'device-bays/<int:pk>/depopulate/', views.DeviceBayDepopulateView.as_view(), name='devicebay_depopulate'), path(r'device-bays/<int:pk>/depopulate/', views.DeviceBayDepopulateView.as_view(), name='devicebay_depopulate'),
path(r'device-bays/rename/', views.DeviceBayBulkRenameView.as_view(), name='devicebay_bulk_rename'), path(r'device-bays/rename/', views.DeviceBayBulkRenameView.as_view(), name='devicebay_bulk_rename'),
path(r'device-bays/import/', views.DeviceBayBulkImportView.as_view(), name='devicebay_import'),
# Inventory items # Inventory items
path(r'inventory-items/', views.InventoryItemListView.as_view(), name='inventoryitem_list'), path(r'inventory-items/', views.InventoryItemListView.as_view(), name='inventoryitem_list'),

View File

@@ -1,4 +1,3 @@
from collections import OrderedDict
import re import re
from django.conf import settings from django.conf import settings
@@ -17,7 +16,8 @@ from django.utils.safestring import mark_safe
from django.views.generic import View from django.views.generic import View
from circuits.models import Circuit from circuits.models import Circuit
from extras.models import Graph from extras.constants import GRAPH_TYPE_DEVICE, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE
from extras.models import Graph, TopologyMap
from extras.views import ObjectConfigContextView from extras.views import ObjectConfigContextView
from ipam.models import Prefix, VLAN from ipam.models import Prefix, VLAN
from ipam.tables import InterfaceIPAddressTable, InterfaceVLANTable from ipam.tables import InterfaceIPAddressTable, InterfaceVLANTable
@@ -26,7 +26,7 @@ from utilities.paginator import EnhancedPaginator
from utilities.utils import csv_format from utilities.utils import csv_format
from utilities.views import ( from utilities.views import (
BulkComponentCreateView, BulkDeleteView, BulkEditView, BulkImportView, ComponentCreateView, GetReturnURLMixin, BulkComponentCreateView, BulkDeleteView, BulkEditView, BulkImportView, ComponentCreateView, GetReturnURLMixin,
ObjectImportView, ObjectDeleteView, ObjectEditView, ObjectListView, ObjectDeleteView, ObjectEditView, ObjectListView,
) )
from virtualization.models import VirtualMachine from virtualization.models import VirtualMachine
from . import filters, forms, tables from . import filters, forms, tables
@@ -208,12 +208,14 @@ class SiteView(PermissionRequiredMixin, View):
'vm_count': VirtualMachine.objects.filter(cluster__site=site).count(), 'vm_count': VirtualMachine.objects.filter(cluster__site=site).count(),
} }
rack_groups = RackGroup.objects.filter(site=site).annotate(rack_count=Count('racks')) rack_groups = RackGroup.objects.filter(site=site).annotate(rack_count=Count('racks'))
show_graphs = Graph.objects.filter(type__model='site').exists() topology_maps = TopologyMap.objects.filter(site=site)
show_graphs = Graph.objects.filter(type=GRAPH_TYPE_SITE).exists()
return render(request, 'dcim/site.html', { return render(request, 'dcim/site.html', {
'site': site, 'site': site,
'stats': stats, 'stats': stats,
'rack_groups': rack_groups, 'rack_groups': rack_groups,
'topology_maps': topology_maps,
'show_graphs': show_graphs, 'show_graphs': show_graphs,
}) })
@@ -419,6 +421,8 @@ class RackView(PermissionRequiredMixin, View):
'nonracked_devices': nonracked_devices, 'nonracked_devices': nonracked_devices,
'next_rack': next_rack, 'next_rack': next_rack,
'prev_rack': prev_rack, 'prev_rack': prev_rack,
'front_elevation': rack.get_front_elevation(),
'rear_elevation': rack.get_rear_elevation(),
}) })
@@ -655,31 +659,11 @@ class DeviceTypeDeleteView(PermissionRequiredMixin, ObjectDeleteView):
default_return_url = 'dcim:devicetype_list' default_return_url = 'dcim:devicetype_list'
class DeviceTypeImportView(PermissionRequiredMixin, ObjectImportView): class DeviceTypeBulkImportView(PermissionRequiredMixin, BulkImportView):
permission_required = [ permission_required = 'dcim.add_devicetype'
'dcim.add_devicetype', model_form = forms.DeviceTypeCSVForm
'dcim.add_consoleporttemplate', table = tables.DeviceTypeTable
'dcim.add_consoleserverporttemplate', default_return_url = 'dcim:devicetype_list'
'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): class DeviceTypeBulkEditView(PermissionRequiredMixin, BulkEditView):
@@ -1055,8 +1039,8 @@ class DeviceView(PermissionRequiredMixin, View):
'secrets': secrets, 'secrets': secrets,
'vc_members': vc_members, 'vc_members': vc_members,
'related_devices': related_devices, 'related_devices': related_devices,
'show_graphs': Graph.objects.filter(type__model='device').exists(), 'show_graphs': Graph.objects.filter(type=GRAPH_TYPE_DEVICE).exists(),
'show_interface_graphs': Graph.objects.filter(type__model='interface').exists(), 'show_interface_graphs': Graph.objects.filter(type=GRAPH_TYPE_INTERFACE).exists(),
}) })
@@ -1194,15 +1178,6 @@ class DeviceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
# Console ports # Console ports
# #
class ConsolePortListView(PermissionRequiredMixin, ObjectListView):
permission_required = 'dcim.view_consoleport'
queryset = ConsolePort.objects.prefetch_related('device', 'device__tenant', 'device__site', 'cable')
filter = filters.ConsolePortFilter
filter_form = forms.ConsolePortFilterForm
table = tables.ConsolePortDetailTable
template_name = 'dcim/device_component_list.html'
class ConsolePortCreateView(PermissionRequiredMixin, ComponentCreateView): class ConsolePortCreateView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_consoleport' permission_required = 'dcim.add_consoleport'
parent_model = Device parent_model = Device
@@ -1224,13 +1199,6 @@ class ConsolePortDeleteView(PermissionRequiredMixin, ObjectDeleteView):
model = ConsolePort model = ConsolePort
class ConsolePortBulkImportView(PermissionRequiredMixin, BulkImportView):
permission_required = 'dcim.add_consoleport'
model_form = forms.ConsolePortCSVForm
table = tables.ConsolePortImportTable
default_return_url = 'dcim:consoleport_list'
class ConsolePortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class ConsolePortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_consoleport' permission_required = 'dcim.delete_consoleport'
queryset = ConsolePort.objects.all() queryset = ConsolePort.objects.all()
@@ -1242,15 +1210,6 @@ class ConsolePortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
# Console server ports # Console server ports
# #
class ConsoleServerPortListView(PermissionRequiredMixin, ObjectListView):
permission_required = 'dcim.view_consoleserverport'
queryset = ConsoleServerPort.objects.prefetch_related('device', 'device__tenant', 'device__site', 'cable')
filter = filters.ConsoleServerPortFilter
filter_form = forms.ConsoleServerPortFilterForm
table = tables.ConsoleServerPortDetailTable
template_name = 'dcim/device_component_list.html'
class ConsoleServerPortCreateView(PermissionRequiredMixin, ComponentCreateView): class ConsoleServerPortCreateView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_consoleserverport' permission_required = 'dcim.add_consoleserverport'
parent_model = Device parent_model = Device
@@ -1272,13 +1231,6 @@ class ConsoleServerPortDeleteView(PermissionRequiredMixin, ObjectDeleteView):
model = ConsoleServerPort model = ConsoleServerPort
class ConsoleServerPortBulkImportView(PermissionRequiredMixin, BulkImportView):
permission_required = 'dcim.add_consoleserverport'
model_form = forms.ConsoleServerPortCSVForm
table = tables.ConsoleServerPortImportTable
default_return_url = 'dcim:consoleserverport_list'
class ConsoleServerPortBulkEditView(PermissionRequiredMixin, BulkEditView): class ConsoleServerPortBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.change_consoleserverport' permission_required = 'dcim.change_consoleserverport'
queryset = ConsoleServerPort.objects.all() queryset = ConsoleServerPort.objects.all()
@@ -1310,15 +1262,6 @@ class ConsoleServerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
# Power ports # Power ports
# #
class PowerPortListView(PermissionRequiredMixin, ObjectListView):
permission_required = 'dcim.view_powerport'
queryset = PowerPort.objects.prefetch_related('device', 'device__tenant', 'device__site', 'cable')
filter = filters.PowerPortFilter
filter_form = forms.PowerPortFilterForm
table = tables.PowerPortDetailTable
template_name = 'dcim/device_component_list.html'
class PowerPortCreateView(PermissionRequiredMixin, ComponentCreateView): class PowerPortCreateView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_powerport' permission_required = 'dcim.add_powerport'
parent_model = Device parent_model = Device
@@ -1340,13 +1283,6 @@ class PowerPortDeleteView(PermissionRequiredMixin, ObjectDeleteView):
model = PowerPort model = PowerPort
class PowerPortBulkImportView(PermissionRequiredMixin, BulkImportView):
permission_required = 'dcim.add_powerport'
model_form = forms.PowerPortCSVForm
table = tables.PowerPortImportTable
default_return_url = 'dcim:powerport_list'
class PowerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class PowerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_powerport' permission_required = 'dcim.delete_powerport'
queryset = PowerPort.objects.all() queryset = PowerPort.objects.all()
@@ -1358,15 +1294,6 @@ class PowerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
# Power outlets # Power outlets
# #
class PowerOutletListView(PermissionRequiredMixin, ObjectListView):
permission_required = 'dcim.view_poweroutlet'
queryset = PowerOutlet.objects.prefetch_related('device', 'device__tenant', 'device__site', 'cable')
filter = filters.PowerOutletFilter
filter_form = forms.PowerOutletFilterForm
table = tables.PowerOutletDetailTable
template_name = 'dcim/device_component_list.html'
class PowerOutletCreateView(PermissionRequiredMixin, ComponentCreateView): class PowerOutletCreateView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_poweroutlet' permission_required = 'dcim.add_poweroutlet'
parent_model = Device parent_model = Device
@@ -1388,13 +1315,6 @@ class PowerOutletDeleteView(PermissionRequiredMixin, ObjectDeleteView):
model = PowerOutlet model = PowerOutlet
class PowerOutletBulkImportView(PermissionRequiredMixin, BulkImportView):
permission_required = 'dcim.add_poweroutlet'
model_form = forms.PowerOutletCSVForm
table = tables.PowerOutletImportTable
default_return_url = 'dcim:poweroutlet_list'
class PowerOutletBulkEditView(PermissionRequiredMixin, BulkEditView): class PowerOutletBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.change_poweroutlet' permission_required = 'dcim.change_poweroutlet'
queryset = PowerOutlet.objects.all() queryset = PowerOutlet.objects.all()
@@ -1426,15 +1346,6 @@ class PowerOutletBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
# Interfaces # Interfaces
# #
class InterfaceListView(PermissionRequiredMixin, ObjectListView):
permission_required = 'dcim.view_interface'
queryset = Interface.objects.prefetch_related('device', 'device__tenant', 'device__site', 'cable')
filter = filters.InterfaceFilter
filter_form = forms.InterfaceFilterForm
table = tables.InterfaceDetailTable
template_name = 'dcim/device_component_list.html'
class InterfaceView(PermissionRequiredMixin, View): class InterfaceView(PermissionRequiredMixin, View):
permission_required = 'dcim.view_interface' permission_required = 'dcim.view_interface'
@@ -1493,13 +1404,6 @@ class InterfaceDeleteView(PermissionRequiredMixin, ObjectDeleteView):
model = Interface model = Interface
class InterfaceBulkImportView(PermissionRequiredMixin, BulkImportView):
permission_required = 'dcim.add_interface'
model_form = forms.InterfaceCSVForm
table = tables.InterfaceImportTable
default_return_url = 'dcim:interface_list'
class InterfaceBulkEditView(PermissionRequiredMixin, BulkEditView): class InterfaceBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.change_interface' permission_required = 'dcim.change_interface'
queryset = Interface.objects.all() queryset = Interface.objects.all()
@@ -1531,15 +1435,6 @@ class InterfaceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
# Front ports # Front ports
# #
class FrontPortListView(PermissionRequiredMixin, ObjectListView):
permission_required = 'dcim.view_frontport'
queryset = FrontPort.objects.prefetch_related('device', 'device__tenant', 'device__site', 'cable')
filter = filters.FrontPortFilter
filter_form = forms.FrontPortFilterForm
table = tables.FrontPortDetailTable
template_name = 'dcim/device_component_list.html'
class FrontPortCreateView(PermissionRequiredMixin, ComponentCreateView): class FrontPortCreateView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_frontport' permission_required = 'dcim.add_frontport'
parent_model = Device parent_model = Device
@@ -1561,13 +1456,6 @@ class FrontPortDeleteView(PermissionRequiredMixin, ObjectDeleteView):
model = FrontPort model = FrontPort
class FrontPortBulkImportView(PermissionRequiredMixin, BulkImportView):
permission_required = 'dcim.add_frontport'
model_form = forms.FrontPortCSVForm
table = tables.FrontPortImportTable
default_return_url = 'dcim:frontport_list'
class FrontPortBulkEditView(PermissionRequiredMixin, BulkEditView): class FrontPortBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.change_frontport' permission_required = 'dcim.change_frontport'
queryset = FrontPort.objects.all() queryset = FrontPort.objects.all()
@@ -1599,15 +1487,6 @@ class FrontPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
# Rear ports # Rear ports
# #
class RearPortListView(PermissionRequiredMixin, ObjectListView):
permission_required = 'dcim.view_rearport'
queryset = RearPort.objects.prefetch_related('device', 'device__tenant', 'device__site', 'cable')
filter = filters.RearPortFilter
filter_form = forms.RearPortFilterForm
table = tables.RearPortDetailTable
template_name = 'dcim/device_component_list.html'
class RearPortCreateView(PermissionRequiredMixin, ComponentCreateView): class RearPortCreateView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_rearport' permission_required = 'dcim.add_rearport'
parent_model = Device parent_model = Device
@@ -1629,13 +1508,6 @@ class RearPortDeleteView(PermissionRequiredMixin, ObjectDeleteView):
model = RearPort model = RearPort
class RearPortBulkImportView(PermissionRequiredMixin, BulkImportView):
permission_required = 'dcim.add_rearport'
model_form = forms.RearPortCSVForm
table = tables.RearPortImportTable
default_return_url = 'dcim:rearport_list'
class RearPortBulkEditView(PermissionRequiredMixin, BulkEditView): class RearPortBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.change_rearport' permission_required = 'dcim.change_rearport'
queryset = RearPort.objects.all() queryset = RearPort.objects.all()
@@ -1667,17 +1539,6 @@ class RearPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
# Device bays # Device bays
# #
class DeviceBayListView(PermissionRequiredMixin, ObjectListView):
permission_required = 'dcim.view_devicebay'
queryset = DeviceBay.objects.prefetch_related(
'device', 'device__site', 'installed_device', 'installed_device__site'
)
filter = filters.DeviceBayFilter
filter_form = forms.DeviceBayFilterForm
table = tables.DeviceBayDetailTable
template_name = 'dcim/device_component_list.html'
class DeviceBayCreateView(PermissionRequiredMixin, ComponentCreateView): class DeviceBayCreateView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_devicebay' permission_required = 'dcim.add_devicebay'
parent_model = Device parent_model = Device
@@ -1768,13 +1629,6 @@ class DeviceBayDepopulateView(PermissionRequiredMixin, View):
}) })
class DeviceBayBulkImportView(PermissionRequiredMixin, BulkImportView):
permission_required = 'dcim.add_devicebay'
model_form = forms.DeviceBayCSVForm
table = tables.DeviceBayImportTable
default_return_url = 'dcim:devicebay_list'
class DeviceBayBulkRenameView(PermissionRequiredMixin, BulkRenameView): class DeviceBayBulkRenameView(PermissionRequiredMixin, BulkRenameView):
permission_required = 'dcim.change_devicebay' permission_required = 'dcim.change_devicebay'
queryset = DeviceBay.objects.all() queryset = DeviceBay.objects.all()

View File

@@ -1 +1,15 @@
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
default_app_config = 'extras.apps.ExtrasConfig' default_app_config = 'extras.apps.ExtrasConfig'
# check that django-rq is installed and we can connect to redis
if settings.WEBHOOKS_ENABLED:
try:
import django_rq
except ImportError:
raise ImproperlyConfigured(
"django-rq is not installed! You must install this package per "
"the documentation to use the webhook backend."
)

View File

@@ -3,7 +3,7 @@ from django.contrib import admin
from netbox.admin import admin_site from netbox.admin import admin_site
from utilities.forms import LaxURLField from utilities.forms import LaxURLField
from .models import CustomField, CustomFieldChoice, CustomLink, Graph, ExportTemplate, Webhook from .models import CustomField, CustomFieldChoice, CustomLink, Graph, ExportTemplate, TopologyMap, Webhook
def order_content_types(field): def order_content_types(field):
@@ -164,3 +164,15 @@ class ExportTemplateAdmin(admin.ModelAdmin):
'content_type', 'content_type',
] ]
form = ExportTemplateForm form = ExportTemplateForm
#
# Topology maps
#
@admin.register(TopologyMap, site=admin_site)
class TopologyMapAdmin(admin.ModelAdmin):
list_display = ['name', 'slug', 'site']
prepopulated_fields = {
'slug': ['name'],
}

View File

@@ -5,7 +5,7 @@ from django.db import transaction
from rest_framework import serializers from rest_framework import serializers
from rest_framework.exceptions import ValidationError from rest_framework.exceptions import ValidationError
from extras.choices import * from extras.constants import *
from extras.models import CustomField, CustomFieldChoice, CustomFieldValue from extras.models import CustomField, CustomFieldChoice, CustomFieldValue
from utilities.api import ValidatedModelSerializer from utilities.api import ValidatedModelSerializer
@@ -37,7 +37,7 @@ class CustomFieldsSerializer(serializers.BaseSerializer):
if value not in [None, '']: if value not in [None, '']:
# Validate integer # Validate integer
if cf.type == CustomFieldTypeChoices.TYPE_INTEGER: if cf.type == CF_TYPE_INTEGER:
try: try:
int(value) int(value)
except ValueError: except ValueError:
@@ -46,13 +46,13 @@ class CustomFieldsSerializer(serializers.BaseSerializer):
) )
# Validate boolean # Validate boolean
if cf.type == CustomFieldTypeChoices.TYPE_BOOLEAN and value not in [True, False, 1, 0]: if cf.type == CF_TYPE_BOOLEAN and value not in [True, False, 1, 0]:
raise ValidationError( raise ValidationError(
"Invalid value for boolean field {}: {}".format(field_name, value) "Invalid value for boolean field {}: {}".format(field_name, value)
) )
# Validate date # Validate date
if cf.type == CustomFieldTypeChoices.TYPE_DATE: if cf.type == CF_TYPE_DATE:
try: try:
datetime.strptime(value, '%Y-%m-%d') datetime.strptime(value, '%Y-%m-%d')
except ValueError: except ValueError:
@@ -61,7 +61,7 @@ class CustomFieldsSerializer(serializers.BaseSerializer):
) )
# Validate selected choice # Validate selected choice
if cf.type == CustomFieldTypeChoices.TYPE_SELECT: if cf.type == CF_TYPE_SELECT:
try: try:
value = int(value) value = int(value)
except ValueError: except ValueError:
@@ -100,7 +100,7 @@ class CustomFieldModelSerializer(ValidatedModelSerializer):
instance.custom_fields = {} instance.custom_fields = {}
for field in fields: for field in fields:
value = instance.cf.get(field.name) value = instance.cf.get(field.name)
if field.type == CustomFieldTypeChoices.TYPE_SELECT and value is not None: if field.type == CF_TYPE_SELECT and value is not None:
instance.custom_fields[field.name] = CustomFieldChoiceSerializer(value).data instance.custom_fields[field.name] = CustomFieldChoiceSerializer(value).data
else: else:
instance.custom_fields[field.name] = value instance.custom_fields[field.name] = value

View File

@@ -8,10 +8,10 @@ from dcim.api.nested_serializers import (
NestedRegionSerializer, NestedSiteSerializer, NestedRegionSerializer, NestedSiteSerializer,
) )
from dcim.models import Device, DeviceRole, Platform, Rack, Region, Site from dcim.models import Device, DeviceRole, Platform, Rack, Region, Site
from extras.choices import *
from extras.constants import * from extras.constants import *
from extras.models import ( from extras.models import (
ConfigContext, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, Tag, ConfigContext, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, TopologyMap,
Tag
) )
from tenancy.api.nested_serializers import NestedTenantSerializer, NestedTenantGroupSerializer from tenancy.api.nested_serializers import NestedTenantSerializer, NestedTenantGroupSerializer
from tenancy.models import Tenant, TenantGroup from tenancy.models import Tenant, TenantGroup
@@ -28,9 +28,7 @@ from .nested_serializers import *
# #
class GraphSerializer(ValidatedModelSerializer): class GraphSerializer(ValidatedModelSerializer):
type = ContentTypeField( type = ChoiceField(choices=GRAPH_TYPE_CHOICES)
queryset=ContentType.objects.all()
)
class Meta: class Meta:
model = Graph model = Graph
@@ -40,9 +38,7 @@ class GraphSerializer(ValidatedModelSerializer):
class RenderedGraphSerializer(serializers.ModelSerializer): class RenderedGraphSerializer(serializers.ModelSerializer):
embed_url = serializers.SerializerMethodField() embed_url = serializers.SerializerMethodField()
embed_link = serializers.SerializerMethodField() embed_link = serializers.SerializerMethodField()
type = ContentTypeField( type = ChoiceField(choices=GRAPH_TYPE_CHOICES)
queryset=ContentType.objects.all()
)
class Meta: class Meta:
model = Graph model = Graph
@@ -61,8 +57,8 @@ class RenderedGraphSerializer(serializers.ModelSerializer):
class ExportTemplateSerializer(ValidatedModelSerializer): class ExportTemplateSerializer(ValidatedModelSerializer):
template_language = ChoiceField( template_language = ChoiceField(
choices=ExportTemplateLanguageChoices, choices=TEMPLATE_LANGUAGE_CHOICES,
default=ExportTemplateLanguageChoices.LANGUAGE_JINJA2 default=TEMPLATE_LANGUAGE_JINJA2
) )
class Meta: class Meta:
@@ -73,6 +69,18 @@ class ExportTemplateSerializer(ValidatedModelSerializer):
] ]
#
# Topology maps
#
class TopologyMapSerializer(ValidatedModelSerializer):
site = NestedSiteSerializer()
class Meta:
model = TopologyMap
fields = ['id', 'name', 'slug', 'site', 'device_patterns', 'description']
# #
# Tags # Tags
# #
@@ -173,18 +181,12 @@ class ConfigContextSerializer(ValidatedModelSerializer):
required=False, required=False,
many=True many=True
) )
tags = serializers.SlugRelatedField(
queryset=Tag.objects.all(),
slug_field='slug',
required=False,
many=True
)
class Meta: class Meta:
model = ConfigContext model = ConfigContext
fields = [ fields = [
'id', 'name', 'weight', 'description', 'is_active', 'regions', 'sites', 'roles', 'platforms', 'id', 'name', 'weight', 'description', 'is_active', 'regions', 'sites', 'roles', 'platforms',
'tenant_groups', 'tenants', 'tags', 'data', 'tenant_groups', 'tenants', 'data',
] ]
@@ -211,52 +213,6 @@ class ReportDetailSerializer(ReportSerializer):
result = ReportResultSerializer() 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 # Change logging
# #
@@ -266,7 +222,7 @@ class ObjectChangeSerializer(serializers.ModelSerializer):
read_only=True read_only=True
) )
action = ChoiceField( action = ChoiceField(
choices=ObjectChangeActionChoices, choices=OBJECTCHANGE_ACTION_CHOICES,
read_only=True read_only=True
) )
changed_object_type = ContentTypeField( changed_object_type = ContentTypeField(

View File

@@ -18,7 +18,7 @@ router.APIRootView = ExtrasRootView
router.register(r'_choices', views.ExtrasFieldChoicesViewSet, basename='field-choice') router.register(r'_choices', views.ExtrasFieldChoicesViewSet, basename='field-choice')
# Custom field choices # Custom field choices
router.register(r'_custom_field_choices', views.CustomFieldChoicesViewSet, basename='custom-field-choice') router.register(r'_custom_field_choices', views.CustomFieldChoicesViewSet, base_name='custom-field-choice')
# Graphs # Graphs
router.register(r'graphs', views.GraphViewSet) router.register(r'graphs', views.GraphViewSet)
@@ -26,6 +26,9 @@ router.register(r'graphs', views.GraphViewSet)
# Export templates # Export templates
router.register(r'export-templates', views.ExportTemplateViewSet) router.register(r'export-templates', views.ExportTemplateViewSet)
# Topology maps
router.register(r'topology-maps', views.TopologyMapViewSet)
# Tags # Tags
router.register(r'tags', views.TagViewSet) router.register(r'tags', views.TagViewSet)
@@ -38,9 +41,6 @@ router.register(r'config-contexts', views.ConfigContextViewSet)
# Reports # Reports
router.register(r'reports', views.ReportViewSet, basename='report') router.register(r'reports', views.ReportViewSet, basename='report')
# Scripts
router.register(r'scripts', views.ScriptViewSet, basename='script')
# Change logging # Change logging
router.register(r'object-changes', views.ObjectChangeViewSet) router.register(r'object-changes', views.ObjectChangeViewSet)

View File

@@ -2,8 +2,8 @@ from collections import OrderedDict
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.db.models import Count from django.db.models import Count
from django.http import Http404 from django.http import Http404, HttpResponse
from rest_framework import status from django.shortcuts import get_object_or_404
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.exceptions import PermissionDenied from rest_framework.exceptions import PermissionDenied
from rest_framework.response import Response from rest_framework.response import Response
@@ -11,10 +11,10 @@ from rest_framework.viewsets import ReadOnlyModelViewSet, ViewSet
from extras import filters from extras import filters
from extras.models import ( from extras.models import (
ConfigContext, CustomFieldChoice, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, Tag, ConfigContext, CustomFieldChoice, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, TopologyMap,
Tag,
) )
from extras.reports import get_report, get_reports from extras.reports import get_report, get_reports
from extras.scripts import get_script, get_scripts
from utilities.api import FieldChoicesViewSet, IsAuthenticatedOrLoginNotRequired, ModelViewSet from utilities.api import FieldChoicesViewSet, IsAuthenticatedOrLoginNotRequired, ModelViewSet
from . import serializers from . import serializers
@@ -115,6 +115,34 @@ class ExportTemplateViewSet(ModelViewSet):
filterset_class = filters.ExportTemplateFilter 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 # Tags
# #
@@ -224,56 +252,6 @@ class ReportViewSet(ViewSet):
return Response(serializer.data) 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 # Change logging
# #

View File

@@ -1,7 +1,6 @@
from django.apps import AppConfig from django.apps import AppConfig
from django.conf import settings from django.conf import settings
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
import redis
class ExtrasConfig(AppConfig): class ExtrasConfig(AppConfig):
@@ -11,18 +10,26 @@ class ExtrasConfig(AppConfig):
import extras.signals import extras.signals
# Check that we can connect to the configured Redis database. # Check that we can connect to the configured Redis database if webhooks are enabled.
try: if settings.WEBHOOKS_ENABLED:
rs = redis.Redis( try:
host=settings.WEBHOOKS_REDIS_HOST, import redis
port=settings.WEBHOOKS_REDIS_PORT, except ImportError:
db=settings.WEBHOOKS_REDIS_DATABASE, raise ImproperlyConfigured(
password=settings.WEBHOOKS_REDIS_PASSWORD or None, "WEBHOOKS_ENABLED is True but the redis Python package is not installed. (Try 'pip install "
ssl=settings.WEBHOOKS_REDIS_SSL, "redis'.)"
) )
rs.ping() try:
except redis.exceptions.ConnectionError: rs = redis.Redis(
raise ImproperlyConfigured( host=settings.REDIS_HOST,
"Unable to connect to the Redis database. Check that the Redis configuration has been defined in " port=settings.REDIS_PORT,
"configuration.py." db=settings.REDIS_DATABASE,
) password=settings.REDIS_PASSWORD or None,
ssl=settings.REDIS_SSL,
)
rs.ping()
except redis.exceptions.ConnectionError:
raise ImproperlyConfigured(
"Unable to connect to the Redis database. Check that the Redis configuration has been defined in "
"configuration.py."
)

View File

@@ -1,140 +0,0 @@
from utilities.choices import ChoiceSet
#
# CustomFields
#
class CustomFieldTypeChoices(ChoiceSet):
TYPE_TEXT = 'text'
TYPE_INTEGER = 'integer'
TYPE_BOOLEAN = 'boolean'
TYPE_DATE = 'date'
TYPE_URL = 'url'
TYPE_SELECT = 'select'
CHOICES = (
(TYPE_TEXT, 'Text'),
(TYPE_INTEGER, 'Integer'),
(TYPE_BOOLEAN, 'Boolean (true/false)'),
(TYPE_DATE, 'Date'),
(TYPE_URL, 'URL'),
(TYPE_SELECT, 'Selection'),
)
LEGACY_MAP = {
TYPE_TEXT: 100,
TYPE_INTEGER: 200,
TYPE_BOOLEAN: 300,
TYPE_DATE: 400,
TYPE_URL: 500,
TYPE_SELECT: 600,
}
class CustomFieldFilterLogicChoices(ChoiceSet):
FILTER_DISABLED = 'disabled'
FILTER_LOOSE = 'loose'
FILTER_EXACT = 'exact'
CHOICES = (
(FILTER_DISABLED, 'Disabled'),
(FILTER_LOOSE, 'Loose'),
(FILTER_EXACT, 'Exact'),
)
LEGACY_MAP = {
FILTER_DISABLED: 0,
FILTER_LOOSE: 1,
FILTER_EXACT: 2,
}
#
# CustomLinks
#
class CustomLinkButtonClassChoices(ChoiceSet):
CLASS_DEFAULT = 'default'
CLASS_PRIMARY = 'primary'
CLASS_SUCCESS = 'success'
CLASS_INFO = 'info'
CLASS_WARNING = 'warning'
CLASS_DANGER = 'danger'
CLASS_LINK = 'link'
CHOICES = (
(CLASS_DEFAULT, 'Default'),
(CLASS_PRIMARY, 'Primary (blue)'),
(CLASS_SUCCESS, 'Success (green)'),
(CLASS_INFO, 'Info (aqua)'),
(CLASS_WARNING, 'Warning (orange)'),
(CLASS_DANGER, 'Danger (red)'),
(CLASS_LINK, 'None (link)'),
)
#
# ObjectChanges
#
class ObjectChangeActionChoices(ChoiceSet):
ACTION_CREATE = 'create'
ACTION_UPDATE = 'update'
ACTION_DELETE = 'delete'
CHOICES = (
(ACTION_CREATE, 'Created'),
(ACTION_UPDATE, 'Updated'),
(ACTION_DELETE, 'Deleted'),
)
LEGACY_MAP = {
ACTION_CREATE: 1,
ACTION_UPDATE: 2,
ACTION_DELETE: 3,
}
#
# ExportTemplates
#
class ExportTemplateLanguageChoices(ChoiceSet):
LANGUAGE_DJANGO = 'django'
LANGUAGE_JINJA2 = 'jinja2'
CHOICES = (
(LANGUAGE_DJANGO, 'Django'),
(LANGUAGE_JINJA2, 'Jinja2'),
)
LEGACY_MAP = {
LANGUAGE_DJANGO: 10,
LANGUAGE_JINJA2: 20,
}
#
# Webhooks
#
class WebhookContentTypeChoices(ChoiceSet):
CONTENTTYPE_JSON = 'application/json'
CONTENTTYPE_FORMDATA = 'application/x-www-form-urlencoded'
CHOICES = (
(CONTENTTYPE_JSON, 'JSON'),
(CONTENTTYPE_FORMDATA, 'Form data'),
)
LEGACY_MAP = {
CONTENTTYPE_JSON: 1,
CONTENTTYPE_FORMDATA: 2,
}

View File

@@ -19,6 +19,32 @@ CUSTOMFIELD_MODELS = [
'virtualization.virtualmachine', 'virtualization.virtualmachine',
] ]
# Custom field types
CF_TYPE_TEXT = 100
CF_TYPE_INTEGER = 200
CF_TYPE_BOOLEAN = 300
CF_TYPE_DATE = 400
CF_TYPE_URL = 500
CF_TYPE_SELECT = 600
CUSTOMFIELD_TYPE_CHOICES = (
(CF_TYPE_TEXT, 'Text'),
(CF_TYPE_INTEGER, 'Integer'),
(CF_TYPE_BOOLEAN, 'Boolean (true/false)'),
(CF_TYPE_DATE, 'Date'),
(CF_TYPE_URL, 'URL'),
(CF_TYPE_SELECT, 'Selection'),
)
# Custom field filter logic choices
CF_FILTER_DISABLED = 0
CF_FILTER_LOOSE = 1
CF_FILTER_EXACT = 2
CF_FILTER_CHOICES = (
(CF_FILTER_DISABLED, 'Disabled'),
(CF_FILTER_LOOSE, 'Loose'),
(CF_FILTER_EXACT, 'Exact'),
)
# Custom links # Custom links
CUSTOMLINK_MODELS = [ CUSTOMLINK_MODELS = [
'circuits.circuit', 'circuits.circuit',
@@ -42,6 +68,35 @@ CUSTOMLINK_MODELS = [
'virtualization.virtualmachine', 'virtualization.virtualmachine',
] ]
BUTTON_CLASS_DEFAULT = 'default'
BUTTON_CLASS_PRIMARY = 'primary'
BUTTON_CLASS_SUCCESS = 'success'
BUTTON_CLASS_INFO = 'info'
BUTTON_CLASS_WARNING = 'warning'
BUTTON_CLASS_DANGER = 'danger'
BUTTON_CLASS_LINK = 'link'
BUTTON_CLASS_CHOICES = (
(BUTTON_CLASS_DEFAULT, 'Default'),
(BUTTON_CLASS_PRIMARY, 'Primary (blue)'),
(BUTTON_CLASS_SUCCESS, 'Success (green)'),
(BUTTON_CLASS_INFO, 'Info (aqua)'),
(BUTTON_CLASS_WARNING, 'Warning (orange)'),
(BUTTON_CLASS_DANGER, 'Danger (red)'),
(BUTTON_CLASS_LINK, 'None (link)'),
)
# Graph types
GRAPH_TYPE_INTERFACE = 100
GRAPH_TYPE_DEVICE = 150
GRAPH_TYPE_PROVIDER = 200
GRAPH_TYPE_SITE = 300
GRAPH_TYPE_CHOICES = (
(GRAPH_TYPE_INTERFACE, 'Interface'),
(GRAPH_TYPE_DEVICE, 'Device'),
(GRAPH_TYPE_PROVIDER, 'Provider'),
(GRAPH_TYPE_SITE, 'Site'),
)
# Models which support export templates # Models which support export templates
EXPORTTEMPLATE_MODELS = [ EXPORTTEMPLATE_MODELS = [
'circuits.circuit', 'circuits.circuit',
@@ -73,6 +128,52 @@ EXPORTTEMPLATE_MODELS = [
'virtualization.virtualmachine', 'virtualization.virtualmachine',
] ]
# ExportTemplate language choices
TEMPLATE_LANGUAGE_DJANGO = 10
TEMPLATE_LANGUAGE_JINJA2 = 20
TEMPLATE_LANGUAGE_CHOICES = (
(TEMPLATE_LANGUAGE_DJANGO, 'Django'),
(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
OBJECTCHANGE_ACTION_DELETE = 3
OBJECTCHANGE_ACTION_CHOICES = (
(OBJECTCHANGE_ACTION_CREATE, 'Created'),
(OBJECTCHANGE_ACTION_UPDATE, 'Updated'),
(OBJECTCHANGE_ACTION_DELETE, 'Deleted'),
)
# User action types
ACTION_CREATE = 1
ACTION_IMPORT = 2
ACTION_EDIT = 3
ACTION_BULK_EDIT = 4
ACTION_DELETE = 5
ACTION_BULK_DELETE = 6
ACTION_BULK_CREATE = 7
ACTION_CHOICES = (
(ACTION_CREATE, 'created'),
(ACTION_BULK_CREATE, 'bulk created'),
(ACTION_IMPORT, 'imported'),
(ACTION_EDIT, 'modified'),
(ACTION_BULK_EDIT, 'bulk edited'),
(ACTION_DELETE, 'deleted'),
(ACTION_BULK_DELETE, 'bulk deleted'),
)
# Report logging levels # Report logging levels
LOG_DEFAULT = 0 LOG_DEFAULT = 0
LOG_SUCCESS = 10 LOG_SUCCESS = 10
@@ -87,6 +188,14 @@ LOG_LEVEL_CODES = {
LOG_FAILURE: 'failure', LOG_FAILURE: 'failure',
} }
# webhook content types
WEBHOOK_CT_JSON = 1
WEBHOOK_CT_X_WWW_FORM_ENCODED = 2
WEBHOOK_CT_CHOICES = (
(WEBHOOK_CT_JSON, 'application/json'),
(WEBHOOK_CT_X_WWW_FORM_ENCODED, 'application/x-www-form-urlencoded'),
)
# Models which support registered webhooks # Models which support registered webhooks
WEBHOOK_MODELS = [ WEBHOOK_MODELS = [
'circuits.circuit', 'circuits.circuit',

View File

@@ -4,8 +4,8 @@ from django.db.models import Q
from dcim.models import DeviceRole, Platform, Region, Site from dcim.models import DeviceRole, Platform, Region, Site
from tenancy.models import Tenant, TenantGroup from tenancy.models import Tenant, TenantGroup
from .choices import * from .constants import *
from .models import ConfigContext, CustomField, Graph, ExportTemplate, ObjectChange, Tag from .models import ConfigContext, CustomField, Graph, ExportTemplate, ObjectChange, Tag, TopologyMap
class CustomFieldFilter(django_filters.Filter): class CustomFieldFilter(django_filters.Filter):
@@ -25,7 +25,7 @@ class CustomFieldFilter(django_filters.Filter):
return queryset return queryset
# Selection fields get special treatment (values must be integers) # Selection fields get special treatment (values must be integers)
if self.cf_type == CustomFieldTypeChoices.TYPE_SELECT: if self.cf_type == CF_TYPE_SELECT:
try: try:
# Treat 0 as None # Treat 0 as None
if int(value) == 0: if int(value) == 0:
@@ -42,8 +42,7 @@ class CustomFieldFilter(django_filters.Filter):
return queryset.none() return queryset.none()
# Apply the assigned filter logic (exact or loose) # Apply the assigned filter logic (exact or loose)
if (self.cf_type == CustomFieldTypeChoices.TYPE_BOOLEAN or if self.cf_type == CF_TYPE_BOOLEAN or self.filter_logic == CF_FILTER_EXACT:
self.filter_logic == CustomFieldFilterLogicChoices.FILTER_EXACT):
queryset = queryset.filter( queryset = queryset.filter(
custom_field_values__field__name=self.field_name, custom_field_values__field__name=self.field_name,
custom_field_values__serialized_value=value custom_field_values__serialized_value=value
@@ -66,11 +65,7 @@ class CustomFieldFilterSet(django_filters.FilterSet):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
obj_type = ContentType.objects.get_for_model(self._meta.model) obj_type = ContentType.objects.get_for_model(self._meta.model)
custom_fields = CustomField.objects.filter( custom_fields = CustomField.objects.filter(obj_type=obj_type).exclude(filter_logic=CF_FILTER_DISABLED)
obj_type=obj_type
).exclude(
filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED
)
for cf in custom_fields: for cf in custom_fields:
self.filters['cf_{}'.format(cf.name)] = CustomFieldFilter(field_name=cf.name, custom_field=cf) self.filters['cf_{}'.format(cf.name)] = CustomFieldFilter(field_name=cf.name, custom_field=cf)
@@ -108,6 +103,24 @@ 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): class ConfigContextFilter(django_filters.FilterSet):
q = django_filters.CharFilter( q = django_filters.CharFilter(
method='search', method='search',
@@ -179,12 +192,6 @@ class ConfigContextFilter(django_filters.FilterSet):
to_field_name='slug', to_field_name='slug',
label='Tenant (slug)', label='Tenant (slug)',
) )
tag = django_filters.ModelMultipleChoiceFilter(
field_name='tags__slug',
queryset=Tag.objects.all(),
to_field_name='slug',
label='Tag (slug)',
)
class Meta: class Meta:
model = ConfigContext model = ConfigContext
@@ -234,24 +241,3 @@ class ObjectChangeFilter(django_filters.FilterSet):
Q(user_name__icontains=value) | Q(user_name__icontains=value) |
Q(object_repr__icontains=value) Q(object_repr__icontains=value)
) )
class CreatedUpdatedFilterSet(django_filters.FilterSet):
created = django_filters.DateFilter()
created__gte = django_filters.DateFilter(
field_name='created',
lookup_expr='gte'
)
created__lte = django_filters.DateFilter(
field_name='created',
lookup_expr='lte'
)
last_updated = django_filters.DateTimeFilter()
last_updated__gte = django_filters.DateTimeFilter(
field_name='last_updated',
lookup_expr='gte'
)
last_updated__lte = django_filters.DateTimeFilter(
field_name='last_updated',
lookup_expr='lte'
)

View File

@@ -13,7 +13,7 @@ from utilities.forms import (
CommentField, ContentTypeSelect, FilterChoiceField, LaxURLField, JSONField, SlugField, StaticSelect2, CommentField, ContentTypeSelect, FilterChoiceField, LaxURLField, JSONField, SlugField, StaticSelect2,
BOOLEAN_WITH_BLANK_CHOICES, BOOLEAN_WITH_BLANK_CHOICES,
) )
from .choices import * from .constants import *
from .models import ConfigContext, CustomField, CustomFieldValue, ImageAttachment, ObjectChange, Tag from .models import ConfigContext, CustomField, CustomFieldValue, ImageAttachment, ObjectChange, Tag
@@ -28,18 +28,18 @@ def get_custom_fields_for_model(content_type, filterable_only=False, bulk_edit=F
field_dict = OrderedDict() field_dict = OrderedDict()
custom_fields = CustomField.objects.filter(obj_type=content_type) custom_fields = CustomField.objects.filter(obj_type=content_type)
if filterable_only: if filterable_only:
custom_fields = custom_fields.exclude(filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED) custom_fields = custom_fields.exclude(filter_logic=CF_FILTER_DISABLED)
for cf in custom_fields: for cf in custom_fields:
field_name = 'cf_{}'.format(str(cf.name)) field_name = 'cf_{}'.format(str(cf.name))
initial = cf.default if not bulk_edit else None initial = cf.default if not bulk_edit else None
# Integer # Integer
if cf.type == CustomFieldTypeChoices.TYPE_INTEGER: if cf.type == CF_TYPE_INTEGER:
field = forms.IntegerField(required=cf.required, initial=initial) field = forms.IntegerField(required=cf.required, initial=initial)
# Boolean # Boolean
elif cf.type == CustomFieldTypeChoices.TYPE_BOOLEAN: elif cf.type == CF_TYPE_BOOLEAN:
choices = ( choices = (
(None, '---------'), (None, '---------'),
(1, 'True'), (1, 'True'),
@@ -56,11 +56,11 @@ def get_custom_fields_for_model(content_type, filterable_only=False, bulk_edit=F
) )
# Date # Date
elif cf.type == CustomFieldTypeChoices.TYPE_DATE: elif cf.type == CF_TYPE_DATE:
field = forms.DateField(required=cf.required, initial=initial, help_text="Date format: YYYY-MM-DD") field = forms.DateField(required=cf.required, initial=initial, help_text="Date format: YYYY-MM-DD")
# Select # Select
elif cf.type == CustomFieldTypeChoices.TYPE_SELECT: elif cf.type == CF_TYPE_SELECT:
choices = [(cfc.pk, cfc) for cfc in cf.choices.all()] choices = [(cfc.pk, cfc) for cfc in cf.choices.all()]
if not cf.required or bulk_edit or filterable_only: if not cf.required or bulk_edit or filterable_only:
choices = [(None, '---------')] + choices choices = [(None, '---------')] + choices
@@ -74,7 +74,7 @@ def get_custom_fields_for_model(content_type, filterable_only=False, bulk_edit=F
field = forms.TypedChoiceField(choices=choices, coerce=int, required=cf.required, initial=default_choice) field = forms.TypedChoiceField(choices=choices, coerce=int, required=cf.required, initial=default_choice)
# URL # URL
elif cf.type == CustomFieldTypeChoices.TYPE_URL: elif cf.type == CF_TYPE_URL:
field = LaxURLField(required=cf.required, initial=initial) field = LaxURLField(required=cf.required, initial=initial)
# Text # Text
@@ -237,14 +237,6 @@ class TagBulkEditForm(BootstrapMixin, BulkEditForm):
# #
class ConfigContextForm(BootstrapMixin, forms.ModelForm): class ConfigContextForm(BootstrapMixin, forms.ModelForm):
tags = forms.ModelMultipleChoiceField(
queryset=Tag.objects.all(),
to_field_name='slug',
required=False,
widget=APISelectMultiple(
api_url="/api/extras/tags/"
)
)
data = JSONField( data = JSONField(
label='' label=''
) )
@@ -253,7 +245,7 @@ class ConfigContextForm(BootstrapMixin, forms.ModelForm):
model = ConfigContext model = ConfigContext
fields = [ fields = [
'name', 'weight', 'description', 'is_active', 'regions', 'sites', 'roles', 'platforms', 'tenant_groups', 'name', 'weight', 'description', 'is_active', 'regions', 'sites', 'roles', 'platforms', 'tenant_groups',
'tenants', 'tags', 'data', 'tenants', 'data',
] ]
widgets = { widgets = {
'regions': APISelectMultiple( 'regions': APISelectMultiple(
@@ -273,7 +265,7 @@ class ConfigContextForm(BootstrapMixin, forms.ModelForm):
), ),
'tenants': APISelectMultiple( 'tenants': APISelectMultiple(
api_url="/api/tenancy/tenants/" api_url="/api/tenancy/tenants/"
), )
} }
@@ -354,14 +346,6 @@ class ConfigContextFilterForm(BootstrapMixin, forms.Form):
value_field="slug", value_field="slug",
) )
) )
tag = FilterChoiceField(
queryset=Tag.objects.all(),
to_field_name='slug',
widget=APISelectMultiple(
api_url="/api/extras/tags/",
value_field="slug",
)
)
# #
@@ -416,7 +400,7 @@ class ObjectChangeFilterForm(BootstrapMixin, forms.Form):
) )
) )
action = forms.ChoiceField( action = forms.ChoiceField(
choices=add_blank_choice(ObjectChangeActionChoices), choices=add_blank_choice(OBJECTCHANGE_ACTION_CHOICES),
required=False required=False
) )
user = forms.ModelChoiceField( user = forms.ModelChoiceField(

View File

@@ -9,9 +9,8 @@ from django.db.models.signals import pre_delete, post_save
from django.utils import timezone from django.utils import timezone
from django_prometheus.models import model_deletes, model_inserts, model_updates from django_prometheus.models import model_deletes, model_inserts, model_updates
from extras.utils import is_taggable
from utilities.querysets import DummyQuerySet from utilities.querysets import DummyQuerySet
from .choices import ObjectChangeActionChoices from .constants import *
from .models import ObjectChange from .models import ObjectChange
from .signals import purge_changelog from .signals import purge_changelog
from .webhooks import enqueue_webhooks from .webhooks import enqueue_webhooks
@@ -24,7 +23,7 @@ def handle_changed_object(sender, instance, **kwargs):
Fires when an object is created or updated. Fires when an object is created or updated.
""" """
# Queue the object for processing once the request completes # Queue the object for processing once the request completes
action = ObjectChangeActionChoices.ACTION_CREATE if kwargs['created'] else ObjectChangeActionChoices.ACTION_UPDATE action = OBJECTCHANGE_ACTION_CREATE if kwargs['created'] else OBJECTCHANGE_ACTION_UPDATE
_thread_locals.changed_objects.append( _thread_locals.changed_objects.append(
(instance, action) (instance, action)
) )
@@ -42,12 +41,12 @@ def handle_deleted_object(sender, instance, **kwargs):
copy = deepcopy(instance) copy = deepcopy(instance)
# Preserve tags # Preserve tags
if is_taggable(instance): if hasattr(instance, 'tags'):
copy.tags = DummyQuerySet(instance.tags.all()) copy.tags = DummyQuerySet(instance.tags.all())
# Queue the copy of the object for processing once the request completes # Queue the copy of the object for processing once the request completes
_thread_locals.changed_objects.append( _thread_locals.changed_objects.append(
(copy, ObjectChangeActionChoices.ACTION_DELETE) (copy, OBJECTCHANGE_ACTION_DELETE)
) )
@@ -102,7 +101,7 @@ class ObjectChangeMiddleware(object):
for instance, action in _thread_locals.changed_objects: for instance, action in _thread_locals.changed_objects:
# Refresh cached custom field values # Refresh cached custom field values
if action in [ObjectChangeActionChoices.ACTION_CREATE, ObjectChangeActionChoices.ACTION_UPDATE]: if action in [OBJECTCHANGE_ACTION_CREATE, OBJECTCHANGE_ACTION_UPDATE]:
if hasattr(instance, 'cache_custom_fields'): if hasattr(instance, 'cache_custom_fields'):
instance.cache_custom_fields() instance.cache_custom_fields()
@@ -117,11 +116,11 @@ class ObjectChangeMiddleware(object):
enqueue_webhooks(instance, request.user, request.id, action) enqueue_webhooks(instance, request.user, request.id, action)
# Increment metric counters # Increment metric counters
if action == ObjectChangeActionChoices.ACTION_CREATE: if action == OBJECTCHANGE_ACTION_CREATE:
model_inserts.labels(instance._meta.model_name).inc() model_inserts.labels(instance._meta.model_name).inc()
elif action == ObjectChangeActionChoices.ACTION_UPDATE: elif action == OBJECTCHANGE_ACTION_UPDATE:
model_updates.labels(instance._meta.model_name).inc() model_updates.labels(instance._meta.model_name).inc()
elif action == ObjectChangeActionChoices.ACTION_DELETE: elif action == OBJECTCHANGE_ACTION_DELETE:
model_deletes.labels(instance._meta.model_name).inc() model_deletes.labels(instance._meta.model_name).inc()
# Housekeeping: 1% chance of clearing out expired ObjectChanges. This applies only to requests which result in # Housekeeping: 1% chance of clearing out expired ObjectChanges. This applies only to requests which result in

View File

@@ -8,6 +8,8 @@ import django.db.models.deletion
import extras.models import extras.models
from django.db.utils import OperationalError from django.db.utils import OperationalError
from extras.constants import CF_FILTER_DISABLED, CF_FILTER_EXACT, CF_FILTER_LOOSE, CF_TYPE_SELECT
def verify_postgresql_version(apps, schema_editor): def verify_postgresql_version(apps, schema_editor):
""" """

View File

@@ -2,19 +2,21 @@
# Generated by Django 1.11.9 on 2018-02-21 19:48 # Generated by Django 1.11.9 on 2018-02-21 19:48
from django.db import migrations, models from django.db import migrations, models
from extras.constants import CF_FILTER_DISABLED, CF_FILTER_EXACT, CF_FILTER_LOOSE, CF_TYPE_SELECT
def is_filterable_to_filter_logic(apps, schema_editor): def is_filterable_to_filter_logic(apps, schema_editor):
CustomField = apps.get_model('extras', 'CustomField') CustomField = apps.get_model('extras', 'CustomField')
CustomField.objects.filter(is_filterable=False).update(filter_logic=0) CustomField.objects.filter(is_filterable=False).update(filter_logic=CF_FILTER_DISABLED)
CustomField.objects.filter(is_filterable=True).update(filter_logic=1) CustomField.objects.filter(is_filterable=True).update(filter_logic=CF_FILTER_LOOSE)
# Select fields match on primary key only # Select fields match on primary key only
CustomField.objects.filter(is_filterable=True, type=600).update(filter_logic=2) CustomField.objects.filter(is_filterable=True, type=CF_TYPE_SELECT).update(filter_logic=CF_FILTER_EXACT)
def filter_logic_to_is_filterable(apps, schema_editor): def filter_logic_to_is_filterable(apps, schema_editor):
CustomField = apps.get_model('extras', 'CustomField') CustomField = apps.get_model('extras', 'CustomField')
CustomField.objects.filter(filter_logic=0).update(is_filterable=False) CustomField.objects.filter(filter_logic=CF_FILTER_DISABLED).update(is_filterable=False)
CustomField.objects.exclude(filter_logic=0).update(is_filterable=True) CustomField.objects.exclude(filter_logic=CF_FILTER_DISABLED).update(is_filterable=True)
class Migration(migrations.Migration): class Migration(migrations.Migration):

View File

@@ -1,16 +0,0 @@
# 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',
),
]

View File

@@ -1,69 +0,0 @@
from django.db import migrations, models
import django.db.models.deletion
CUSTOMFIELD_TYPE_CHOICES = (
(100, 'text'),
(200, 'integer'),
(300, 'boolean'),
(400, 'date'),
(500, 'url'),
(600, 'select')
)
CUSTOMFIELD_FILTER_LOGIC_CHOICES = (
(0, 'disabled'),
(1, 'integer'),
(2, 'exact'),
)
def customfield_type_to_slug(apps, schema_editor):
CustomField = apps.get_model('extras', 'CustomField')
for id, slug in CUSTOMFIELD_TYPE_CHOICES:
CustomField.objects.filter(type=str(id)).update(type=slug)
def customfield_filter_logic_to_slug(apps, schema_editor):
CustomField = apps.get_model('extras', 'CustomField')
for id, slug in CUSTOMFIELD_FILTER_LOGIC_CHOICES:
CustomField.objects.filter(filter_logic=str(id)).update(filter_logic=slug)
class Migration(migrations.Migration):
atomic = False
dependencies = [
('extras', '0028_remove_topology_maps'),
]
operations = [
# CustomField.type
migrations.AlterField(
model_name='customfield',
name='type',
field=models.CharField(default='text', max_length=50),
),
migrations.RunPython(
code=customfield_type_to_slug
),
# Update CustomFieldChoice.field.limit_choices_to
migrations.AlterField(
model_name='customfieldchoice',
name='field',
field=models.ForeignKey(limit_choices_to={'type': 'select'}, on_delete=django.db.models.deletion.CASCADE, related_name='choices', to='extras.CustomField'),
),
# CustomField.filter_logic
migrations.AlterField(
model_name='customfield',
name='filter_logic',
field=models.CharField(default='loose', max_length=50),
),
migrations.RunPython(
code=customfield_filter_logic_to_slug
),
]

View File

@@ -1,36 +0,0 @@
from django.db import migrations, models
OBJECTCHANGE_ACTION_CHOICES = (
(1, 'create'),
(2, 'update'),
(3, 'delete'),
)
def objectchange_action_to_slug(apps, schema_editor):
ObjectChange = apps.get_model('extras', 'ObjectChange')
for id, slug in OBJECTCHANGE_ACTION_CHOICES:
ObjectChange.objects.filter(action=str(id)).update(action=slug)
class Migration(migrations.Migration):
atomic = False
dependencies = [
('extras', '0029_3569_customfield_fields'),
]
operations = [
# ObjectChange.action
migrations.AlterField(
model_name='objectchange',
name='action',
field=models.CharField(max_length=50),
),
migrations.RunPython(
code=objectchange_action_to_slug
),
]

View File

@@ -1,35 +0,0 @@
from django.db import migrations, models
EXPORTTEMPLATE_LANGUAGE_CHOICES = (
(10, 'django'),
(20, 'jinja2'),
)
def exporttemplate_language_to_slug(apps, schema_editor):
ExportTemplate = apps.get_model('extras', 'ExportTemplate')
for id, slug in EXPORTTEMPLATE_LANGUAGE_CHOICES:
ExportTemplate.objects.filter(template_language=str(id)).update(template_language=slug)
class Migration(migrations.Migration):
atomic = False
dependencies = [
('extras', '0030_3569_objectchange_fields'),
]
operations = [
# ExportTemplate.template_language
migrations.AlterField(
model_name='exporttemplate',
name='template_language',
field=models.CharField(default='jinja2', max_length=50),
),
migrations.RunPython(
code=exporttemplate_language_to_slug
),
]

View File

@@ -1,35 +0,0 @@
from django.db import migrations, models
WEBHOOK_CONTENTTYPE_CHOICES = (
(1, 'application/json'),
(2, 'application/x-www-form-urlencoded'),
)
def webhook_contenttype_to_slug(apps, schema_editor):
Webhook = apps.get_model('extras', 'Webhook')
for id, slug in WEBHOOK_CONTENTTYPE_CHOICES:
Webhook.objects.filter(http_content_type=str(id)).update(http_content_type=slug)
class Migration(migrations.Migration):
atomic = False
dependencies = [
('extras', '0031_3569_exporttemplate_fields'),
]
operations = [
# Webhook.http_content_type
migrations.AlterField(
model_name='webhook',
name='http_content_type',
field=models.CharField(default='application/json', max_length=50),
),
migrations.RunPython(
code=webhook_contenttype_to_slug
),
]

View File

@@ -1,46 +0,0 @@
from django.db import migrations, models
import django.db.models.deletion
GRAPH_TYPE_CHOICES = (
(100, 'dcim', 'interface'),
(150, 'dcim', 'device'),
(200, 'circuits', 'provider'),
(300, 'dcim', 'site'),
)
def graph_type_to_fk(apps, schema_editor):
Graph = apps.get_model('extras', 'Graph')
ContentType = apps.get_model('contenttypes', 'ContentType')
# On a new installation (and during tests) content types might not yet exist. So, we only perform the bulk
# updates if a Graph has been created, which implies that we're working with a populated database.
if Graph.objects.exists():
for id, app_label, model in GRAPH_TYPE_CHOICES:
content_type = ContentType.objects.get(app_label=app_label, model=model)
Graph.objects.filter(type=id).update(type=content_type.pk)
class Migration(migrations.Migration):
dependencies = [
('extras', '0032_3569_webhook_fields'),
]
operations = [
# We have to swap the legacy IDs to ContentType PKs *before* we alter the field, to avoid triggering an
# IntegrityError on the ForeignKey.
migrations.RunPython(
code=graph_type_to_fk
),
migrations.AlterField(
model_name='graph',
name='type',
field=models.ForeignKey(
limit_choices_to={'model__in': ['device', 'interface', 'provider', 'site']},
on_delete=django.db.models.deletion.CASCADE,
to='contenttypes.ContentType'
),
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 2.2.6 on 2019-12-11 09:17
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('extras', '0033_graph_type_to_fk'),
]
operations = [
migrations.AddField(
model_name='configcontext',
name='tags',
field=models.ManyToManyField(blank=True, related_name='_configcontext_tags_+', to='extras.Tag'),
),
]

View File

@@ -1,21 +1,23 @@
from collections import OrderedDict from collections import OrderedDict
from datetime import date from datetime import date
import graphviz
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.contrib.postgres.fields import JSONField from django.contrib.postgres.fields import JSONField
from django.core.validators import ValidationError from django.core.validators import ValidationError
from django.db import models from django.db import models
from django.db.models import F, Q
from django.http import HttpResponse from django.http import HttpResponse
from django.template import Template, Context from django.template import Template, Context
from django.urls import reverse from django.urls import reverse
from jinja2 import Environment from jinja2 import Environment
from taggit.models import TagBase, GenericTaggedItemBase from taggit.models import TagBase, GenericTaggedItemBase
from dcim.constants import CONNECTION_STATUS_CONNECTED
from utilities.fields import ColorField from utilities.fields import ColorField
from utilities.utils import deepmerge, model_names_to_filter_dict from utilities.utils import deepmerge, foreground_color, model_names_to_filter_dict
from .choices import *
from .constants import * from .constants import *
from .querysets import ConfigContextQuerySet from .querysets import ConfigContextQuerySet
@@ -63,10 +65,9 @@ class Webhook(models.Model):
verbose_name='URL', verbose_name='URL',
help_text="A POST will be sent to this URL when the webhook is called." help_text="A POST will be sent to this URL when the webhook is called."
) )
http_content_type = models.CharField( http_content_type = models.PositiveSmallIntegerField(
max_length=50, choices=WEBHOOK_CT_CHOICES,
choices=WebhookContentTypeChoices, default=WEBHOOK_CT_JSON,
default=WebhookContentTypeChoices.CONTENTTYPE_JSON,
verbose_name='HTTP content type' verbose_name='HTTP content type'
) )
additional_headers = JSONField( additional_headers = JSONField(
@@ -184,10 +185,9 @@ class CustomField(models.Model):
limit_choices_to=get_custom_field_models, limit_choices_to=get_custom_field_models,
help_text='The object(s) to which this field applies.' help_text='The object(s) to which this field applies.'
) )
type = models.CharField( type = models.PositiveSmallIntegerField(
max_length=50, choices=CUSTOMFIELD_TYPE_CHOICES,
choices=CustomFieldTypeChoices, default=CF_TYPE_TEXT
default=CustomFieldTypeChoices.TYPE_TEXT
) )
name = models.CharField( name = models.CharField(
max_length=50, max_length=50,
@@ -208,10 +208,9 @@ class CustomField(models.Model):
help_text='If true, this field is required when creating new objects ' help_text='If true, this field is required when creating new objects '
'or editing an existing object.' 'or editing an existing object.'
) )
filter_logic = models.CharField( filter_logic = models.PositiveSmallIntegerField(
max_length=50, choices=CF_FILTER_CHOICES,
choices=CustomFieldFilterLogicChoices, default=CF_FILTER_LOOSE,
default=CustomFieldFilterLogicChoices.FILTER_LOOSE,
help_text='Loose matches any instance of a given string; exact ' help_text='Loose matches any instance of a given string; exact '
'matches the entire field.' 'matches the entire field.'
) )
@@ -237,15 +236,15 @@ class CustomField(models.Model):
""" """
if value is None: if value is None:
return '' return ''
if self.type == CustomFieldTypeChoices.TYPE_BOOLEAN: if self.type == CF_TYPE_BOOLEAN:
return str(int(bool(value))) return str(int(bool(value)))
if self.type == CustomFieldTypeChoices.TYPE_DATE: if self.type == CF_TYPE_DATE:
# Could be date/datetime object or string # Could be date/datetime object or string
try: try:
return value.strftime('%Y-%m-%d') return value.strftime('%Y-%m-%d')
except AttributeError: except AttributeError:
return value return value
if self.type == CustomFieldTypeChoices.TYPE_SELECT: if self.type == CF_TYPE_SELECT:
# Could be ModelChoiceField or TypedChoiceField # Could be ModelChoiceField or TypedChoiceField
return str(value.id) if hasattr(value, 'id') else str(value) return str(value.id) if hasattr(value, 'id') else str(value)
return value return value
@@ -256,14 +255,14 @@ class CustomField(models.Model):
""" """
if serialized_value == '': if serialized_value == '':
return None return None
if self.type == CustomFieldTypeChoices.TYPE_INTEGER: if self.type == CF_TYPE_INTEGER:
return int(serialized_value) return int(serialized_value)
if self.type == CustomFieldTypeChoices.TYPE_BOOLEAN: if self.type == CF_TYPE_BOOLEAN:
return bool(int(serialized_value)) return bool(int(serialized_value))
if self.type == CustomFieldTypeChoices.TYPE_DATE: if self.type == CF_TYPE_DATE:
# Read date as YYYY-MM-DD # Read date as YYYY-MM-DD
return date(*[int(n) for n in serialized_value.split('-')]) return date(*[int(n) for n in serialized_value.split('-')])
if self.type == CustomFieldTypeChoices.TYPE_SELECT: if self.type == CF_TYPE_SELECT:
return self.choices.get(pk=int(serialized_value)) return self.choices.get(pk=int(serialized_value))
return serialized_value return serialized_value
@@ -316,7 +315,7 @@ class CustomFieldChoice(models.Model):
to='extras.CustomField', to='extras.CustomField',
on_delete=models.CASCADE, on_delete=models.CASCADE,
related_name='choices', related_name='choices',
limit_choices_to={'type': CustomFieldTypeChoices.TYPE_SELECT} limit_choices_to={'type': CF_TYPE_SELECT}
) )
value = models.CharField( value = models.CharField(
max_length=100 max_length=100
@@ -334,17 +333,14 @@ class CustomFieldChoice(models.Model):
return self.value return self.value
def clean(self): def clean(self):
if self.field.type != CustomFieldTypeChoices.TYPE_SELECT: if self.field.type != CF_TYPE_SELECT:
raise ValidationError("Custom field choices can only be assigned to selection fields.") raise ValidationError("Custom field choices can only be assigned to selection fields.")
def delete(self, using=None, keep_parents=False): def delete(self, using=None, keep_parents=False):
# When deleting a CustomFieldChoice, delete all CustomFieldValues which point to it # When deleting a CustomFieldChoice, delete all CustomFieldValues which point to it
pk = self.pk pk = self.pk
super().delete(using, keep_parents) super().delete(using, keep_parents)
CustomFieldValue.objects.filter( CustomFieldValue.objects.filter(field__type=CF_TYPE_SELECT, serialized_value=str(pk)).delete()
field__type=CustomFieldTypeChoices.TYPE_SELECT,
serialized_value=str(pk)
).delete()
# #
@@ -388,8 +384,8 @@ class CustomLink(models.Model):
) )
button_class = models.CharField( button_class = models.CharField(
max_length=30, max_length=30,
choices=CustomLinkButtonClassChoices, choices=BUTTON_CLASS_CHOICES,
default=CustomLinkButtonClassChoices.CLASS_DEFAULT, default=BUTTON_CLASS_DEFAULT,
help_text="The class of the first link in a group will be used for the dropdown button" help_text="The class of the first link in a group will be used for the dropdown button"
) )
new_window = models.BooleanField( new_window = models.BooleanField(
@@ -408,12 +404,8 @@ class CustomLink(models.Model):
# #
class Graph(models.Model): class Graph(models.Model):
type = models.ForeignKey( type = models.PositiveSmallIntegerField(
to=ContentType, choices=GRAPH_TYPE_CHOICES
on_delete=models.CASCADE,
limit_choices_to={
'model__in': ['device', 'interface', 'provider', 'site']
}
) )
weight = models.PositiveSmallIntegerField( weight = models.PositiveSmallIntegerField(
default=1000 default=1000
@@ -469,10 +461,9 @@ class ExportTemplate(models.Model):
max_length=200, max_length=200,
blank=True blank=True
) )
template_language = models.CharField( template_language = models.PositiveSmallIntegerField(
max_length=50, choices=TEMPLATE_LANGUAGE_CHOICES,
choices=ExportTemplateLanguageChoices, default=TEMPLATE_LANGUAGE_JINJA2
default=ExportTemplateLanguageChoices.LANGUAGE_JINJA2
) )
template_code = models.TextField( template_code = models.TextField(
help_text='The list of objects being exported is passed as a context variable named <code>queryset</code>.' help_text='The list of objects being exported is passed as a context variable named <code>queryset</code>.'
@@ -506,11 +497,11 @@ class ExportTemplate(models.Model):
'queryset': queryset 'queryset': queryset
} }
if self.template_language == ExportTemplateLanguageChoices.LANGUAGE_DJANGO: if self.template_language == TEMPLATE_LANGUAGE_DJANGO:
template = Template(self.template_code) template = Template(self.template_code)
output = template.render(Context(context)) output = template.render(Context(context))
elif self.template_language == ExportTemplateLanguageChoices.LANGUAGE_JINJA2: elif self.template_language == TEMPLATE_LANGUAGE_JINJA2:
template = Environment().from_string(source=self.template_code) template = Environment().from_string(source=self.template_code)
output = template.render(**context) output = template.render(**context)
@@ -540,6 +531,154 @@ class ExportTemplate(models.Model):
return response 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 # Image attachments
# #
@@ -611,20 +750,11 @@ class ImageAttachment(models.Model):
@property @property
def size(self): def size(self):
""" """
Wrapper around `image.size` to suppress an OSError in case the file is inaccessible. Also opportunistically Wrapper around `image.size` to suppress an OSError in case the file is inaccessible.
catch other exceptions that we know other storage back-ends to throw.
""" """
expected_exceptions = [OSError]
try:
from botocore.exceptions import ClientError
expected_exceptions.append(ClientError)
except ImportError:
pass
try: try:
return self.image.size return self.image.size
except tuple(expected_exceptions): except OSError:
return None return None
@@ -682,11 +812,6 @@ class ConfigContext(models.Model):
related_name='+', related_name='+',
blank=True blank=True
) )
tags = models.ManyToManyField(
to='extras.Tag',
related_name='+',
blank=True
)
data = JSONField() data = JSONField()
objects = ConfigContextQuerySet.as_manager() objects = ConfigContextQuerySet.as_manager()
@@ -822,9 +947,8 @@ class ObjectChange(models.Model):
request_id = models.UUIDField( request_id = models.UUIDField(
editable=False editable=False
) )
action = models.CharField( action = models.PositiveSmallIntegerField(
max_length=50, choices=OBJECTCHANGE_ACTION_CHOICES
choices=ObjectChangeActionChoices
) )
changed_object_type = models.ForeignKey( changed_object_type = models.ForeignKey(
to=ContentType, to=ContentType,

View File

@@ -46,6 +46,5 @@ class ConfigContextQuerySet(QuerySet):
Q(platforms=obj.platform) | Q(platforms=None), Q(platforms=obj.platform) | Q(platforms=None),
Q(tenant_groups=tenant_group) | Q(tenant_groups=None), Q(tenant_groups=tenant_group) | Q(tenant_groups=None),
Q(tenants=obj.tenant) | Q(tenants=None), Q(tenants=obj.tenant) | Q(tenants=None),
Q(tags__slug__in=obj.tags.slugs()) | Q(tags=None),
is_active=True, is_active=True,
).order_by('weight', 'name') ).order_by('weight', 'name')

View File

@@ -242,21 +242,16 @@ class BaseScript:
def __str__(self): def __str__(self):
return getattr(self.Meta, 'name', self.__class__.__name__) return getattr(self.Meta, 'name', self.__class__.__name__)
@classmethod def _get_vars(self):
def module(cls):
return cls.__module__
@classmethod
def _get_vars(cls):
vars = OrderedDict() vars = OrderedDict()
# Infer order from Meta.field_order (Python 3.5 and lower) # Infer order from Meta.field_order (Python 3.5 and lower)
field_order = getattr(cls.Meta, 'field_order', []) field_order = getattr(self.Meta, 'field_order', [])
for name in field_order: for name in field_order:
vars[name] = getattr(cls, name) vars[name] = getattr(self, name)
# Default to order of declaration on class # Default to order of declaration on class
for name, attr in cls.__dict__.items(): for name, attr in self.__class__.__dict__.items():
if name not in vars and issubclass(attr.__class__, ScriptVariable): if name not in vars and issubclass(attr.__class__, ScriptVariable):
vars[name] = attr vars[name] = attr
@@ -387,18 +382,14 @@ def run_script(script, data, files, commit=True):
return output, execution_time return output, execution_time
def get_scripts(use_names=False): def get_scripts():
"""
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() scripts = OrderedDict()
# Iterate through all modules within the reports path. These are the user-created files in which reports are # Iterate through all modules within the reports path. These are the user-created files in which reports are
# defined. # defined.
for importer, module_name, _ in pkgutil.iter_modules([settings.SCRIPTS_ROOT]): for importer, module_name, _ in pkgutil.iter_modules([settings.SCRIPTS_ROOT]):
module = importer.find_module(module_name).load_module(module_name) module = importer.find_module(module_name).load_module(module_name)
if use_names and hasattr(module, 'name'): if hasattr(module, 'name'):
module_name = module.name module_name = module.name
module_scripts = OrderedDict() module_scripts = OrderedDict()
for name, cls in inspect.getmembers(module, is_script): for name, cls in inspect.getmembers(module, is_script):
@@ -406,13 +397,3 @@ def get_scripts(use_names=False):
scripts[module_name] = module_scripts scripts[module_name] = module_scripts
return 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)

View File

@@ -38,11 +38,11 @@ OBJECTCHANGE_TIME = """
""" """
OBJECTCHANGE_ACTION = """ OBJECTCHANGE_ACTION = """
{% if record.action == 'create' %} {% if record.action == 1 %}
<span class="label label-success">Created</span> <span class="label label-success">Created</span>
{% elif record.action == 'update' %} {% elif record.action == 2 %}
<span class="label label-primary">Updated</span> <span class="label label-primary">Updated</span>
{% elif record.action == 'delete' %} {% elif record.action == 3 %}
<span class="label label-danger">Deleted</span> <span class="label label-danger">Deleted</span>
{% endif %} {% endif %}
""" """

View File

@@ -3,9 +3,8 @@ from django.urls import reverse
from rest_framework import status from rest_framework import status
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Platform, Region, Site 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.models import ConfigContext, Graph, ExportTemplate, Tag
from extras.scripts import BooleanVar, IntegerVar, Script, StringVar
from tenancy.models import Tenant, TenantGroup from tenancy.models import Tenant, TenantGroup
from utilities.testing import APITestCase from utilities.testing import APITestCase
@@ -16,21 +15,14 @@ class GraphTest(APITestCase):
super().setUp() super().setUp()
site_ct = ContentType.objects.get_for_model(Site)
self.graph1 = Graph.objects.create( self.graph1 = Graph.objects.create(
type=site_ct, type=GRAPH_TYPE_SITE, name='Test Graph 1', source='http://example.com/graphs.py?site={{ obj.name }}&foo=1'
name='Test Graph 1',
source='http://example.com/graphs.py?site={{ obj.name }}&foo=1'
) )
self.graph2 = Graph.objects.create( self.graph2 = Graph.objects.create(
type=site_ct, type=GRAPH_TYPE_SITE, name='Test Graph 2', source='http://example.com/graphs.py?site={{ obj.name }}&foo=2'
name='Test Graph 2',
source='http://example.com/graphs.py?site={{ obj.name }}&foo=2'
) )
self.graph3 = Graph.objects.create( self.graph3 = Graph.objects.create(
type=site_ct, type=GRAPH_TYPE_SITE, name='Test Graph 3', source='http://example.com/graphs.py?site={{ obj.name }}&foo=3'
name='Test Graph 3',
source='http://example.com/graphs.py?site={{ obj.name }}&foo=3'
) )
def test_get_graph(self): def test_get_graph(self):
@@ -50,7 +42,7 @@ class GraphTest(APITestCase):
def test_create_graph(self): def test_create_graph(self):
data = { data = {
'type': 'dcim.site', 'type': GRAPH_TYPE_SITE,
'name': 'Test Graph 4', 'name': 'Test Graph 4',
'source': 'http://example.com/graphs.py?site={{ obj.name }}&foo=4', 'source': 'http://example.com/graphs.py?site={{ obj.name }}&foo=4',
} }
@@ -61,7 +53,7 @@ class GraphTest(APITestCase):
self.assertHttpStatus(response, status.HTTP_201_CREATED) self.assertHttpStatus(response, status.HTTP_201_CREATED)
self.assertEqual(Graph.objects.count(), 4) self.assertEqual(Graph.objects.count(), 4)
graph4 = Graph.objects.get(pk=response.data['id']) graph4 = Graph.objects.get(pk=response.data['id'])
self.assertEqual(graph4.type, ContentType.objects.get_for_model(Site)) self.assertEqual(graph4.type, data['type'])
self.assertEqual(graph4.name, data['name']) self.assertEqual(graph4.name, data['name'])
self.assertEqual(graph4.source, data['source']) self.assertEqual(graph4.source, data['source'])
@@ -69,17 +61,17 @@ class GraphTest(APITestCase):
data = [ data = [
{ {
'type': 'dcim.site', 'type': GRAPH_TYPE_SITE,
'name': 'Test Graph 4', 'name': 'Test Graph 4',
'source': 'http://example.com/graphs.py?site={{ obj.name }}&foo=4', 'source': 'http://example.com/graphs.py?site={{ obj.name }}&foo=4',
}, },
{ {
'type': 'dcim.site', 'type': GRAPH_TYPE_SITE,
'name': 'Test Graph 5', 'name': 'Test Graph 5',
'source': 'http://example.com/graphs.py?site={{ obj.name }}&foo=5', 'source': 'http://example.com/graphs.py?site={{ obj.name }}&foo=5',
}, },
{ {
'type': 'dcim.site', 'type': GRAPH_TYPE_SITE,
'name': 'Test Graph 6', 'name': 'Test Graph 6',
'source': 'http://example.com/graphs.py?site={{ obj.name }}&foo=6', 'source': 'http://example.com/graphs.py?site={{ obj.name }}&foo=6',
}, },
@@ -97,7 +89,7 @@ class GraphTest(APITestCase):
def test_update_graph(self): def test_update_graph(self):
data = { data = {
'type': 'dcim.site', 'type': GRAPH_TYPE_SITE,
'name': 'Test Graph X', 'name': 'Test Graph X',
'source': 'http://example.com/graphs.py?site={{ obj.name }}&foo=99', 'source': 'http://example.com/graphs.py?site={{ obj.name }}&foo=99',
} }
@@ -108,7 +100,7 @@ class GraphTest(APITestCase):
self.assertHttpStatus(response, status.HTTP_200_OK) self.assertHttpStatus(response, status.HTTP_200_OK)
self.assertEqual(Graph.objects.count(), 3) self.assertEqual(Graph.objects.count(), 3)
graph1 = Graph.objects.get(pk=response.data['id']) graph1 = Graph.objects.get(pk=response.data['id'])
self.assertEqual(graph1.type, ContentType.objects.get_for_model(Site)) self.assertEqual(graph1.type, data['type'])
self.assertEqual(graph1.name, data['name']) self.assertEqual(graph1.name, data['name'])
self.assertEqual(graph1.source, data['source']) self.assertEqual(graph1.source, data['source'])
@@ -370,8 +362,6 @@ class ConfigContextTest(APITestCase):
tenantgroup2 = TenantGroup.objects.create(name='Test Tenant Group 2', slug='test-tenant-group-2') tenantgroup2 = TenantGroup.objects.create(name='Test Tenant Group 2', slug='test-tenant-group-2')
tenant1 = Tenant.objects.create(name='Test Tenant 1', slug='test-tenant-1') tenant1 = Tenant.objects.create(name='Test Tenant 1', slug='test-tenant-1')
tenant2 = Tenant.objects.create(name='Test Tenant 2', slug='test-tenant-2') tenant2 = Tenant.objects.create(name='Test Tenant 2', slug='test-tenant-2')
tag1 = Tag.objects.create(name='Test Tag 1', slug='test-tag-1')
tag2 = Tag.objects.create(name='Test Tag 2', slug='test-tag-2')
data = { data = {
'name': 'Test Config Context 4', 'name': 'Test Config Context 4',
@@ -382,7 +372,6 @@ class ConfigContextTest(APITestCase):
'platforms': [platform1.pk, platform2.pk], 'platforms': [platform1.pk, platform2.pk],
'tenant_groups': [tenantgroup1.pk, tenantgroup2.pk], 'tenant_groups': [tenantgroup1.pk, tenantgroup2.pk],
'tenants': [tenant1.pk, tenant2.pk], 'tenants': [tenant1.pk, tenant2.pk],
'tags': [tag1.slug, tag2.slug],
'data': {'foo': 'XXX'} 'data': {'foo': 'XXX'}
} }
@@ -405,8 +394,6 @@ class ConfigContextTest(APITestCase):
self.assertEqual(tenantgroup2.pk, data['tenant_groups'][1]) self.assertEqual(tenantgroup2.pk, data['tenant_groups'][1])
self.assertEqual(tenant1.pk, data['tenants'][0]) self.assertEqual(tenant1.pk, data['tenants'][0])
self.assertEqual(tenant2.pk, data['tenants'][1]) self.assertEqual(tenant2.pk, data['tenants'][1])
self.assertEqual(tag1.slug, data['tags'][0])
self.assertEqual(tag2.slug, data['tags'][1])
self.assertEqual(configcontext4.data, data['data']) self.assertEqual(configcontext4.data, data['data'])
def test_create_configcontext_bulk(self): def test_create_configcontext_bulk(self):
@@ -533,68 +520,3 @@ class ConfigContextTest(APITestCase):
configcontext6.sites.add(site2) configcontext6.sites.add(site2)
rendered_context = device.get_config_context() rendered_context = device.get_config_context()
self.assertEqual(rendered_context['bar'], 456) 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')

View File

@@ -3,7 +3,6 @@ from django.urls import reverse
from rest_framework import status from rest_framework import status
from dcim.models import Site from dcim.models import Site
from extras.choices import *
from extras.constants import * from extras.constants import *
from extras.models import CustomField, CustomFieldValue, ObjectChange from extras.models import CustomField, CustomFieldValue, ObjectChange
from utilities.testing import APITestCase from utilities.testing import APITestCase
@@ -18,7 +17,7 @@ class ChangeLogTest(APITestCase):
# Create a custom field on the Site model # Create a custom field on the Site model
ct = ContentType.objects.get_for_model(Site) ct = ContentType.objects.get_for_model(Site)
cf = CustomField( cf = CustomField(
type=CustomFieldTypeChoices.TYPE_TEXT, type=CF_TYPE_TEXT,
name='my_field', name='my_field',
required=False required=False
) )
@@ -50,7 +49,7 @@ class ChangeLogTest(APITestCase):
changed_object_id=site.pk changed_object_id=site.pk
) )
self.assertEqual(oc.changed_object, site) self.assertEqual(oc.changed_object, site)
self.assertEqual(oc.action, ObjectChangeActionChoices.ACTION_CREATE) self.assertEqual(oc.action, OBJECTCHANGE_ACTION_CREATE)
self.assertEqual(oc.object_data['custom_fields'], data['custom_fields']) self.assertEqual(oc.object_data['custom_fields'], data['custom_fields'])
self.assertListEqual(sorted(oc.object_data['tags']), data['tags']) self.assertListEqual(sorted(oc.object_data['tags']), data['tags'])
@@ -82,7 +81,7 @@ class ChangeLogTest(APITestCase):
changed_object_id=site.pk changed_object_id=site.pk
) )
self.assertEqual(oc.changed_object, site) self.assertEqual(oc.changed_object, site)
self.assertEqual(oc.action, ObjectChangeActionChoices.ACTION_UPDATE) self.assertEqual(oc.action, OBJECTCHANGE_ACTION_UPDATE)
self.assertEqual(oc.object_data['custom_fields'], data['custom_fields']) self.assertEqual(oc.object_data['custom_fields'], data['custom_fields'])
self.assertListEqual(sorted(oc.object_data['tags']), data['tags']) self.assertListEqual(sorted(oc.object_data['tags']), data['tags'])
@@ -111,6 +110,6 @@ class ChangeLogTest(APITestCase):
oc = ObjectChange.objects.first() oc = ObjectChange.objects.first()
self.assertEqual(oc.changed_object, None) self.assertEqual(oc.changed_object, None)
self.assertEqual(oc.object_repr, site.name) self.assertEqual(oc.object_repr, site.name)
self.assertEqual(oc.action, ObjectChangeActionChoices.ACTION_DELETE) self.assertEqual(oc.action, OBJECTCHANGE_ACTION_DELETE)
self.assertEqual(oc.object_data['custom_fields'], {'my_field': 'ABC'}) self.assertEqual(oc.object_data['custom_fields'], {'my_field': 'ABC'})
self.assertListEqual(sorted(oc.object_data['tags']), ['bar', 'foo']) self.assertListEqual(sorted(oc.object_data['tags']), ['bar', 'foo'])

View File

@@ -6,7 +6,7 @@ from django.urls import reverse
from rest_framework import status from rest_framework import status
from dcim.models import Site from dcim.models import Site
from extras.choices import * from extras.constants import CF_TYPE_TEXT, CF_TYPE_INTEGER, CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_SELECT, CF_TYPE_URL, CF_TYPE_SELECT
from extras.models import CustomField, CustomFieldValue, CustomFieldChoice from extras.models import CustomField, CustomFieldValue, CustomFieldChoice
from utilities.testing import APITestCase from utilities.testing import APITestCase
from virtualization.models import VirtualMachine from virtualization.models import VirtualMachine
@@ -25,13 +25,13 @@ class CustomFieldTest(TestCase):
def test_simple_fields(self): def test_simple_fields(self):
DATA = ( DATA = (
{'field_type': CustomFieldTypeChoices.TYPE_TEXT, 'field_value': 'Foobar!', 'empty_value': ''}, {'field_type': CF_TYPE_TEXT, 'field_value': 'Foobar!', 'empty_value': ''},
{'field_type': CustomFieldTypeChoices.TYPE_INTEGER, 'field_value': 0, 'empty_value': None}, {'field_type': CF_TYPE_INTEGER, 'field_value': 0, 'empty_value': None},
{'field_type': CustomFieldTypeChoices.TYPE_INTEGER, 'field_value': 42, 'empty_value': None}, {'field_type': CF_TYPE_INTEGER, 'field_value': 42, 'empty_value': None},
{'field_type': CustomFieldTypeChoices.TYPE_BOOLEAN, 'field_value': True, 'empty_value': None}, {'field_type': CF_TYPE_BOOLEAN, 'field_value': True, 'empty_value': None},
{'field_type': CustomFieldTypeChoices.TYPE_BOOLEAN, 'field_value': False, 'empty_value': None}, {'field_type': CF_TYPE_BOOLEAN, 'field_value': False, 'empty_value': None},
{'field_type': CustomFieldTypeChoices.TYPE_DATE, 'field_value': date(2016, 6, 23), 'empty_value': None}, {'field_type': CF_TYPE_DATE, 'field_value': date(2016, 6, 23), 'empty_value': None},
{'field_type': CustomFieldTypeChoices.TYPE_URL, 'field_value': 'http://example.com/', 'empty_value': ''}, {'field_type': CF_TYPE_URL, 'field_value': 'http://example.com/', 'empty_value': ''},
) )
obj_type = ContentType.objects.get_for_model(Site) obj_type = ContentType.objects.get_for_model(Site)
@@ -67,7 +67,7 @@ class CustomFieldTest(TestCase):
obj_type = ContentType.objects.get_for_model(Site) obj_type = ContentType.objects.get_for_model(Site)
# Create a custom field # Create a custom field
cf = CustomField(type=CustomFieldTypeChoices.TYPE_SELECT, name='my_field', required=False) cf = CustomField(type=CF_TYPE_SELECT, name='my_field', required=False)
cf.save() cf.save()
cf.obj_type.set([obj_type]) cf.obj_type.set([obj_type])
cf.save() cf.save()
@@ -107,37 +107,37 @@ class CustomFieldAPITest(APITestCase):
content_type = ContentType.objects.get_for_model(Site) content_type = ContentType.objects.get_for_model(Site)
# Text custom field # Text custom field
self.cf_text = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='magic_word') self.cf_text = CustomField(type=CF_TYPE_TEXT, name='magic_word')
self.cf_text.save() self.cf_text.save()
self.cf_text.obj_type.set([content_type]) self.cf_text.obj_type.set([content_type])
self.cf_text.save() self.cf_text.save()
# Integer custom field # Integer custom field
self.cf_integer = CustomField(type=CustomFieldTypeChoices.TYPE_INTEGER, name='magic_number') self.cf_integer = CustomField(type=CF_TYPE_INTEGER, name='magic_number')
self.cf_integer.save() self.cf_integer.save()
self.cf_integer.obj_type.set([content_type]) self.cf_integer.obj_type.set([content_type])
self.cf_integer.save() self.cf_integer.save()
# Boolean custom field # Boolean custom field
self.cf_boolean = CustomField(type=CustomFieldTypeChoices.TYPE_BOOLEAN, name='is_magic') self.cf_boolean = CustomField(type=CF_TYPE_BOOLEAN, name='is_magic')
self.cf_boolean.save() self.cf_boolean.save()
self.cf_boolean.obj_type.set([content_type]) self.cf_boolean.obj_type.set([content_type])
self.cf_boolean.save() self.cf_boolean.save()
# Date custom field # Date custom field
self.cf_date = CustomField(type=CustomFieldTypeChoices.TYPE_DATE, name='magic_date') self.cf_date = CustomField(type=CF_TYPE_DATE, name='magic_date')
self.cf_date.save() self.cf_date.save()
self.cf_date.obj_type.set([content_type]) self.cf_date.obj_type.set([content_type])
self.cf_date.save() self.cf_date.save()
# URL custom field # URL custom field
self.cf_url = CustomField(type=CustomFieldTypeChoices.TYPE_URL, name='magic_url') self.cf_url = CustomField(type=CF_TYPE_URL, name='magic_url')
self.cf_url.save() self.cf_url.save()
self.cf_url.obj_type.set([content_type]) self.cf_url.obj_type.set([content_type])
self.cf_url.save() self.cf_url.save()
# Select custom field # Select custom field
self.cf_select = CustomField(type=CustomFieldTypeChoices.TYPE_SELECT, name='magic_choice') self.cf_select = CustomField(type=CF_TYPE_SELECT, name='magic_choice')
self.cf_select.save() self.cf_select.save()
self.cf_select.obj_type.set([content_type]) self.cf_select.obj_type.set([content_type])
self.cf_select.save() self.cf_select.save()
@@ -308,8 +308,8 @@ class CustomFieldChoiceAPITest(APITestCase):
vm_content_type = ContentType.objects.get_for_model(VirtualMachine) vm_content_type = ContentType.objects.get_for_model(VirtualMachine)
self.cf_1 = CustomField.objects.create(name="cf_1", type=CustomFieldTypeChoices.TYPE_SELECT) self.cf_1 = CustomField.objects.create(name="cf_1", type=CF_TYPE_SELECT)
self.cf_2 = CustomField.objects.create(name="cf_2", type=CustomFieldTypeChoices.TYPE_SELECT) self.cf_2 = CustomField.objects.create(name="cf_2", type=CF_TYPE_SELECT)
self.cf_choice_1 = CustomFieldChoice.objects.create(field=self.cf_1, value="cf_field_1", weight=100) self.cf_choice_1 = CustomFieldChoice.objects.create(field=self.cf_1, value="cf_field_1", weight=100)
self.cf_choice_2 = CustomFieldChoice.objects.create(field=self.cf_1, value="cf_field_2", weight=50) self.cf_choice_2 = CustomFieldChoice.objects.create(field=self.cf_1, value="cf_field_2", weight=50)

View File

@@ -6,7 +6,7 @@ from django.test import Client, TestCase
from django.urls import reverse from django.urls import reverse
from dcim.models import Site from dcim.models import Site
from extras.choices import ObjectChangeActionChoices from extras.constants import OBJECTCHANGE_ACTION_UPDATE
from extras.models import ConfigContext, ObjectChange, Tag from extras.models import ConfigContext, ObjectChange, Tag
from utilities.testing import create_test_user from utilities.testing import create_test_user
@@ -83,7 +83,7 @@ class ObjectChangeTestCase(TestCase):
# Create three ObjectChanges # Create three ObjectChanges
for i in range(1, 4): for i in range(1, 4):
oc = site.to_objectchange(action=ObjectChangeActionChoices.ACTION_UPDATE) oc = site.to_objectchange(action=OBJECTCHANGE_ACTION_UPDATE)
oc.user = user oc.user = user
oc.request_id = uuid.uuid4() oc.request_id = uuid.uuid4()
oc.save() oc.save()

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