diff --git a/.github/lock.yml b/.github/lock.yml index 36a41b04e..e00f3f4db 100644 --- a/.github/lock.yml +++ b/.github/lock.yml @@ -5,7 +5,7 @@ daysUntilLock: 90 # Skip issues and pull requests created before a given timestamp. Timestamp must # follow ISO 8601 (`YYYY-MM-DD`). Set to `false` to disable -skipCreatedBefore: 2020-01-01 +skipCreatedBefore: false # Issues and pull requests with these labels will be ignored. Set to `[]` to disable exemptLabels: [] diff --git a/.gitignore b/.gitignore index 36c6d3fa8..66a8b13e8 100644 --- a/.gitignore +++ b/.gitignore @@ -12,5 +12,9 @@ fabfile.py *.swp gunicorn_config.py +gunicorn.py +netbox.log +netbox.pid .DS_Store .vscode +.coverage diff --git a/.travis.yml b/.travis.yml index 29fa87b64..872121c21 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,6 +10,7 @@ python: install: - pip install -r requirements.txt - pip install pycodestyle + - pip install coverage before_script: - psql --version - psql -U postgres -c 'SELECT version();' diff --git a/base_requirements.txt b/base_requirements.txt index f0f6cfe38..7e221f40b 100644 --- a/base_requirements.txt +++ b/base_requirements.txt @@ -22,14 +22,14 @@ django-filter # https://github.com/django-mptt/django-mptt django-mptt -# Django integration for RQ (Reqis queuing) -# https://github.com/rq/django-rq -django-rq - # Prometheus metrics library for Django # https://github.com/korfuri/django-prometheus django-prometheus +# Django integration for RQ (Reqis queuing) +# https://github.com/rq/django-rq +django-rq + # Abstraction models for rendering and paginating HTML tables # https://github.com/jieter/django-tables2 django-tables2 @@ -54,10 +54,6 @@ djangorestframework # https://github.com/axnsan12/drf-yasg drf-yasg[validation] -# Python interface to the graphviz graph rendering utility -# https://github.com/xflr6/graphviz -graphviz - # Simple markup language for rendering HTML # https://github.com/Python-Markdown/markdown # py-gfm requires Markdown<3.0 @@ -82,3 +78,11 @@ py-gfm # Extensive cryptographic library (fork of pycrypto) # https://github.com/Legrandin/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 diff --git a/contrib/gunicorn.py b/contrib/gunicorn.py new file mode 100644 index 000000000..363dbc2ff --- /dev/null +++ b/contrib/gunicorn.py @@ -0,0 +1,16 @@ +# 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 diff --git a/contrib/netbox-rq.service b/contrib/netbox-rq.service new file mode 100644 index 000000000..7a300a195 --- /dev/null +++ b/contrib/netbox-rq.service @@ -0,0 +1,22 @@ +[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 diff --git a/contrib/netbox.service b/contrib/netbox.service new file mode 100644 index 000000000..3cc9069c6 --- /dev/null +++ b/contrib/netbox.service @@ -0,0 +1,22 @@ +[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 diff --git a/docs/additional-features/graphs.md b/docs/additional-features/graphs.md index b20a6b424..264b7f1b7 100644 --- a/docs/additional-features/graphs.md +++ b/docs/additional-features/graphs.md @@ -8,6 +8,11 @@ NetBox does not have the ability to generate graphs natively, but this feature a * **Source URL:** The source of the image to be embedded. The associated object will be available as a template variable named `obj`. * **Link URL (optional):** A URL to which the graph will be linked. The associated object will be available as a template variable named `obj`. +Graph names and links can be rendered using the Django or Jinja2 template languages. + +!!! warning + Support for the Django templating language will be removed in NetBox v2.8. Jinja2 is recommended. + ## Examples You only need to define one graph object for each graph you want to include when viewing an object. For example, if you want to include a graph of traffic through an interface over the past five minutes, your graph source might looks like this: diff --git a/docs/additional-features/topology-maps.md b/docs/additional-features/topology-maps.md deleted file mode 100644 index 21bbe404d..000000000 --- a/docs/additional-features/topology-maps.md +++ /dev/null @@ -1,17 +0,0 @@ -# Topology Maps - -NetBox can generate simple topology maps from the physical network connections recorded in its database. First, you'll need to create a topology map definition under the admin UI at Extras > Topology Maps. - -Each topology map is associated with a site. A site can have multiple topology maps, which might each illustrate a different aspect of its infrastructure (for example, production versus backend infrastructure). - -To define the scope of a topology map, decide which devices you want to include. The map will only include interface connections with both points terminated on an included device. Specify the devices to include in the **device patterns** field by entering a list of [regular expressions](https://en.wikipedia.org/wiki/Regular_expression) matching device names. For example, if you wanted to include "mgmt-switch1" through "mgmt-switch99", you might use the regex `mgmt-switch\d+`. - -Each line of the **device patterns** field represents a hierarchical layer within the topology map. For example, you might map a traditional network with core, distribution, and access tiers like this: - -``` -core-switch-[abcd] -dist-switch\d -access-switch\d+;oob-switch\d+ -``` - -Note that you can combine multiple regexes onto one line using semicolons. The order in which regexes are listed on a line is significant: devices matching the first regex will be rendered first, and subsequent groups will be rendered to the right of those. diff --git a/docs/configuration/optional-settings.md b/docs/configuration/optional-settings.md index 89532d4b7..bc79e90ab 100644 --- a/docs/configuration/optional-settings.md +++ b/docs/configuration/optional-settings.md @@ -293,6 +293,26 @@ 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 Default: UTC @@ -301,14 +321,6 @@ 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 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). diff --git a/docs/configuration/required-settings.md b/docs/configuration/required-settings.md index 92b2fbfb8..dd7492cb4 100644 --- a/docs/configuration/required-settings.md +++ b/docs/configuration/required-settings.md @@ -25,7 +25,7 @@ NetBox requires access to a PostgreSQL database service to store data. This serv Example: -``` +```python DATABASE = { 'NAME': 'netbox', # Database name 'USER': 'netbox', # PostgreSQL username @@ -42,40 +42,48 @@ DATABASE = { [Redis](https://redis.io/) is an in-memory data store similar to memcached. While Redis has been an optional component of NetBox since the introduction of webhooks in version 2.4, it is required starting in 2.6 to support NetBox's caching -functionality (as well as other planned features). +functionality (as well as other planned features). In 2.7, the connection settings were broken down into two sections for +webhooks and caching, allowing the user to connect to different Redis instances/databases per feature. -Redis is configured using a configuration setting similar to `DATABASE`: +Redis is configured using a configuration setting similar to `DATABASE` and these settings are the same for both of the `webhooks` and `caching` subsections: * `HOST` - Name or IP address of the Redis server (use `localhost` if running locally) * `PORT` - TCP port of the Redis service; leave blank for default port (6379) * `PASSWORD` - Redis password (if set) -* `DATABASE` - Numeric database ID for webhooks -* `CACHE_DATABASE` - Numeric database ID for caching +* `DATABASE` - Numeric database ID * `DEFAULT_TIMEOUT` - Connection timeout in seconds * `SSL` - Use SSL connection to Redis Example: -``` +```python REDIS = { - 'HOST': 'localhost', - 'PORT': 6379, - 'PASSWORD': '', - 'DATABASE': 0, - 'CACHE_DATABASE': 1, - 'DEFAULT_TIMEOUT': 300, - 'SSL': False, + '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: - If you were using these settings in a prior release with webhooks, the `DATABASE` setting remains the same but - an additional `CACHE_DATABASE` setting has been added with a default value of 1 to support the caching backend. The - `DATABASE` setting will be renamed in a future release of NetBox to better relay the meaning of the setting. + If you are upgrading from a version prior to v2.7, please note that the Redis connection configuration settings have + changed. Manual modification to bring the `REDIS` section inline with the above specification is necessary !!! warning: - It is highly recommended to keep the webhook and cache databases seperate. Using the same database number for both may result in webhook - processing data being lost in cache flushing events. + It is highly recommended to keep the webhook and cache databases separate. Using the same database number on the + same Redis instance for both may result in webhook processing data being lost during cache flushing events. --- diff --git a/docs/core-functionality/sites-and-racks.md b/docs/core-functionality/sites-and-racks.md index bf3c473fd..f86e24b3e 100644 --- a/docs/core-functionality/sites-and-racks.md +++ b/docs/core-functionality/sites-and-racks.md @@ -40,6 +40,8 @@ Racks can be arranged into groups. As with sites, how you choose to designate ra Each rack group must be assigned to a parent site. Hierarchical recursion of rack groups is not currently supported. +The name and facility ID of each rack within a group must be unique. (Racks not assigned to the same rack group may have identical names and/or facility IDs.) + ## Rack Roles Each rack can optionally be assigned a functional role. For example, you might designate a rack for compute or storage resources, or to house colocated customer devices. Rack roles are fully customizable. diff --git a/docs/installation/1-postgresql.md b/docs/installation/1-postgresql.md index 5e9c98c5c..376a62ae2 100644 --- a/docs/installation/1-postgresql.md +++ b/docs/installation/1-postgresql.md @@ -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. !!! warning - NetBox v2.2 and later requires PostgreSQL 9.4 or higher. + NetBox requires PostgreSQL 9.4 or higher. # Installation diff --git a/docs/installation/2-netbox.md b/docs/installation/2-netbox.md index 6d2706eb0..cbe2c70c0 100644 --- a/docs/installation/2-netbox.md +++ b/docs/installation/2-netbox.md @@ -5,14 +5,14 @@ This section of the documentation discusses installing and configuring the NetBo **Ubuntu** ```no-highlight -# apt-get install -y python3 python3-pip python3-dev build-essential libxml2-dev libxslt1-dev libffi-dev graphviz libpq-dev libssl-dev redis-server zlib1g-dev +# apt-get install -y python3 python3-pip python3-dev build-essential libxml2-dev libxslt1-dev libffi-dev libpq-dev libssl-dev redis-server zlib1g-dev ``` **CentOS** ```no-highlight # yum install -y epel-release -# yum install -y gcc python36 python36-devel python36-setuptools libxml2-devel libxslt-devel libffi-devel graphviz openssl-devel redhat-rpm-config redis +# yum install -y gcc python36 python36-devel python36-setuptools libxml2-devel libxslt-devel libffi-devel openssl-devel redhat-rpm-config redis # easy_install-3.6 pip # ln -s /usr/bin/python3.6 /usr/bin/python3 ``` @@ -90,6 +90,14 @@ NetBox supports integration with the [NAPALM automation](https://napalm-automati # 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 Move into the NetBox configuration directory and make a copy of `configuration.example.py` named `configuration.py`. @@ -139,13 +147,22 @@ Redis is a in-memory key-value store required as part of the NetBox installation ```python REDIS = { - 'HOST': 'localhost', - 'PORT': 6379, - 'PASSWORD': '', - 'DATABASE': 0, - 'CACHE_DATABASE': 1, - 'DEFAULT_TIMEOUT': 300, - 'SSL': False, + '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, + } } ``` @@ -195,27 +212,7 @@ Superuser created successfully. ```no-highlight # python3 manage.py collectstatic --no-input -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) +959 static files copied to '/opt/netbox/netbox/static'. ``` # Test the Application @@ -237,3 +234,11 @@ Next, connect to the name or IP of the server (as defined in `ALLOWED_HOSTS`) on !!! 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. + +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) diff --git a/docs/installation/3-http-daemon.md b/docs/installation/3-http-daemon.md index 9c29fc979..4ca566aa3 100644 --- a/docs/installation/3-http-daemon.md +++ b/docs/installation/3-http-daemon.md @@ -1,4 +1,4 @@ -We'll set up a simple WSGI front end using [gunicorn](http://gunicorn.org/) for the purposes of this guide. For web servers, we provide example configurations for both [nginx](https://www.nginx.com/resources/wiki/) and [Apache](http://httpd.apache.org/docs/2.4). (You are of course free to use whichever combination of HTTP and WSGI services you'd like.) We'll also use [supervisord](http://supervisord.org/) to enable service persistence. +We'll set up a simple WSGI front end using [gunicorn](http://gunicorn.org/) for the purposes of this guide. For web servers, we provide example configurations for both [nginx](https://www.nginx.com/resources/wiki/) and [Apache](http://httpd.apache.org/docs/2.4). (You are of course free to use whichever combination of HTTP and WSGI services you'd like.) We'll use systemd to enable service persistence. !!! info For the sake of brevity, only Ubuntu 18.04 instructions are provided here, but this sort of web server and WSGI configuration is not unique to NetBox. Please consult your distribution's documentation for assistance if needed. @@ -107,47 +107,53 @@ Install gunicorn: # pip3 install gunicorn ``` -Save the following configuration in the root netbox installation path as `gunicorn_config.py` (e.g. `/opt/netbox/gunicorn_config.py` per our example installation). Be sure to verify the location of the gunicorn executable on your server (e.g. `which gunicorn`) and to update the `pythonpath` variable if needed. If using CentOS/RHEL, change the username from `www-data` to `nginx` or `apache`. More info on `max_requests` can be found in the [gunicorn docs](https://docs.gunicorn.org/en/stable/settings.html#max-requests). +Copy `contrib/gunicorn.py` to `/opt/netbox/gunicorn.py`. We make a copy of this file to ensure that any changes to it do not get overwritten by a future upgrade. ```no-highlight -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 +# cp contrib/gunicorn.py /opt/netbox/gunicorn.py ``` -# supervisord Installation +You may wish to edit this file to change the bound IP address or port number, or to make performance-related adjustments. -Install supervisor: +# systemd configuration + +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 -# apt-get install -y supervisor +# cp contrib/*.service /etc/systemd/system/ ``` -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`. +!!! note + 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 -[program:netbox] -command = gunicorn -c /opt/netbox/gunicorn_config.py netbox.wsgi -directory = /opt/netbox/netbox/ -user = www-data - -[program:netbox-rqworker] -command = python3 /opt/netbox/netbox/manage.py rqworker -directory = /opt/netbox/netbox/ -user = www-data +# systemctl daemon-reload +# systemctl start netbox.service +# systemctl start netbox-rq.service +# systemctl enable netbox.service +# systemctl enable netbox-rq.service ``` -Then, restart the supervisor service to detect and run the gunicorn service: +You can use the command `systemctl status netbox` to verify that the WSGI service is running: -```no-highlight -# service supervisor restart +``` +# systemctl status netbox.service +● 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 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. +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. !!! info - 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. + 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. diff --git a/docs/installation/index.md b/docs/installation/index.md index 54daa62e3..4962eb7d0 100644 --- a/docs/installation/index.md +++ b/docs/installation/index.md @@ -12,3 +12,5 @@ The following sections detail how to set up a new instance of NetBox: If you are upgrading from an existing installation, please consult the [upgrading guide](upgrading.md). NetBox v2.5 and later requires Python 3.5 or higher. Please see the instructions for [migrating to Python 3](migrating-to-python3.md) if you are still using Python 2. + +Netbox v2.5.9 and later moved to using systemd instead of supervisord. Please see the instructions for [migrating to systemd](migrating-to-systemd.md) if you are still using supervisord. diff --git a/docs/installation/migrating-to-systemd.md b/docs/installation/migrating-to-systemd.md new file mode 100644 index 000000000..6199b5511 --- /dev/null +++ b/docs/installation/migrating-to-systemd.md @@ -0,0 +1,100 @@ +# 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 +``` diff --git a/docs/installation/upgrading.md b/docs/installation/upgrading.md index 85af66536..6a2c0188f 100644 --- a/docs/installation/upgrading.md +++ b/docs/installation/upgrading.md @@ -84,14 +84,12 @@ This script: # Restart the WSGI Service -Finally, restart the WSGI service to run the new code. If you followed this guide for the initial installation, this is done using `supervisorctl`: +Finally, restart the WSGI services to run the new code. If you followed this guide for the initial installation, this is done using `systemctl: ```no-highlight -# sudo supervisorctl restart netbox +# sudo systemctl restart netbox +# sudo systemctl restart netbox-rqworker ``` -If using webhooks, also restart the Redis worker: - -```no-highlight -# sudo supervisorctl restart netbox-rqworker -``` +!!! note + It's possible you are still using supervisord instead of the linux native systemd. If you are still using supervisord you can restart the services by either restarting supervisord or by using supervisorctl to restart netbox. diff --git a/docs/media/installation/netbox_ui_admin.png b/docs/media/installation/netbox_ui_admin.png new file mode 100644 index 000000000..bde4947d5 Binary files /dev/null and b/docs/media/installation/netbox_ui_admin.png differ diff --git a/docs/media/installation/netbox_ui_guest.png b/docs/media/installation/netbox_ui_guest.png new file mode 100644 index 000000000..a20a5467a Binary files /dev/null and b/docs/media/installation/netbox_ui_guest.png differ diff --git a/docs/release-notes/index.md b/docs/release-notes/index.md index 10b4ba75c..e44a306fe 120000 --- a/docs/release-notes/index.md +++ b/docs/release-notes/index.md @@ -1 +1 @@ -version-2.6.md \ No newline at end of file +version-2.7.md \ No newline at end of file diff --git a/docs/release-notes/version-2.6.md b/docs/release-notes/version-2.6.md index b31e769a3..9fd258b0f 100644 --- a/docs/release-notes/version-2.6.md +++ b/docs/release-notes/version-2.6.md @@ -1,16 +1,3 @@ -# v2.6.13 (FUTURE) - -## Enhancements - -* [#3525](https://github.com/netbox-community/netbox/issues/3525) - Enable IP address filtering with multiple address terms -* [#3840](https://github.com/netbox-community/netbox/issues/3840) - Only show the valid list of interface VLAN choices - -## Bug Fixes - -* [#3914](https://github.com/netbox-community/netbox/issues/3914) - Fix interface filter field when unauthenticated - ---- - # v2.6.12 (2020-01-13) ## Enhancements diff --git a/docs/release-notes/version-2.7.md b/docs/release-notes/version-2.7.md new file mode 100644 index 000000000..ac9d81e2c --- /dev/null +++ b/docs/release-notes/version-2.7.md @@ -0,0 +1,276 @@ +# 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 +* netbox-community 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': '', + 'AWS_SECRET_ACCESS_KEY': '', + '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//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//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=` and the height of each rack +unit with `unit_height=`. 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//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//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/netbox-community/netbox/issues/33) - Add ability to clone objects (pre-populate form fields) +* [#648](https://github.com/netbox-community/netbox/issues/648) - Pre-populate forms when selecting "create and add another" +* [#792](https://github.com/netbox-community/netbox/issues/792) - Add power port and power outlet types +* [#1865](https://github.com/netbox-community/netbox/issues/1865) - Add console port and console server port types +* [#2669](https://github.com/netbox-community/netbox/issues/2669) - Relax uniqueness constraint on device and VM names +* [#2902](https://github.com/netbox-community/netbox/issues/2902) - Replace `supervisord` with `systemd` +* [#3455](https://github.com/netbox-community/netbox/issues/3455) - Add tenant assignment to cluster +* [#3520](https://github.com/netbox-community/netbox/issues/3520) - Add Jinja2 template support for Graphs +* [#3525](https://github.com/netbox-community/netbox/issues/3525) - Enable IP address filtering with multiple address terms +* [#3564](https://github.com/netbox-community/netbox/issues/3564) - Add list views for device components +* [#3538](https://github.com/netbox-community/netbox/issues/3538) - Introduce a REST API endpoint for executing custom + scripts +* [#3655](https://github.com/netbox-community/netbox/issues/3655) - Add `description` field to organizational models +* [#3664](https://github.com/netbox-community/netbox/issues/3664) - Enable applying configuration contexts by tags +* [#3706](https://github.com/netbox-community/netbox/issues/3706) - Increase `available_power` maximum value on PowerFeed +* [#3731](https://github.com/netbox-community/netbox/issues/3731) - Change Graph.type to a ContentType foreign key field +* [#3801](https://github.com/netbox-community/netbox/issues/3801) - Use YAML for export of device types + +## Bug Fixes + +* [#3830](https://github.com/netbox-community/netbox/issues/3830) - Ensure deterministic ordering for all models +* [#3900](https://github.com/netbox-community/netbox/issues/3900) - Fix exception when deleting device types +* [#3914](https://github.com/netbox-community/netbox/issues/3914) - Fix interface filter field when unauthenticated +* [#3919](https://github.com/netbox-community/netbox/issues/3919) - Fix utilization graph extending out of bounds when utilization > 100% +* [#3927](https://github.com/netbox-community/netbox/issues/3927) - Fix exception when deleting devices with secrets assigned +* [#3930](https://github.com/netbox-community/netbox/issues/3930) - Fix API rendering of the `family` field for aggregates + +## Bug Fixes (From Beta) + +* [#3868](https://github.com/netbox-community/netbox/issues/3868) - Fix creation of interfaces for virtual machines +* [#3878](https://github.com/netbox-community/netbox/issues/3878) - Fix database migration for cable status 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: Added field `template_language` (to indicate `django` or `jinja2`) +* extras.Graph: The `type` field has been changed to a content type foreign key. Models are specified as + `.`; e.g. `dcim.site`. +* ipam.Role: Added field `description` +* secrets.SecretRole: Added field `description` +* virtualization.Cluster: Added field `tenant` diff --git a/netbox/circuits/api/serializers.py b/netbox/circuits/api/serializers.py index 39a0b6b26..b22135b3f 100644 --- a/netbox/circuits/api/serializers.py +++ b/netbox/circuits/api/serializers.py @@ -1,7 +1,7 @@ from rest_framework import serializers from taggit_serializer.serializers import TaggitSerializer, TagListSerializerField -from circuits.constants import CIRCUIT_STATUS_CHOICES +from circuits.choices import CircuitStatusChoices from circuits.models import Provider, Circuit, CircuitTermination, CircuitType from dcim.api.nested_serializers import NestedCableSerializer, NestedSiteSerializer from dcim.api.serializers import ConnectedEndpointSerializer @@ -36,12 +36,12 @@ class CircuitTypeSerializer(ValidatedModelSerializer): class Meta: model = CircuitType - fields = ['id', 'name', 'slug', 'circuit_count'] + fields = ['id', 'name', 'slug', 'description', 'circuit_count'] class CircuitSerializer(TaggitSerializer, CustomFieldModelSerializer): provider = NestedProviderSerializer() - status = ChoiceField(choices=CIRCUIT_STATUS_CHOICES, required=False) + status = ChoiceField(choices=CircuitStatusChoices, required=False) type = NestedCircuitTypeSerializer() tenant = NestedTenantSerializer(required=False, allow_null=True) tags = TagListSerializerField(required=False) diff --git a/netbox/circuits/api/views.py b/netbox/circuits/api/views.py index 65b0db14b..98b7c9184 100644 --- a/netbox/circuits/api/views.py +++ b/netbox/circuits/api/views.py @@ -7,7 +7,7 @@ from circuits import filters from circuits.models import Provider, CircuitTermination, CircuitType, Circuit from extras.api.serializers import RenderedGraphSerializer from extras.api.views import CustomFieldModelViewSet -from extras.models import Graph, GRAPH_TYPE_PROVIDER +from extras.models import Graph from utilities.api import FieldChoicesViewSet, ModelViewSet from . import serializers @@ -18,8 +18,8 @@ from . import serializers class CircuitsFieldChoicesViewSet(FieldChoicesViewSet): fields = ( - (Circuit, ['status']), - (CircuitTermination, ['term_side']), + (serializers.CircuitSerializer, ['status']), + (serializers.CircuitTerminationSerializer, ['term_side']), ) @@ -32,7 +32,7 @@ class ProviderViewSet(CustomFieldModelViewSet): circuit_count=Count('circuits') ) serializer_class = serializers.ProviderSerializer - filterset_class = filters.ProviderFilter + filterset_class = filters.ProviderFilterSet @action(detail=True) def graphs(self, request, pk): @@ -40,7 +40,7 @@ class ProviderViewSet(CustomFieldModelViewSet): A convenience method for rendering graphs for a particular provider. """ provider = get_object_or_404(Provider, pk=pk) - queryset = Graph.objects.filter(type=GRAPH_TYPE_PROVIDER) + queryset = Graph.objects.filter(type__model='provider') serializer = RenderedGraphSerializer(queryset, many=True, context={'graphed_object': provider}) return Response(serializer.data) @@ -54,7 +54,7 @@ class CircuitTypeViewSet(ModelViewSet): circuit_count=Count('circuits') ) serializer_class = serializers.CircuitTypeSerializer - filterset_class = filters.CircuitTypeFilter + filterset_class = filters.CircuitTypeFilterSet # @@ -64,7 +64,7 @@ class CircuitTypeViewSet(ModelViewSet): class CircuitViewSet(CustomFieldModelViewSet): queryset = Circuit.objects.prefetch_related('type', 'tenant', 'provider').prefetch_related('tags') serializer_class = serializers.CircuitSerializer - filterset_class = filters.CircuitFilter + filterset_class = filters.CircuitFilterSet # @@ -76,4 +76,4 @@ class CircuitTerminationViewSet(ModelViewSet): 'circuit', 'site', 'connected_endpoint__device', 'cable' ) serializer_class = serializers.CircuitTerminationSerializer - filterset_class = filters.CircuitTerminationFilter + filterset_class = filters.CircuitTerminationFilterSet diff --git a/netbox/circuits/choices.py b/netbox/circuits/choices.py new file mode 100644 index 000000000..94a765d11 --- /dev/null +++ b/netbox/circuits/choices.py @@ -0,0 +1,48 @@ +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') + ) diff --git a/netbox/circuits/constants.py b/netbox/circuits/constants.py deleted file mode 100644 index 9e180e655..000000000 --- a/netbox/circuits/constants.py +++ /dev/null @@ -1,23 +0,0 @@ -# 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'), -) diff --git a/netbox/circuits/filters.py b/netbox/circuits/filters.py index 1f1d078f6..c27ffb8d7 100644 --- a/netbox/circuits/filters.py +++ b/netbox/circuits/filters.py @@ -3,20 +3,20 @@ from django.db.models import Q from dcim.models import Region, Site from extras.filters import CustomFieldFilterSet, CreatedUpdatedFilterSet -from tenancy.filtersets import TenancyFilterSet +from tenancy.filters import TenancyFilterSet from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter -from .constants import * +from .choices import * from .models import Circuit, CircuitTermination, CircuitType, Provider __all__ = ( - 'CircuitFilter', - 'CircuitTerminationFilter', - 'CircuitTypeFilter', - 'ProviderFilter', + 'CircuitFilterSet', + 'CircuitTerminationFilterSet', + 'CircuitTypeFilterSet', + 'ProviderFilterSet', ) -class ProviderFilter(CustomFieldFilterSet, CreatedUpdatedFilterSet): +class ProviderFilterSet(CustomFieldFilterSet, CreatedUpdatedFilterSet): id__in = NumericInFilter( field_name='id', lookup_expr='in' @@ -65,14 +65,14 @@ class ProviderFilter(CustomFieldFilterSet, CreatedUpdatedFilterSet): ) -class CircuitTypeFilter(NameSlugSearchFilterSet): +class CircuitTypeFilterSet(NameSlugSearchFilterSet): class Meta: model = CircuitType fields = ['id', 'name', 'slug'] -class CircuitFilter(CustomFieldFilterSet, TenancyFilterSet, CreatedUpdatedFilterSet): +class CircuitFilterSet(CustomFieldFilterSet, TenancyFilterSet, CreatedUpdatedFilterSet): id__in = NumericInFilter( field_name='id', lookup_expr='in' @@ -102,7 +102,7 @@ class CircuitFilter(CustomFieldFilterSet, TenancyFilterSet, CreatedUpdatedFilter label='Circuit type (slug)', ) status = django_filters.MultipleChoiceFilter( - choices=CIRCUIT_STATUS_CHOICES, + choices=CircuitStatusChoices, null_value=None ) site_id = django_filters.ModelMultipleChoiceFilter( @@ -146,7 +146,7 @@ class CircuitFilter(CustomFieldFilterSet, TenancyFilterSet, CreatedUpdatedFilter ).distinct() -class CircuitTerminationFilter(django_filters.FilterSet): +class CircuitTerminationFilterSet(django_filters.FilterSet): q = django_filters.CharFilter( method='search', label='Search', diff --git a/netbox/circuits/fixtures/initial_data.json b/netbox/circuits/fixtures/initial_data.json deleted file mode 100644 index c918bbeea..000000000 --- a/netbox/circuits/fixtures/initial_data.json +++ /dev/null @@ -1,26 +0,0 @@ -[ -{ - "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" - } -} -] diff --git a/netbox/circuits/forms.py b/netbox/circuits/forms.py index 4a5c06a6e..d5d78e7bd 100644 --- a/netbox/circuits/forms.py +++ b/netbox/circuits/forms.py @@ -9,7 +9,7 @@ from utilities.forms import ( APISelect, APISelectMultiple, add_blank_choice, BootstrapMixin, CommentField, CSVChoiceField, DatePicker, FilterChoiceField, SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple ) -from .constants import * +from .choices import CircuitStatusChoices from .models import Circuit, CircuitTermination, CircuitType, Provider @@ -140,7 +140,7 @@ class CircuitTypeForm(BootstrapMixin, forms.ModelForm): class Meta: model = CircuitType fields = [ - 'name', 'slug', + 'name', 'slug', 'description', ] @@ -205,7 +205,7 @@ class CircuitCSVForm(forms.ModelForm): } ) status = CSVChoiceField( - choices=CIRCUIT_STATUS_CHOICES, + choices=CircuitStatusChoices, required=False, help_text='Operational status' ) @@ -246,7 +246,7 @@ class CircuitBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEdit ) ) status = forms.ChoiceField( - choices=add_blank_choice(CIRCUIT_STATUS_CHOICES), + choices=add_blank_choice(CircuitStatusChoices), required=False, initial='', widget=StaticSelect2() @@ -303,7 +303,7 @@ class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm ) ) status = forms.MultipleChoiceField( - choices=CIRCUIT_STATUS_CHOICES, + choices=CircuitStatusChoices, required=False, widget=StaticSelect2Multiple() ) diff --git a/netbox/circuits/migrations/0001_initial_squashed_0010_circuit_status.py b/netbox/circuits/migrations/0001_initial_squashed_0006_terminations.py similarity index 58% rename from netbox/circuits/migrations/0001_initial_squashed_0010_circuit_status.py rename to netbox/circuits/migrations/0001_initial_squashed_0006_terminations.py index 3fcec7933..4eec30667 100644 --- a/netbox/circuits/migrations/0001_initial_squashed_0010_circuit_status.py +++ b/netbox/circuits/migrations/0001_initial_squashed_0006_terminations.py @@ -1,40 +1,36 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11.14 on 2018-07-31 02:25 -import dcim.fields -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models + +import dcim.fields + + +def circuits_to_terms(apps, schema_editor): + Circuit = apps.get_model('circuits', 'Circuit') + CircuitTermination = apps.get_model('circuits', 'CircuitTermination') + for c in Circuit.objects.all(): + CircuitTermination( + circuit=c, + term_side=b'A', + site=c.site, + interface=c.interface, + port_speed=c.port_speed, + upstream_speed=c.upstream_speed, + xconnect_id=c.xconnect_id, + pp_info=c.pp_info, + ).save() class Migration(migrations.Migration): - replaces = [('circuits', '0001_initial'), ('circuits', '0002_auto_20160622_1821'), ('circuits', '0003_provider_32bit_asn_support'), ('circuits', '0004_circuit_add_tenant'), ('circuits', '0005_circuit_add_upstream_speed'), ('circuits', '0006_terminations'), ('circuits', '0007_circuit_add_description'), ('circuits', '0008_circuittermination_interface_protect_on_delete'), ('circuits', '0009_unicode_literals'), ('circuits', '0010_circuit_status')] + replaces = [('circuits', '0001_initial'), ('circuits', '0002_auto_20160622_1821'), ('circuits', '0003_provider_32bit_asn_support'), ('circuits', '0004_circuit_add_tenant'), ('circuits', '0005_circuit_add_upstream_speed'), ('circuits', '0006_terminations')] dependencies = [ + ('tenancy', '0001_initial'), ('dcim', '0001_initial'), ('dcim', '0022_color_names_to_rgb'), - ('tenancy', '0001_initial'), ] operations = [ - migrations.CreateModel( - name='Provider', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', models.DateField(auto_now_add=True)), - ('last_updated', models.DateTimeField(auto_now=True)), - ('name', models.CharField(max_length=50, unique=True)), - ('slug', models.SlugField(unique=True)), - ('asn', dcim.fields.ASNField(blank=True, null=True, verbose_name='ASN')), - ('account', models.CharField(blank=True, max_length=30, verbose_name='Account number')), - ('portal_url', models.URLField(blank=True, verbose_name='Portal')), - ('noc_contact', models.TextField(blank=True, verbose_name='NOC contact')), - ('admin_contact', models.TextField(blank=True, verbose_name='Admin contact')), - ('comments', models.TextField(blank=True)), - ], - options={ - 'ordering': ['name'], - }, - ), migrations.CreateModel( name='CircuitType', fields=[ @@ -46,49 +42,93 @@ class Migration(migrations.Migration): 'ordering': ['name'], }, ), + migrations.CreateModel( + name='Provider', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateField(auto_now_add=True)), + ('last_updated', models.DateTimeField(auto_now=True)), + ('name', models.CharField(max_length=50, unique=True)), + ('slug', models.SlugField(unique=True)), + ('asn', dcim.fields.ASNField(blank=True, null=True, verbose_name=b'ASN')), + ('account', models.CharField(blank=True, max_length=30, verbose_name=b'Account number')), + ('portal_url', models.URLField(blank=True, verbose_name=b'Portal')), + ('noc_contact', models.TextField(blank=True, verbose_name=b'NOC contact')), + ('admin_contact', models.TextField(blank=True, verbose_name=b'Admin contact')), + ('comments', models.TextField(blank=True)), + ], + options={ + 'ordering': ['name'], + }, + ), migrations.CreateModel( name='Circuit', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('created', models.DateField(auto_now_add=True)), ('last_updated', models.DateTimeField(auto_now=True)), - ('cid', models.CharField(max_length=50, verbose_name='Circuit ID')), - ('install_date', models.DateField(blank=True, null=True, verbose_name='Date installed')), - ('commit_rate', models.PositiveIntegerField(blank=True, null=True, verbose_name='Commit rate (Kbps)')), + ('cid', models.CharField(max_length=50, verbose_name=b'Circuit ID')), + ('install_date', models.DateField(blank=True, null=True, verbose_name=b'Date installed')), + ('port_speed', models.PositiveIntegerField(verbose_name=b'Port speed (Kbps)')), + ('commit_rate', models.PositiveIntegerField(blank=True, null=True, verbose_name=b'Commit rate (Kbps)')), + ('xconnect_id', models.CharField(blank=True, max_length=50, verbose_name=b'Cross-connect ID')), + ('pp_info', models.CharField(blank=True, max_length=100, verbose_name=b'Patch panel/port(s)')), ('comments', models.TextField(blank=True)), + ('interface', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='circuit', to='dcim.Interface')), ('provider', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='circuits', to='circuits.Provider')), + ('site', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='circuits', to='dcim.Site')), ('type', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='circuits', to='circuits.CircuitType')), ('tenant', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='circuits', to='tenancy.Tenant')), - ('description', models.CharField(blank=True, max_length=100)), - ('status', models.PositiveSmallIntegerField(choices=[[2, 'Planned'], [3, 'Provisioning'], [1, 'Active'], [4, 'Offline'], [0, 'Deprovisioning'], [5, 'Decommissioned']], default=1)) + ('upstream_speed', models.PositiveIntegerField(blank=True, help_text=b'Upstream speed, if different from port speed', null=True, verbose_name=b'Upstream speed (Kbps)')), ], options={ 'ordering': ['provider', 'cid'], + 'unique_together': {('provider', 'cid')}, }, ), - migrations.AlterUniqueTogether( - name='circuit', - unique_together=set([('provider', 'cid')]), - ), migrations.CreateModel( name='CircuitTermination', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('term_side', models.CharField(choices=[('A', 'A'), ('Z', 'Z')], max_length=1, verbose_name='Termination')), - ('port_speed', models.PositiveIntegerField(verbose_name='Port speed (Kbps)')), - ('upstream_speed', models.PositiveIntegerField(blank=True, help_text='Upstream speed, if different from port speed', null=True, verbose_name='Upstream speed (Kbps)')), - ('xconnect_id', models.CharField(blank=True, max_length=50, verbose_name='Cross-connect ID')), - ('pp_info', models.CharField(blank=True, max_length=100, verbose_name='Patch panel/port(s)')), + ('term_side', models.CharField(choices=[(b'A', b'A'), (b'Z', b'Z')], max_length=1, verbose_name='Termination')), + ('port_speed', models.PositiveIntegerField(verbose_name=b'Port speed (Kbps)')), + ('upstream_speed', models.PositiveIntegerField(blank=True, help_text=b'Upstream speed, if different from port speed', null=True, verbose_name=b'Upstream speed (Kbps)')), + ('xconnect_id', models.CharField(blank=True, max_length=50, verbose_name=b'Cross-connect ID')), + ('pp_info', models.CharField(blank=True, max_length=100, verbose_name=b'Patch panel/port(s)')), ('circuit', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='terminations', to='circuits.Circuit')), - ('interface', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='circuit_termination', to='dcim.Interface')), + ('interface', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='circuit_termination', to='dcim.Interface')), ('site', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='circuit_terminations', to='dcim.Site')), ], options={ 'ordering': ['circuit', 'term_side'], + 'unique_together': {('circuit', 'term_side')}, }, ), - migrations.AlterUniqueTogether( - name='circuittermination', - unique_together=set([('circuit', 'term_side')]), + migrations.RunPython( + code=circuits_to_terms, + ), + migrations.RemoveField( + model_name='circuit', + name='interface', + ), + migrations.RemoveField( + model_name='circuit', + name='port_speed', + ), + migrations.RemoveField( + model_name='circuit', + name='pp_info', + ), + migrations.RemoveField( + model_name='circuit', + name='site', + ), + migrations.RemoveField( + model_name='circuit', + name='upstream_speed', + ), + migrations.RemoveField( + model_name='circuit', + name='xconnect_id', ), ] diff --git a/netbox/circuits/migrations/0007_circuit_add_description_squashed_0017_circuittype_description.py b/netbox/circuits/migrations/0007_circuit_add_description_squashed_0017_circuittype_description.py new file mode 100644 index 000000000..5bcd863a4 --- /dev/null +++ b/netbox/circuits/migrations/0007_circuit_add_description_squashed_0017_circuittype_description.py @@ -0,0 +1,254 @@ +import sys + +import django.db.models.deletion +import taggit.managers +from django.db import migrations, models + +import dcim.fields + +CONNECTION_STATUS_CONNECTED = True + +CIRCUIT_STATUS_CHOICES = ( + (0, 'deprovisioning'), + (1, 'active'), + (2, 'planned'), + (3, 'provisioning'), + (4, 'offline'), + (5, 'decommissioned') +) + + +def circuit_terminations_to_cables(apps, schema_editor): + """ + Copy all existing CircuitTermination Interface associations as Cables + """ + ContentType = apps.get_model('contenttypes', 'ContentType') + CircuitTermination = apps.get_model('circuits', 'CircuitTermination') + Interface = apps.get_model('dcim', 'Interface') + Cable = apps.get_model('dcim', 'Cable') + + # Load content types + circuittermination_type = ContentType.objects.get_for_model(CircuitTermination) + interface_type = ContentType.objects.get_for_model(Interface) + + # Create a new Cable instance from each console connection + if 'test' not in sys.argv: + print("\n Adding circuit terminations... ", end='', flush=True) + for circuittermination in CircuitTermination.objects.filter(interface__isnull=False): + + # Create the new Cable + cable = Cable.objects.create( + termination_a_type=circuittermination_type, + termination_a_id=circuittermination.id, + termination_b_type=interface_type, + termination_b_id=circuittermination.interface_id, + status=CONNECTION_STATUS_CONNECTED + ) + + # Cache the Cable on its two termination points + CircuitTermination.objects.filter(pk=circuittermination.pk).update( + cable=cable, + connected_endpoint=circuittermination.interface, + connection_status=CONNECTION_STATUS_CONNECTED + ) + # Cache the connected Cable on the Interface + Interface.objects.filter(pk=circuittermination.interface_id).update( + cable=cable, + _connected_circuittermination=circuittermination, + connection_status=CONNECTION_STATUS_CONNECTED + ) + + cable_count = Cable.objects.filter(termination_a_type=circuittermination_type).count() + if 'test' not in sys.argv: + print("{} cables created".format(cable_count)) + + +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): + + replaces = [('circuits', '0007_circuit_add_description'), ('circuits', '0008_circuittermination_interface_protect_on_delete'), ('circuits', '0009_unicode_literals'), ('circuits', '0010_circuit_status'), ('circuits', '0011_tags'), ('circuits', '0012_change_logging'), ('circuits', '0013_cables'), ('circuits', '0014_circuittermination_description'), ('circuits', '0015_custom_tag_models'), ('circuits', '0016_3569_circuit_fields'), ('circuits', '0017_circuittype_description')] + + dependencies = [ + ('circuits', '0006_terminations'), + ('extras', '0019_tag_taggeditem'), + ('taggit', '0002_auto_20150616_2121'), + ('dcim', '0066_cables'), + ] + + operations = [ + migrations.AddField( + model_name='circuit', + name='description', + field=models.CharField(blank=True, max_length=100), + ), + migrations.AlterField( + model_name='circuittermination', + name='interface', + field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='circuit_termination', to='dcim.Interface'), + ), + migrations.AlterField( + model_name='circuit', + name='cid', + field=models.CharField(max_length=50, verbose_name='Circuit ID'), + ), + migrations.AlterField( + model_name='circuit', + name='commit_rate', + field=models.PositiveIntegerField(blank=True, null=True, verbose_name='Commit rate (Kbps)'), + ), + migrations.AlterField( + model_name='circuit', + name='install_date', + field=models.DateField(blank=True, null=True, verbose_name='Date installed'), + ), + migrations.AlterField( + model_name='circuittermination', + name='port_speed', + field=models.PositiveIntegerField(verbose_name='Port speed (Kbps)'), + ), + migrations.AlterField( + model_name='circuittermination', + name='pp_info', + field=models.CharField(blank=True, max_length=100, verbose_name='Patch panel/port(s)'), + ), + migrations.AlterField( + model_name='circuittermination', + name='term_side', + field=models.CharField(choices=[('A', 'A'), ('Z', 'Z')], max_length=1, verbose_name='Termination'), + ), + migrations.AlterField( + model_name='circuittermination', + name='upstream_speed', + field=models.PositiveIntegerField(blank=True, help_text='Upstream speed, if different from port speed', null=True, verbose_name='Upstream speed (Kbps)'), + ), + migrations.AlterField( + model_name='circuittermination', + name='xconnect_id', + field=models.CharField(blank=True, max_length=50, verbose_name='Cross-connect ID'), + ), + migrations.AlterField( + model_name='provider', + name='account', + field=models.CharField(blank=True, max_length=30, verbose_name='Account number'), + ), + migrations.AlterField( + model_name='provider', + name='admin_contact', + field=models.TextField(blank=True, verbose_name='Admin contact'), + ), + migrations.AlterField( + model_name='provider', + name='asn', + field=dcim.fields.ASNField(blank=True, null=True, verbose_name='ASN'), + ), + migrations.AlterField( + model_name='provider', + name='noc_contact', + field=models.TextField(blank=True, verbose_name='NOC contact'), + ), + migrations.AlterField( + model_name='provider', + name='portal_url', + field=models.URLField(blank=True, verbose_name='Portal'), + ), + migrations.AddField( + model_name='circuit', + name='status', + field=models.PositiveSmallIntegerField(choices=[[2, 'Planned'], [3, 'Provisioning'], [1, 'Active'], [4, 'Offline'], [0, 'Deprovisioning'], [5, 'Decommissioned']], default=1), + ), + migrations.AddField( + model_name='circuit', + name='tags', + field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'), + ), + migrations.AddField( + model_name='provider', + name='tags', + field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'), + ), + migrations.AddField( + model_name='circuittype', + name='created', + field=models.DateField(auto_now_add=True, null=True), + ), + migrations.AddField( + model_name='circuittype', + name='last_updated', + field=models.DateTimeField(auto_now=True, null=True), + ), + migrations.AlterField( + model_name='circuit', + name='created', + field=models.DateField(auto_now_add=True, null=True), + ), + migrations.AlterField( + model_name='circuit', + name='last_updated', + field=models.DateTimeField(auto_now=True, null=True), + ), + migrations.AlterField( + model_name='provider', + name='created', + field=models.DateField(auto_now_add=True, null=True), + ), + migrations.AlterField( + model_name='provider', + name='last_updated', + field=models.DateTimeField(auto_now=True, null=True), + ), + migrations.AddField( + model_name='circuittermination', + name='connected_endpoint', + field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.Interface'), + ), + migrations.AddField( + model_name='circuittermination', + name='connection_status', + field=models.NullBooleanField(), + ), + migrations.AddField( + model_name='circuittermination', + name='cable', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.Cable'), + ), + migrations.RunPython( + code=circuit_terminations_to_cables, + ), + migrations.RemoveField( + model_name='circuittermination', + name='interface', + ), + migrations.AddField( + model_name='circuittermination', + name='description', + field=models.CharField(blank=True, max_length=100), + ), + migrations.AlterField( + model_name='circuit', + name='tags', + field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags'), + ), + migrations.AlterField( + model_name='provider', + name='tags', + field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags'), + ), + migrations.AlterField( + model_name='circuit', + name='status', + field=models.CharField(default='active', max_length=50), + ), + migrations.RunPython( + code=circuit_status_to_slug, + ), + migrations.AddField( + model_name='circuittype', + name='description', + field=models.CharField(blank=True, max_length=100), + ), + ] diff --git a/netbox/circuits/migrations/0013_cables.py b/netbox/circuits/migrations/0013_cables.py index 4e9125a99..ec0284be0 100644 --- a/netbox/circuits/migrations/0013_cables.py +++ b/netbox/circuits/migrations/0013_cables.py @@ -3,7 +3,7 @@ import sys from django.db import migrations, models import django.db.models.deletion -from dcim.constants import CONNECTION_STATUS_CONNECTED +CONNECTION_STATUS_CONNECTED = True def circuit_terminations_to_cables(apps, schema_editor): diff --git a/netbox/circuits/migrations/0016_3569_circuit_fields.py b/netbox/circuits/migrations/0016_3569_circuit_fields.py new file mode 100644 index 000000000..a65f72d61 --- /dev/null +++ b/netbox/circuits/migrations/0016_3569_circuit_fields.py @@ -0,0 +1,39 @@ +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 + ), + + ] diff --git a/netbox/circuits/migrations/0017_circuittype_description.py b/netbox/circuits/migrations/0017_circuittype_description.py new file mode 100644 index 000000000..4cb5591dd --- /dev/null +++ b/netbox/circuits/migrations/0017_circuittype_description.py @@ -0,0 +1,18 @@ +# 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), + ), + ] diff --git a/netbox/circuits/models.py b/netbox/circuits/models.py index 8cf18617c..59f6e2004 100644 --- a/netbox/circuits/models.py +++ b/netbox/circuits/models.py @@ -3,13 +3,21 @@ from django.db import models from django.urls import reverse from taggit.managers import TaggableManager -from dcim.constants import CONNECTION_STATUS_CHOICES, STATUS_CLASSES +from dcim.constants import CONNECTION_STATUS_CHOICES from dcim.fields import ASNField from dcim.models import CableTermination from extras.models import CustomFieldModel, ObjectChange, TaggedItem from utilities.models import ChangeLoggedModel from utilities.utils import serialize_object -from .constants import * +from .choices import * + + +__all__ = ( + 'Circuit', + 'CircuitTermination', + 'CircuitType', + 'Provider', +) class Provider(ChangeLoggedModel, CustomFieldModel): @@ -57,7 +65,12 @@ class Provider(ChangeLoggedModel, CustomFieldModel): tags = TaggableManager(through=TaggedItem) - csv_headers = ['name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments'] + csv_headers = [ + 'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments', + ] + clone_fields = [ + 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', + ] class Meta: ordering = ['name'] @@ -93,8 +106,12 @@ class CircuitType(ChangeLoggedModel): slug = models.SlugField( unique=True ) + description = models.CharField( + max_length=100, + blank=True, + ) - csv_headers = ['name', 'slug'] + csv_headers = ['name', 'slug', 'description'] class Meta: ordering = ['name'] @@ -109,6 +126,7 @@ class CircuitType(ChangeLoggedModel): return ( self.name, self.slug, + self.description, ) @@ -132,9 +150,10 @@ class Circuit(ChangeLoggedModel, CustomFieldModel): on_delete=models.PROTECT, related_name='circuits' ) - status = models.PositiveSmallIntegerField( - choices=CIRCUIT_STATUS_CHOICES, - default=CIRCUIT_STATUS_ACTIVE + status = models.CharField( + max_length=50, + choices=CircuitStatusChoices, + default=CircuitStatusChoices.STATUS_ACTIVE ) tenant = models.ForeignKey( to='tenancy.Tenant', @@ -170,6 +189,18 @@ class Circuit(ChangeLoggedModel, CustomFieldModel): csv_headers = [ '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: ordering = ['provider', 'cid'] @@ -195,7 +226,7 @@ class Circuit(ChangeLoggedModel, CustomFieldModel): ) def get_status_class(self): - return STATUS_CLASSES[self.status] + return self.STATUS_CLASS_MAP.get(self.status) def _get_termination(self, side): for ct in self.terminations.all(): @@ -220,7 +251,7 @@ class CircuitTermination(CableTermination): ) term_side = models.CharField( max_length=1, - choices=TERM_SIDE_CHOICES, + choices=CircuitTerminationSideChoices, verbose_name='Termination' ) site = models.ForeignKey( diff --git a/netbox/circuits/tables.py b/netbox/circuits/tables.py index d67abdd1a..dbd9e6ba1 100644 --- a/netbox/circuits/tables.py +++ b/netbox/circuits/tables.py @@ -50,12 +50,14 @@ class CircuitTypeTable(BaseTable): name = tables.LinkColumn() circuit_count = tables.Column(verbose_name='Circuits') actions = tables.TemplateColumn( - template_code=CIRCUITTYPE_ACTIONS, attrs={'td': {'class': 'text-right noprint'}}, verbose_name='' + template_code=CIRCUITTYPE_ACTIONS, + attrs={'td': {'class': 'text-right noprint'}}, + verbose_name='' ) class Meta(BaseTable.Meta): model = CircuitType - fields = ('pk', 'name', 'circuit_count', 'slug', 'actions') + fields = ('pk', 'name', 'circuit_count', 'description', 'slug', 'actions') # diff --git a/netbox/circuits/tests/test_api.py b/netbox/circuits/tests/test_api.py index e53c2c402..b1b6d9e14 100644 --- a/netbox/circuits/tests/test_api.py +++ b/netbox/circuits/tests/test_api.py @@ -1,12 +1,35 @@ +from django.contrib.contenttypes.models import ContentType from django.urls import reverse from rest_framework import status -from circuits.constants import CIRCUIT_STATUS_ACTIVE, TERM_SIDE_A, TERM_SIDE_Z +from circuits.choices import * from circuits.models import Circuit, CircuitTermination, CircuitType, Provider -from dcim.models import Device, DeviceRole, DeviceType, Interface, Manufacturer, Site -from extras.constants import GRAPH_TYPE_PROVIDER +from dcim.models import Site from extras.models import Graph -from utilities.testing import APITestCase +from utilities.testing import APITestCase, choices_to_dict + + +class AppTest(APITestCase): + + def test_root(self): + + url = reverse('circuits-api:api-root') + response = self.client.get('{}?format=api'.format(url), **self.header) + + self.assertEqual(response.status_code, 200) + + def test_choices(self): + + url = reverse('circuits-api:field-choice-list') + response = self.client.get(url, **self.header) + + self.assertEqual(response.status_code, 200) + + # Circuit + self.assertEqual(choices_to_dict(response.data.get('circuit:status')), CircuitStatusChoices.as_dict()) + + # CircuitTermination + self.assertEqual(choices_to_dict(response.data.get('circuit-termination:term_side')), CircuitTerminationSideChoices.as_dict()) class ProviderTest(APITestCase): @@ -28,16 +51,20 @@ class ProviderTest(APITestCase): def test_get_provider_graphs(self): + provider_ct = ContentType.objects.get(app_label='circuits', model='provider') self.graph1 = Graph.objects.create( - type=GRAPH_TYPE_PROVIDER, name='Test Graph 1', + type=provider_ct, + name='Test Graph 1', source='http://example.com/graphs.py?provider={{ obj.slug }}&foo=1' ) self.graph2 = Graph.objects.create( - type=GRAPH_TYPE_PROVIDER, name='Test Graph 2', + type=provider_ct, + name='Test Graph 2', source='http://example.com/graphs.py?provider={{ obj.slug }}&foo=2' ) self.graph3 = Graph.objects.create( - type=GRAPH_TYPE_PROVIDER, name='Test Graph 3', + type=provider_ct, + name='Test Graph 3', source='http://example.com/graphs.py?provider={{ obj.slug }}&foo=3' ) @@ -250,7 +277,7 @@ class CircuitTest(APITestCase): 'cid': 'TEST0004', 'provider': self.provider1.pk, 'type': self.circuittype1.pk, - 'status': CIRCUIT_STATUS_ACTIVE, + 'status': CircuitStatusChoices.STATUS_ACTIVE, } url = reverse('circuits-api:circuit-list') @@ -270,19 +297,19 @@ class CircuitTest(APITestCase): 'cid': 'TEST0004', 'provider': self.provider1.pk, 'type': self.circuittype1.pk, - 'status': CIRCUIT_STATUS_ACTIVE, + 'status': CircuitStatusChoices.STATUS_ACTIVE, }, { 'cid': 'TEST0005', 'provider': self.provider1.pk, 'type': self.circuittype1.pk, - 'status': CIRCUIT_STATUS_ACTIVE, + 'status': CircuitStatusChoices.STATUS_ACTIVE, }, { 'cid': 'TEST0006', 'provider': self.provider1.pk, 'type': self.circuittype1.pk, - 'status': CIRCUIT_STATUS_ACTIVE, + 'status': CircuitStatusChoices.STATUS_ACTIVE, }, ] @@ -336,16 +363,28 @@ class CircuitTerminationTest(APITestCase): self.circuit2 = Circuit.objects.create(cid='TEST0002', provider=provider, type=circuittype) self.circuit3 = Circuit.objects.create(cid='TEST0003', provider=provider, type=circuittype) self.circuittermination1 = CircuitTermination.objects.create( - circuit=self.circuit1, term_side=TERM_SIDE_A, site=self.site1, port_speed=1000000 + circuit=self.circuit1, + term_side=CircuitTerminationSideChoices.SIDE_A, + site=self.site1, + port_speed=1000000 ) self.circuittermination2 = CircuitTermination.objects.create( - circuit=self.circuit1, term_side=TERM_SIDE_Z, site=self.site2, port_speed=1000000 + circuit=self.circuit1, + term_side=CircuitTerminationSideChoices.SIDE_Z, + site=self.site2, + port_speed=1000000 ) self.circuittermination3 = CircuitTermination.objects.create( - circuit=self.circuit2, term_side=TERM_SIDE_A, site=self.site1, port_speed=1000000 + circuit=self.circuit2, + term_side=CircuitTerminationSideChoices.SIDE_A, + site=self.site1, + port_speed=1000000 ) self.circuittermination4 = CircuitTermination.objects.create( - circuit=self.circuit2, term_side=TERM_SIDE_Z, site=self.site2, port_speed=1000000 + circuit=self.circuit2, + term_side=CircuitTerminationSideChoices.SIDE_Z, + site=self.site2, + port_speed=1000000 ) def test_get_circuittermination(self): @@ -366,7 +405,7 @@ class CircuitTerminationTest(APITestCase): data = { 'circuit': self.circuit3.pk, - 'term_side': TERM_SIDE_A, + 'term_side': CircuitTerminationSideChoices.SIDE_A, 'site': self.site1.pk, 'port_speed': 1000000, } @@ -385,12 +424,15 @@ class CircuitTerminationTest(APITestCase): def test_update_circuittermination(self): circuittermination5 = CircuitTermination.objects.create( - circuit=self.circuit3, term_side=TERM_SIDE_A, site=self.site1, port_speed=1000000 + circuit=self.circuit3, + term_side=CircuitTerminationSideChoices.SIDE_A, + site=self.site1, + port_speed=1000000 ) data = { 'circuit': self.circuit3.pk, - 'term_side': TERM_SIDE_Z, + 'term_side': CircuitTerminationSideChoices.SIDE_Z, 'site': self.site2.pk, 'port_speed': 1000000, } diff --git a/netbox/circuits/tests/test_filters.py b/netbox/circuits/tests/test_filters.py index a715ad757..46c2bacbe 100644 --- a/netbox/circuits/tests/test_filters.py +++ b/netbox/circuits/tests/test_filters.py @@ -1,6 +1,6 @@ from django.test import TestCase -from circuits.constants import CIRCUIT_STATUS_ACTIVE, CIRCUIT_STATUS_OFFLINE, CIRCUIT_STATUS_PLANNED +from circuits.choices import * from circuits.filters import * from circuits.models import Circuit, CircuitTermination, CircuitType, Provider from dcim.models import Region, Site @@ -8,7 +8,7 @@ from dcim.models import Region, Site class ProviderTestCase(TestCase): queryset = Provider.objects.all() - filterset = ProviderFilter + filterset = ProviderFilterSet @classmethod def setUpTestData(cls): @@ -91,7 +91,7 @@ class ProviderTestCase(TestCase): class CircuitTypeTestCase(TestCase): queryset = CircuitType.objects.all() - filterset = CircuitTypeFilter + filterset = CircuitTypeFilterSet @classmethod def setUpTestData(cls): @@ -117,7 +117,7 @@ class CircuitTypeTestCase(TestCase): class CircuitTestCase(TestCase): queryset = Circuit.objects.all() - filterset = CircuitFilter + filterset = CircuitFilterSet @classmethod def setUpTestData(cls): @@ -151,12 +151,12 @@ class CircuitTestCase(TestCase): Provider.objects.bulk_create(providers) circuits = ( - Circuit(provider=providers[0], type=circuit_types[0], cid='Test Circuit 1', install_date='2020-01-01', commit_rate=1000, status=CIRCUIT_STATUS_ACTIVE), - Circuit(provider=providers[0], type=circuit_types[0], cid='Test Circuit 2', install_date='2020-01-02', commit_rate=2000, status=CIRCUIT_STATUS_ACTIVE), - Circuit(provider=providers[0], type=circuit_types[0], cid='Test Circuit 3', install_date='2020-01-03', commit_rate=3000, status=CIRCUIT_STATUS_PLANNED), - Circuit(provider=providers[1], type=circuit_types[1], cid='Test Circuit 4', install_date='2020-01-04', commit_rate=4000, status=CIRCUIT_STATUS_PLANNED), - Circuit(provider=providers[1], type=circuit_types[1], cid='Test Circuit 5', install_date='2020-01-05', commit_rate=5000, status=CIRCUIT_STATUS_OFFLINE), - Circuit(provider=providers[1], type=circuit_types[1], cid='Test Circuit 6', install_date='2020-01-06', commit_rate=6000, status=CIRCUIT_STATUS_OFFLINE), + Circuit(provider=providers[0], type=circuit_types[0], cid='Test Circuit 1', install_date='2020-01-01', commit_rate=1000, status=CircuitStatusChoices.STATUS_ACTIVE), + Circuit(provider=providers[0], type=circuit_types[0], cid='Test Circuit 2', install_date='2020-01-02', commit_rate=2000, status=CircuitStatusChoices.STATUS_ACTIVE), + Circuit(provider=providers[0], type=circuit_types[0], cid='Test Circuit 3', install_date='2020-01-03', commit_rate=3000, status=CircuitStatusChoices.STATUS_PLANNED), + Circuit(provider=providers[1], type=circuit_types[1], cid='Test Circuit 4', install_date='2020-01-04', commit_rate=4000, status=CircuitStatusChoices.STATUS_PLANNED), + Circuit(provider=providers[1], type=circuit_types[1], cid='Test Circuit 5', install_date='2020-01-05', commit_rate=5000, status=CircuitStatusChoices.STATUS_OFFLINE), + Circuit(provider=providers[1], type=circuit_types[1], cid='Test Circuit 6', install_date='2020-01-06', commit_rate=6000, status=CircuitStatusChoices.STATUS_OFFLINE), ) Circuit.objects.bulk_create(circuits) @@ -199,7 +199,7 @@ class CircuitTestCase(TestCase): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3) def test_status(self): - params = {'status': [CIRCUIT_STATUS_ACTIVE, CIRCUIT_STATUS_PLANNED]} + params = {'status': [CircuitStatusChoices.STATUS_ACTIVE, CircuitStatusChoices.STATUS_PLANNED]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) def test_region(self): @@ -219,7 +219,7 @@ class CircuitTestCase(TestCase): class CircuitTerminationTestCase(TestCase): queryset = CircuitTermination.objects.all() - filterset = CircuitTerminationFilter + filterset = CircuitTerminationFilterSet @classmethod def setUpTestData(cls): diff --git a/netbox/circuits/tests/test_views.py b/netbox/circuits/tests/test_views.py index cb0ea0a32..576437ef1 100644 --- a/netbox/circuits/tests/test_views.py +++ b/netbox/circuits/tests/test_views.py @@ -10,7 +10,12 @@ from utilities.testing import create_test_user class ProviderTestCase(TestCase): def setUp(self): - user = create_test_user(permissions=['circuits.view_provider']) + user = create_test_user( + permissions=[ + 'circuits.view_provider', + 'circuits.add_provider', + ] + ) self.client = Client() self.client.force_login(user) @@ -36,11 +41,30 @@ class ProviderTestCase(TestCase): response = self.client.get(provider.get_absolute_url()) 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): def setUp(self): - user = create_test_user(permissions=['circuits.view_circuittype']) + user = create_test_user( + permissions=[ + 'circuits.view_circuittype', + 'circuits.add_circuittype', + ] + ) self.client = Client() self.client.force_login(user) @@ -57,11 +81,30 @@ class CircuitTypeTestCase(TestCase): response = self.client.get(url) 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): def setUp(self): - user = create_test_user(permissions=['circuits.view_circuit']) + user = create_test_user( + permissions=[ + 'circuits.view_circuit', + 'circuits.add_circuit', + ] + ) self.client = Client() self.client.force_login(user) @@ -93,3 +136,17 @@ class CircuitTestCase(TestCase): circuit = Circuit.objects.first() response = self.client.get(circuit.get_absolute_url()) 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) diff --git a/netbox/circuits/views.py b/netbox/circuits/views.py index 5d76e38ee..15cf901c1 100644 --- a/netbox/circuits/views.py +++ b/netbox/circuits/views.py @@ -8,14 +8,14 @@ from django.shortcuts import get_object_or_404, redirect, render from django.views.generic import View from django_tables2 import RequestConfig -from extras.models import Graph, GRAPH_TYPE_PROVIDER +from extras.models import Graph from utilities.forms import ConfirmationForm from utilities.paginator import EnhancedPaginator from utilities.views import ( BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView, ) from . import filters, forms, tables -from .constants import TERM_SIDE_A, TERM_SIDE_Z +from .choices import CircuitTerminationSideChoices from .models import Circuit, CircuitTermination, CircuitType, Provider @@ -26,8 +26,8 @@ from .models import Circuit, CircuitTermination, CircuitType, Provider class ProviderListView(PermissionRequiredMixin, ObjectListView): permission_required = 'circuits.view_provider' queryset = Provider.objects.annotate(count_circuits=Count('circuits')) - filter = filters.ProviderFilter - filter_form = forms.ProviderFilterForm + filterset = filters.ProviderFilterSet + filterset_form = forms.ProviderFilterForm table = tables.ProviderDetailTable template_name = 'circuits/provider_list.html' @@ -39,7 +39,7 @@ class ProviderView(PermissionRequiredMixin, View): provider = get_object_or_404(Provider, slug=slug) circuits = Circuit.objects.filter(provider=provider).prefetch_related('type', 'tenant', 'terminations__site') - show_graphs = Graph.objects.filter(type=GRAPH_TYPE_PROVIDER).exists() + show_graphs = Graph.objects.filter(type__model='provider').exists() circuits_table = tables.CircuitTable(circuits, orderable=False) circuits_table.columns.hide('provider') @@ -85,7 +85,7 @@ class ProviderBulkImportView(PermissionRequiredMixin, BulkImportView): class ProviderBulkEditView(PermissionRequiredMixin, BulkEditView): permission_required = 'circuits.change_provider' queryset = Provider.objects.all() - filter = filters.ProviderFilter + filterset = filters.ProviderFilterSet table = tables.ProviderTable form = forms.ProviderBulkEditForm default_return_url = 'circuits:provider_list' @@ -94,7 +94,7 @@ class ProviderBulkEditView(PermissionRequiredMixin, BulkEditView): class ProviderBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'circuits.delete_provider' queryset = Provider.objects.all() - filter = filters.ProviderFilter + filterset = filters.ProviderFilterSet table = tables.ProviderTable default_return_url = 'circuits:provider_list' @@ -148,8 +148,8 @@ class CircuitListView(PermissionRequiredMixin, ObjectListView): a_side=Subquery(_terminations.filter(term_side='A').values('site__name')[:1]), z_side=Subquery(_terminations.filter(term_side='Z').values('site__name')[:1]), ) - filter = filters.CircuitFilter - filter_form = forms.CircuitFilterForm + filterset = filters.CircuitFilterSet + filterset_form = forms.CircuitFilterForm table = tables.CircuitTable template_name = 'circuits/circuit_list.html' @@ -163,12 +163,12 @@ class CircuitView(PermissionRequiredMixin, View): termination_a = CircuitTermination.objects.prefetch_related( 'site__region', 'connected_endpoint__device' ).filter( - circuit=circuit, term_side=TERM_SIDE_A + circuit=circuit, term_side=CircuitTerminationSideChoices.SIDE_A ).first() termination_z = CircuitTermination.objects.prefetch_related( 'site__region', 'connected_endpoint__device' ).filter( - circuit=circuit, term_side=TERM_SIDE_Z + circuit=circuit, term_side=CircuitTerminationSideChoices.SIDE_Z ).first() return render(request, 'circuits/circuit.html', { @@ -206,7 +206,7 @@ class CircuitBulkImportView(PermissionRequiredMixin, BulkImportView): class CircuitBulkEditView(PermissionRequiredMixin, BulkEditView): permission_required = 'circuits.change_circuit' queryset = Circuit.objects.prefetch_related('provider', 'type', 'tenant').prefetch_related('terminations__site') - filter = filters.CircuitFilter + filterset = filters.CircuitFilterSet table = tables.CircuitTable form = forms.CircuitBulkEditForm default_return_url = 'circuits:circuit_list' @@ -215,7 +215,7 @@ class CircuitBulkEditView(PermissionRequiredMixin, BulkEditView): class CircuitBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'circuits.delete_circuit' queryset = Circuit.objects.prefetch_related('provider', 'type', 'tenant').prefetch_related('terminations__site') - filter = filters.CircuitFilter + filterset = filters.CircuitFilterSet table = tables.CircuitTable default_return_url = 'circuits:circuit_list' @@ -224,8 +224,12 @@ class CircuitBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): def circuit_terminations_swap(request, pk): circuit = get_object_or_404(Circuit, pk=pk) - termination_a = CircuitTermination.objects.filter(circuit=circuit, term_side=TERM_SIDE_A).first() - termination_z = CircuitTermination.objects.filter(circuit=circuit, term_side=TERM_SIDE_Z).first() + termination_a = CircuitTermination.objects.filter( + circuit=circuit, term_side=CircuitTerminationSideChoices.SIDE_A + ).first() + termination_z = CircuitTermination.objects.filter( + circuit=circuit, term_side=CircuitTerminationSideChoices.SIDE_Z + ).first() if not termination_a and not termination_z: messages.error(request, "No terminations have been defined for circuit {}.".format(circuit)) return redirect('circuits:circuit', pk=circuit.pk) diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index db5fe992f..f0382a3f5 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -4,6 +4,7 @@ from rest_framework import serializers from rest_framework.validators import UniqueTogetherValidator from taggit_serializer.serializers import TaggitSerializer, TagListSerializerField +from dcim.choices import * from dcim.constants import * from dcim.models import ( Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, @@ -67,7 +68,7 @@ class RegionSerializer(serializers.ModelSerializer): class SiteSerializer(TaggitSerializer, CustomFieldModelSerializer): - status = ChoiceField(choices=SITE_STATUS_CHOICES, required=False) + status = ChoiceField(choices=SiteStatusChoices, required=False) region = NestedRegionSerializer(required=False, allow_null=True) tenant = NestedTenantSerializer(required=False, allow_null=True) time_zone = TimeZoneField(required=False) @@ -107,18 +108,18 @@ class RackRoleSerializer(ValidatedModelSerializer): class Meta: model = RackRole - fields = ['id', 'name', 'slug', 'color', 'rack_count'] + fields = ['id', 'name', 'slug', 'color', 'description', 'rack_count'] class RackSerializer(TaggitSerializer, CustomFieldModelSerializer): site = NestedSiteSerializer() group = NestedRackGroupSerializer(required=False, allow_null=True, default=None) tenant = NestedTenantSerializer(required=False, allow_null=True) - status = ChoiceField(choices=RACK_STATUS_CHOICES, required=False) + status = ChoiceField(choices=RackStatusChoices, required=False) role = NestedRackRoleSerializer(required=False, allow_null=True) - type = ChoiceField(choices=RACK_TYPE_CHOICES, required=False, allow_null=True) - width = ChoiceField(choices=RACK_WIDTH_CHOICES, required=False) - outer_unit = ChoiceField(choices=RACK_DIMENSION_UNIT_CHOICES, required=False) + type = ChoiceField(choices=RackTypeChoices, required=False, allow_null=True) + width = ChoiceField(choices=RackWidthChoices, required=False) + outer_unit = ChoiceField(choices=RackDimensionUnitChoices, required=False) tags = TagListSerializerField(required=False) device_count = serializers.IntegerField(read_only=True) powerfeed_count = serializers.IntegerField(read_only=True) @@ -156,7 +157,7 @@ class RackUnitSerializer(serializers.Serializer): """ id = serializers.IntegerField(read_only=True) name = serializers.CharField(read_only=True) - face = serializers.IntegerField(read_only=True) + face = ChoiceField(choices=DeviceFaceChoices, read_only=True) device = NestedDeviceSerializer(read_only=True) @@ -170,6 +171,31 @@ class RackReservationSerializer(ValidatedModelSerializer): 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 # @@ -186,7 +212,7 @@ class ManufacturerSerializer(ValidatedModelSerializer): class DeviceTypeSerializer(TaggitSerializer, CustomFieldModelSerializer): manufacturer = NestedManufacturerSerializer() - subdevice_role = ChoiceField(choices=SUBDEVICE_ROLE_CHOICES, required=False, allow_null=True) + subdevice_role = ChoiceField(choices=SubdeviceRoleChoices, required=False, allow_null=True) tags = TagListSerializerField(required=False) device_count = serializers.IntegerField(read_only=True) @@ -200,58 +226,72 @@ class DeviceTypeSerializer(TaggitSerializer, CustomFieldModelSerializer): class ConsolePortTemplateSerializer(ValidatedModelSerializer): device_type = NestedDeviceTypeSerializer() + type = ChoiceField( + choices=ConsolePortTypeChoices, + required=False + ) class Meta: model = ConsolePortTemplate - fields = ['id', 'device_type', 'name'] + fields = ['id', 'device_type', 'name', 'type'] class ConsoleServerPortTemplateSerializer(ValidatedModelSerializer): device_type = NestedDeviceTypeSerializer() + type = ChoiceField( + choices=ConsolePortTypeChoices, + required=False + ) class Meta: model = ConsoleServerPortTemplate - fields = ['id', 'device_type', 'name'] + fields = ['id', 'device_type', 'name', 'type'] class PowerPortTemplateSerializer(ValidatedModelSerializer): device_type = NestedDeviceTypeSerializer() + type = ChoiceField( + choices=PowerPortTypeChoices, + required=False + ) class Meta: model = PowerPortTemplate - fields = ['id', 'device_type', 'name', 'maximum_draw', 'allocated_draw'] + fields = ['id', 'device_type', 'name', 'type', 'maximum_draw', 'allocated_draw'] class PowerOutletTemplateSerializer(ValidatedModelSerializer): device_type = NestedDeviceTypeSerializer() + type = ChoiceField( + choices=PowerOutletTypeChoices, + required=False + ) power_port = PowerPortTemplateSerializer( required=False ) feed_leg = ChoiceField( - choices=POWERFEED_LEG_CHOICES, + choices=PowerOutletFeedLegChoices, required=False, allow_null=True ) class Meta: model = PowerOutletTemplate - fields = ['id', 'device_type', 'name', 'power_port', 'feed_leg'] + fields = ['id', 'device_type', 'name', 'type', 'power_port', 'feed_leg'] class InterfaceTemplateSerializer(ValidatedModelSerializer): device_type = NestedDeviceTypeSerializer() - type = ChoiceField(choices=IFACE_TYPE_CHOICES, required=False) - # TODO: Remove in v2.7 (backward-compatibility for form_factor) - form_factor = ChoiceField(choices=IFACE_TYPE_CHOICES, required=False) + type = ChoiceField(choices=InterfaceTypeChoices, required=False) class Meta: model = InterfaceTemplate - fields = ['id', 'device_type', 'name', 'type', 'form_factor', 'mgmt_only'] + fields = ['id', 'device_type', 'name', 'type', 'mgmt_only'] class RearPortTemplateSerializer(ValidatedModelSerializer): device_type = NestedDeviceTypeSerializer() - type = ChoiceField(choices=PORT_TYPE_CHOICES) + type = ChoiceField(choices=PortTypeChoices) class Meta: model = RearPortTemplate @@ -260,7 +300,7 @@ class RearPortTemplateSerializer(ValidatedModelSerializer): class FrontPortTemplateSerializer(ValidatedModelSerializer): device_type = NestedDeviceTypeSerializer() - type = ChoiceField(choices=PORT_TYPE_CHOICES) + type = ChoiceField(choices=PortTypeChoices) rear_port = NestedRearPortTemplateSerializer() class Meta: @@ -286,7 +326,9 @@ class DeviceRoleSerializer(ValidatedModelSerializer): class Meta: model = DeviceRole - fields = ['id', 'name', 'slug', 'color', 'vm_role', 'device_count', 'virtualmachine_count'] + fields = [ + 'id', 'name', 'slug', 'color', 'vm_role', 'description', 'device_count', 'virtualmachine_count', + ] class PlatformSerializer(ValidatedModelSerializer): @@ -309,8 +351,8 @@ class DeviceSerializer(TaggitSerializer, CustomFieldModelSerializer): platform = NestedPlatformSerializer(required=False, allow_null=True) site = NestedSiteSerializer() rack = NestedRackSerializer(required=False, allow_null=True) - face = ChoiceField(choices=RACK_FACE_CHOICES, required=False, allow_null=True) - status = ChoiceField(choices=DEVICE_STATUS_CHOICES, required=False) + face = ChoiceField(choices=DeviceFaceChoices, required=False, allow_null=True) + status = ChoiceField(choices=DeviceStatusChoices, required=False) primary_ip = NestedIPAddressSerializer(read_only=True) primary_ip4 = NestedIPAddressSerializer(required=False, allow_null=True) primary_ip6 = NestedIPAddressSerializer(required=False, allow_null=True) @@ -376,37 +418,49 @@ class DeviceNAPALMSerializer(serializers.Serializer): class ConsoleServerPortSerializer(TaggitSerializer, ConnectedEndpointSerializer): device = NestedDeviceSerializer() + type = ChoiceField( + choices=ConsolePortTypeChoices, + required=False + ) cable = NestedCableSerializer(read_only=True) tags = TagListSerializerField(required=False) class Meta: model = ConsoleServerPort fields = [ - 'id', 'device', 'name', 'description', 'connected_endpoint_type', 'connected_endpoint', 'connection_status', - 'cable', 'tags', + 'id', 'device', 'name', 'type', 'description', 'connected_endpoint_type', 'connected_endpoint', + 'connection_status', 'cable', 'tags', ] class ConsolePortSerializer(TaggitSerializer, ConnectedEndpointSerializer): device = NestedDeviceSerializer() + type = ChoiceField( + choices=ConsolePortTypeChoices, + required=False + ) cable = NestedCableSerializer(read_only=True) tags = TagListSerializerField(required=False) class Meta: model = ConsolePort fields = [ - 'id', 'device', 'name', 'description', 'connected_endpoint_type', 'connected_endpoint', 'connection_status', - 'cable', 'tags', + 'id', 'device', 'name', 'type', 'description', 'connected_endpoint_type', 'connected_endpoint', + 'connection_status', 'cable', 'tags', ] class PowerOutletSerializer(TaggitSerializer, ConnectedEndpointSerializer): device = NestedDeviceSerializer() + type = ChoiceField( + choices=PowerOutletTypeChoices, + required=False + ) power_port = NestedPowerPortSerializer( required=False ) feed_leg = ChoiceField( - choices=POWERFEED_LEG_CHOICES, + choices=PowerOutletFeedLegChoices, required=False, allow_null=True ) @@ -420,31 +474,33 @@ class PowerOutletSerializer(TaggitSerializer, ConnectedEndpointSerializer): class Meta: model = PowerOutlet fields = [ - 'id', 'device', 'name', 'power_port', 'feed_leg', 'description', 'connected_endpoint_type', + 'id', 'device', 'name', 'type', 'power_port', 'feed_leg', 'description', 'connected_endpoint_type', 'connected_endpoint', 'connection_status', 'cable', 'tags', ] class PowerPortSerializer(TaggitSerializer, ConnectedEndpointSerializer): device = NestedDeviceSerializer() + type = ChoiceField( + choices=PowerPortTypeChoices, + required=False + ) cable = NestedCableSerializer(read_only=True) tags = TagListSerializerField(required=False) class Meta: model = PowerPort fields = [ - 'id', 'device', 'name', 'maximum_draw', 'allocated_draw', 'description', 'connected_endpoint_type', + 'id', 'device', 'name', 'type', 'maximum_draw', 'allocated_draw', 'description', 'connected_endpoint_type', 'connected_endpoint', 'connection_status', 'cable', 'tags', ] class InterfaceSerializer(TaggitSerializer, ConnectedEndpointSerializer): device = NestedDeviceSerializer() - type = ChoiceField(choices=IFACE_TYPE_CHOICES, required=False) - # TODO: Remove in v2.7 (backward-compatibility for form_factor) - form_factor = ChoiceField(choices=IFACE_TYPE_CHOICES, required=False) + type = ChoiceField(choices=InterfaceTypeChoices, required=False) lag = NestedInterfaceSerializer(required=False, allow_null=True) - mode = ChoiceField(choices=IFACE_MODE_CHOICES, required=False, allow_null=True) + mode = ChoiceField(choices=InterfaceModeChoices, required=False, allow_null=True) untagged_vlan = NestedVLANSerializer(required=False, allow_null=True) tagged_vlans = SerializedPKRelatedField( queryset=VLAN.objects.all(), @@ -458,9 +514,9 @@ class InterfaceSerializer(TaggitSerializer, ConnectedEndpointSerializer): class Meta: model = Interface fields = [ - 'id', 'device', 'name', 'type', 'form_factor', 'enabled', 'lag', 'mtu', 'mac_address', 'mgmt_only', - 'description', 'connected_endpoint_type', 'connected_endpoint', 'connection_status', 'cable', 'mode', - 'untagged_vlan', 'tagged_vlans', 'tags', 'count_ipaddresses', + 'id', 'device', 'name', 'type', 'enabled', 'lag', 'mtu', 'mac_address', 'mgmt_only', 'description', + 'connected_endpoint_type', 'connected_endpoint', 'connection_status', 'cable', 'mode', 'untagged_vlan', + 'tagged_vlans', 'tags', 'count_ipaddresses', ] # TODO: This validation should be handled by Interface.clean() @@ -486,7 +542,7 @@ class InterfaceSerializer(TaggitSerializer, ConnectedEndpointSerializer): class RearPortSerializer(TaggitSerializer, ValidatedModelSerializer): device = NestedDeviceSerializer() - type = ChoiceField(choices=PORT_TYPE_CHOICES) + type = ChoiceField(choices=PortTypeChoices) cable = NestedCableSerializer(read_only=True) tags = TagListSerializerField(required=False) @@ -508,7 +564,7 @@ class FrontPortRearPortSerializer(WritableNestedSerializer): class FrontPortSerializer(TaggitSerializer, ValidatedModelSerializer): device = NestedDeviceSerializer() - type = ChoiceField(choices=PORT_TYPE_CHOICES) + type = ChoiceField(choices=PortTypeChoices) rear_port = FrontPortRearPortSerializer() cable = NestedCableSerializer(read_only=True) tags = TagListSerializerField(required=False) @@ -553,15 +609,15 @@ class InventoryItemSerializer(TaggitSerializer, ValidatedModelSerializer): class CableSerializer(ValidatedModelSerializer): termination_a_type = ContentTypeField( - queryset=ContentType.objects.filter(model__in=CABLE_TERMINATION_TYPES) + queryset=ContentType.objects.filter(CABLE_TERMINATION_MODELS) ) termination_b_type = ContentTypeField( - queryset=ContentType.objects.filter(model__in=CABLE_TERMINATION_TYPES) + queryset=ContentType.objects.filter(CABLE_TERMINATION_MODELS) ) termination_a = serializers.SerializerMethodField(read_only=True) termination_b = serializers.SerializerMethodField(read_only=True) - status = ChoiceField(choices=CONNECTION_STATUS_CHOICES, required=False) - length_unit = ChoiceField(choices=CABLE_LENGTH_UNIT_CHOICES, required=False, allow_null=True) + status = ChoiceField(choices=CableStatusChoices, required=False) + length_unit = ChoiceField(choices=CableLengthUnitChoices, required=False, allow_null=True) class Meta: model = Cable @@ -666,20 +722,20 @@ class PowerFeedSerializer(TaggitSerializer, CustomFieldModelSerializer): default=None ) type = ChoiceField( - choices=POWERFEED_TYPE_CHOICES, - default=POWERFEED_TYPE_PRIMARY + choices=PowerFeedTypeChoices, + default=PowerFeedTypeChoices.TYPE_PRIMARY ) status = ChoiceField( - choices=POWERFEED_STATUS_CHOICES, - default=POWERFEED_STATUS_ACTIVE + choices=PowerFeedStatusChoices, + default=PowerFeedStatusChoices.STATUS_ACTIVE ) supply = ChoiceField( - choices=POWERFEED_SUPPLY_CHOICES, - default=POWERFEED_SUPPLY_AC + choices=PowerFeedSupplyChoices, + default=PowerFeedSupplyChoices.SUPPLY_AC ) phase = ChoiceField( - choices=POWERFEED_PHASE_CHOICES, - default=POWERFEED_PHASE_SINGLE + choices=PowerFeedPhaseChoices, + default=PowerFeedPhaseChoices.PHASE_SINGLE ) tags = TagListSerializerField( required=False diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index 6e5523206..8bb127f67 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -2,7 +2,7 @@ from collections import OrderedDict from django.conf import settings from django.db.models import Count, F -from django.http import HttpResponseForbidden +from django.http import HttpResponseForbidden, HttpResponse from django.shortcuts import get_object_or_404 from drf_yasg import openapi from drf_yasg.openapi import Parameter @@ -23,7 +23,6 @@ from dcim.models import ( ) from extras.api.serializers import RenderedGraphSerializer 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 ipam.models import Prefix, VLAN from utilities.api import ( @@ -41,21 +40,26 @@ from .exceptions import MissingFilterException class DCIMFieldChoicesViewSet(FieldChoicesViewSet): fields = ( - (Cable, ['length_unit', 'status', 'termination_a_type', 'termination_b_type', 'type']), - (ConsolePort, ['connection_status']), - (Device, ['face', 'status']), - (DeviceType, ['subdevice_role']), - (FrontPort, ['type']), - (FrontPortTemplate, ['type']), - (Interface, ['type', 'mode']), - (InterfaceTemplate, ['type']), - (PowerOutlet, ['feed_leg']), - (PowerOutletTemplate, ['feed_leg']), - (PowerPort, ['connection_status']), - (Rack, ['outer_unit', 'status', 'type', 'width']), - (RearPort, ['type']), - (RearPortTemplate, ['type']), - (Site, ['status']), + (serializers.CableSerializer, ['length_unit', 'status', 'termination_a_type', 'termination_b_type', 'type']), + (serializers.ConsolePortSerializer, ['type', 'connection_status']), + (serializers.ConsolePortTemplateSerializer, ['type']), + (serializers.ConsoleServerPortSerializer, ['type']), + (serializers.ConsoleServerPortTemplateSerializer, ['type']), + (serializers.DeviceSerializer, ['face', 'status']), + (serializers.DeviceTypeSerializer, ['subdevice_role']), + (serializers.FrontPortSerializer, ['type']), + (serializers.FrontPortTemplateSerializer, ['type']), + (serializers.InterfaceSerializer, ['type', 'mode']), + (serializers.InterfaceTemplateSerializer, ['type']), + (serializers.PowerFeedSerializer, ['phase', 'status', 'supply', 'type']), + (serializers.PowerOutletSerializer, ['type', 'feed_leg']), + (serializers.PowerOutletTemplateSerializer, ['type', 'feed_leg']), + (serializers.PowerPortSerializer, ['type', 'connection_status']), + (serializers.PowerPortTemplateSerializer, ['type']), + (serializers.RackSerializer, ['outer_unit', 'status', 'type', 'width']), + (serializers.RearPortSerializer, ['type']), + (serializers.RearPortTemplateSerializer, ['type']), + (serializers.SiteSerializer, ['status']), ) @@ -102,7 +106,7 @@ class RegionViewSet(ModelViewSet): site_count=Count('sites') ) serializer_class = serializers.RegionSerializer - filterset_class = filters.RegionFilter + filterset_class = filters.RegionFilterSet # @@ -121,7 +125,7 @@ class SiteViewSet(CustomFieldModelViewSet): virtualmachine_count=get_subquery(VirtualMachine, 'cluster__site'), ) serializer_class = serializers.SiteSerializer - filterset_class = filters.SiteFilter + filterset_class = filters.SiteFilterSet @action(detail=True) def graphs(self, request, pk): @@ -129,7 +133,7 @@ class SiteViewSet(CustomFieldModelViewSet): A convenience method for rendering graphs for a particular site. """ site = get_object_or_404(Site, pk=pk) - queryset = Graph.objects.filter(type=GRAPH_TYPE_SITE) + queryset = Graph.objects.filter(type__model='site') serializer = RenderedGraphSerializer(queryset, many=True, context={'graphed_object': site}) return Response(serializer.data) @@ -143,7 +147,7 @@ class RackGroupViewSet(ModelViewSet): rack_count=Count('racks') ) serializer_class = serializers.RackGroupSerializer - filterset_class = filters.RackGroupFilter + filterset_class = filters.RackGroupFilterSet # @@ -155,7 +159,7 @@ class RackRoleViewSet(ModelViewSet): rack_count=Count('racks') ) serializer_class = serializers.RackRoleSerializer - filterset_class = filters.RackRoleFilter + filterset_class = filters.RackRoleFilterSet # @@ -170,15 +174,17 @@ class RackViewSet(CustomFieldModelViewSet): powerfeed_count=get_subquery(PowerFeed, 'rack') ) serializer_class = serializers.RackSerializer - filterset_class = filters.RackFilter + filterset_class = filters.RackFilterSet + @swagger_auto_schema(deprecated=True) @action(detail=True) def units(self, request, pk=None): """ List rack units (by rack) """ + # TODO: Remove this action detail route in v2.8 rack = get_object_or_404(Rack, pk=pk) - face = request.GET.get('face', 0) + face = request.GET.get('face', 'front') exclude_pk = request.GET.get('exclude', None) if exclude_pk is not None: try: @@ -197,6 +203,39 @@ class RackViewSet(CustomFieldModelViewSet): rack_units = serializers.RackUnitSerializer(page, many=True, context={'request': request}) 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 @@ -205,7 +244,7 @@ class RackViewSet(CustomFieldModelViewSet): class RackReservationViewSet(ModelViewSet): queryset = RackReservation.objects.prefetch_related('rack', 'user', 'tenant') serializer_class = serializers.RackReservationSerializer - filterset_class = filters.RackReservationFilter + filterset_class = filters.RackReservationFilterSet # Assign user from request def perform_create(self, serializer): @@ -223,7 +262,7 @@ class ManufacturerViewSet(ModelViewSet): platform_count=get_subquery(Platform, 'manufacturer') ) serializer_class = serializers.ManufacturerSerializer - filterset_class = filters.ManufacturerFilter + filterset_class = filters.ManufacturerFilterSet # @@ -235,7 +274,7 @@ class DeviceTypeViewSet(CustomFieldModelViewSet): device_count=Count('instances') ) serializer_class = serializers.DeviceTypeSerializer - filterset_class = filters.DeviceTypeFilter + filterset_class = filters.DeviceTypeFilterSet # @@ -245,49 +284,49 @@ class DeviceTypeViewSet(CustomFieldModelViewSet): class ConsolePortTemplateViewSet(ModelViewSet): queryset = ConsolePortTemplate.objects.prefetch_related('device_type__manufacturer') serializer_class = serializers.ConsolePortTemplateSerializer - filterset_class = filters.ConsolePortTemplateFilter + filterset_class = filters.ConsolePortTemplateFilterSet class ConsoleServerPortTemplateViewSet(ModelViewSet): queryset = ConsoleServerPortTemplate.objects.prefetch_related('device_type__manufacturer') serializer_class = serializers.ConsoleServerPortTemplateSerializer - filterset_class = filters.ConsoleServerPortTemplateFilter + filterset_class = filters.ConsoleServerPortTemplateFilterSet class PowerPortTemplateViewSet(ModelViewSet): queryset = PowerPortTemplate.objects.prefetch_related('device_type__manufacturer') serializer_class = serializers.PowerPortTemplateSerializer - filterset_class = filters.PowerPortTemplateFilter + filterset_class = filters.PowerPortTemplateFilterSet class PowerOutletTemplateViewSet(ModelViewSet): queryset = PowerOutletTemplate.objects.prefetch_related('device_type__manufacturer') serializer_class = serializers.PowerOutletTemplateSerializer - filterset_class = filters.PowerOutletTemplateFilter + filterset_class = filters.PowerOutletTemplateFilterSet class InterfaceTemplateViewSet(ModelViewSet): queryset = InterfaceTemplate.objects.prefetch_related('device_type__manufacturer') serializer_class = serializers.InterfaceTemplateSerializer - filterset_class = filters.InterfaceTemplateFilter + filterset_class = filters.InterfaceTemplateFilterSet class FrontPortTemplateViewSet(ModelViewSet): queryset = FrontPortTemplate.objects.prefetch_related('device_type__manufacturer') serializer_class = serializers.FrontPortTemplateSerializer - filterset_class = filters.FrontPortTemplateFilter + filterset_class = filters.FrontPortTemplateFilterSet class RearPortTemplateViewSet(ModelViewSet): queryset = RearPortTemplate.objects.prefetch_related('device_type__manufacturer') serializer_class = serializers.RearPortTemplateSerializer - filterset_class = filters.RearPortTemplateFilter + filterset_class = filters.RearPortTemplateFilterSet class DeviceBayTemplateViewSet(ModelViewSet): queryset = DeviceBayTemplate.objects.prefetch_related('device_type__manufacturer') serializer_class = serializers.DeviceBayTemplateSerializer - filterset_class = filters.DeviceBayTemplateFilter + filterset_class = filters.DeviceBayTemplateFilterSet # @@ -300,7 +339,7 @@ class DeviceRoleViewSet(ModelViewSet): virtualmachine_count=get_subquery(VirtualMachine, 'role') ) serializer_class = serializers.DeviceRoleSerializer - filterset_class = filters.DeviceRoleFilter + filterset_class = filters.DeviceRoleFilterSet # @@ -313,7 +352,7 @@ class PlatformViewSet(ModelViewSet): virtualmachine_count=get_subquery(VirtualMachine, 'platform') ) serializer_class = serializers.PlatformSerializer - filterset_class = filters.PlatformFilter + filterset_class = filters.PlatformFilterSet # @@ -325,7 +364,7 @@ class DeviceViewSet(CustomFieldModelViewSet): 'device_type__manufacturer', 'device_role', 'tenant', 'platform', 'site', 'rack', 'parent_bay', 'virtual_chassis__master', 'primary_ip4__nat_outside', 'primary_ip6__nat_outside', 'tags', ) - filterset_class = filters.DeviceFilter + filterset_class = filters.DeviceFilterSet def get_serializer_class(self): """ @@ -353,7 +392,7 @@ class DeviceViewSet(CustomFieldModelViewSet): A convenience method for rendering graphs for a particular Device. """ device = get_object_or_404(Device, pk=pk) - queryset = Graph.objects.filter(type=GRAPH_TYPE_DEVICE) + queryset = Graph.objects.filter(type__model='device') serializer = RenderedGraphSerializer(queryset, many=True, context={'graphed_object': device}) return Response(serializer.data) @@ -464,13 +503,13 @@ class DeviceViewSet(CustomFieldModelViewSet): class ConsolePortViewSet(CableTraceMixin, ModelViewSet): queryset = ConsolePort.objects.prefetch_related('device', 'connected_endpoint__device', 'cable', 'tags') serializer_class = serializers.ConsolePortSerializer - filterset_class = filters.ConsolePortFilter + filterset_class = filters.ConsolePortFilterSet class ConsoleServerPortViewSet(CableTraceMixin, ModelViewSet): queryset = ConsoleServerPort.objects.prefetch_related('device', 'connected_endpoint__device', 'cable', 'tags') serializer_class = serializers.ConsoleServerPortSerializer - filterset_class = filters.ConsoleServerPortFilter + filterset_class = filters.ConsoleServerPortFilterSet class PowerPortViewSet(CableTraceMixin, ModelViewSet): @@ -478,13 +517,13 @@ class PowerPortViewSet(CableTraceMixin, ModelViewSet): 'device', '_connected_poweroutlet__device', '_connected_powerfeed', 'cable', 'tags' ) serializer_class = serializers.PowerPortSerializer - filterset_class = filters.PowerPortFilter + filterset_class = filters.PowerPortFilterSet class PowerOutletViewSet(CableTraceMixin, ModelViewSet): queryset = PowerOutlet.objects.prefetch_related('device', 'connected_endpoint__device', 'cable', 'tags') serializer_class = serializers.PowerOutletSerializer - filterset_class = filters.PowerOutletFilter + filterset_class = filters.PowerOutletFilterSet class InterfaceViewSet(CableTraceMixin, ModelViewSet): @@ -494,7 +533,7 @@ class InterfaceViewSet(CableTraceMixin, ModelViewSet): device__isnull=False ) serializer_class = serializers.InterfaceSerializer - filterset_class = filters.InterfaceFilter + filterset_class = filters.InterfaceFilterSet @action(detail=True) def graphs(self, request, pk): @@ -502,7 +541,7 @@ class InterfaceViewSet(CableTraceMixin, ModelViewSet): A convenience method for rendering graphs for a particular interface. """ interface = get_object_or_404(Interface, pk=pk) - queryset = Graph.objects.filter(type=GRAPH_TYPE_INTERFACE) + queryset = Graph.objects.filter(type__model='interface') serializer = RenderedGraphSerializer(queryset, many=True, context={'graphed_object': interface}) return Response(serializer.data) @@ -510,25 +549,25 @@ class InterfaceViewSet(CableTraceMixin, ModelViewSet): class FrontPortViewSet(ModelViewSet): queryset = FrontPort.objects.prefetch_related('device__device_type__manufacturer', 'rear_port', 'cable', 'tags') serializer_class = serializers.FrontPortSerializer - filterset_class = filters.FrontPortFilter + filterset_class = filters.FrontPortFilterSet class RearPortViewSet(ModelViewSet): queryset = RearPort.objects.prefetch_related('device__device_type__manufacturer', 'cable', 'tags') serializer_class = serializers.RearPortSerializer - filterset_class = filters.RearPortFilter + filterset_class = filters.RearPortFilterSet class DeviceBayViewSet(ModelViewSet): queryset = DeviceBay.objects.prefetch_related('installed_device').prefetch_related('tags') serializer_class = serializers.DeviceBaySerializer - filterset_class = filters.DeviceBayFilter + filterset_class = filters.DeviceBayFilterSet class InventoryItemViewSet(ModelViewSet): queryset = InventoryItem.objects.prefetch_related('device', 'manufacturer').prefetch_related('tags') serializer_class = serializers.InventoryItemSerializer - filterset_class = filters.InventoryItemFilter + filterset_class = filters.InventoryItemFilterSet # @@ -542,7 +581,7 @@ class ConsoleConnectionViewSet(ListModelMixin, GenericViewSet): connected_endpoint__isnull=False ) serializer_class = serializers.ConsolePortSerializer - filterset_class = filters.ConsoleConnectionFilter + filterset_class = filters.ConsoleConnectionFilterSet class PowerConnectionViewSet(ListModelMixin, GenericViewSet): @@ -552,7 +591,7 @@ class PowerConnectionViewSet(ListModelMixin, GenericViewSet): _connected_poweroutlet__isnull=False ) serializer_class = serializers.PowerPortSerializer - filterset_class = filters.PowerConnectionFilter + filterset_class = filters.PowerConnectionFilterSet class InterfaceConnectionViewSet(ListModelMixin, GenericViewSet): @@ -564,7 +603,7 @@ class InterfaceConnectionViewSet(ListModelMixin, GenericViewSet): pk__lt=F('_connected_interface') ) serializer_class = serializers.InterfaceConnectionSerializer - filterset_class = filters.InterfaceConnectionFilter + filterset_class = filters.InterfaceConnectionFilterSet # @@ -576,7 +615,7 @@ class CableViewSet(ModelViewSet): 'termination_a', 'termination_b' ) serializer_class = serializers.CableSerializer - filterset_class = filters.CableFilter + filterset_class = filters.CableFilterSet # @@ -588,7 +627,7 @@ class VirtualChassisViewSet(ModelViewSet): member_count=Count('members') ) serializer_class = serializers.VirtualChassisSerializer - filterset_class = filters.VirtualChassisFilter + filterset_class = filters.VirtualChassisFilterSet # @@ -602,7 +641,7 @@ class PowerPanelViewSet(ModelViewSet): powerfeed_count=Count('powerfeeds') ) serializer_class = serializers.PowerPanelSerializer - filterset_class = filters.PowerPanelFilter + filterset_class = filters.PowerPanelFilterSet # @@ -612,7 +651,7 @@ class PowerPanelViewSet(ModelViewSet): class PowerFeedViewSet(CustomFieldModelViewSet): queryset = PowerFeed.objects.prefetch_related('power_panel', 'rack', 'tags') serializer_class = serializers.PowerFeedSerializer - filterset_class = filters.PowerFeedFilter + filterset_class = filters.PowerFeedFilterSet # diff --git a/netbox/dcim/choices.py b/netbox/dcim/choices.py new file mode 100644 index 000000000..0ba7b1f71 --- /dev/null +++ b/netbox/dcim/choices.py @@ -0,0 +1,1076 @@ +from utilities.choices import ChoiceSet + + +# +# Sites +# + +class SiteStatusChoices(ChoiceSet): + + STATUS_ACTIVE = 'active' + STATUS_PLANNED = 'planned' + STATUS_RETIRED = 'retired' + + CHOICES = ( + (STATUS_ACTIVE, 'Active'), + (STATUS_PLANNED, 'Planned'), + (STATUS_RETIRED, 'Retired'), + ) + + LEGACY_MAP = { + STATUS_ACTIVE: 1, + STATUS_PLANNED: 2, + STATUS_RETIRED: 4, + } + + +# +# Racks +# + +class RackTypeChoices(ChoiceSet): + + TYPE_2POST = '2-post-frame' + TYPE_4POST = '4-post-frame' + TYPE_CABINET = '4-post-cabinet' + TYPE_WALLFRAME = 'wall-frame' + TYPE_WALLCABINET = 'wall-cabinet' + + CHOICES = ( + (TYPE_2POST, '2-post frame'), + (TYPE_4POST, '4-post frame'), + (TYPE_CABINET, '4-post cabinet'), + (TYPE_WALLFRAME, 'Wall-mounted frame'), + (TYPE_WALLCABINET, 'Wall-mounted cabinet'), + ) + + LEGACY_MAP = { + TYPE_2POST: 100, + TYPE_4POST: 200, + TYPE_CABINET: 300, + TYPE_WALLFRAME: 1000, + TYPE_WALLCABINET: 1100, + } + + +class RackWidthChoices(ChoiceSet): + + WIDTH_19IN = 19 + WIDTH_23IN = 23 + + CHOICES = ( + (WIDTH_19IN, '19 inches'), + (WIDTH_23IN, '23 inches'), + ) + + +class RackStatusChoices(ChoiceSet): + + STATUS_RESERVED = 'reserved' + STATUS_AVAILABLE = 'available' + STATUS_PLANNED = 'planned' + STATUS_ACTIVE = 'active' + STATUS_DEPRECATED = 'deprecated' + + CHOICES = ( + (STATUS_RESERVED, 'Reserved'), + (STATUS_AVAILABLE, 'Available'), + (STATUS_PLANNED, 'Planned'), + (STATUS_ACTIVE, 'Active'), + (STATUS_DEPRECATED, 'Deprecated'), + ) + + LEGACY_MAP = { + STATUS_RESERVED: 0, + STATUS_AVAILABLE: 1, + STATUS_PLANNED: 2, + STATUS_ACTIVE: 3, + STATUS_DEPRECATED: 4, + } + + +class RackDimensionUnitChoices(ChoiceSet): + + UNIT_MILLIMETER = 'mm' + UNIT_INCH = 'in' + + CHOICES = ( + (UNIT_MILLIMETER, 'Millimeters'), + (UNIT_INCH, 'Inches'), + ) + + LEGACY_MAP = { + UNIT_MILLIMETER: 1000, + UNIT_INCH: 2000, + } + + +class RackElevationDetailRenderChoices(ChoiceSet): + + RENDER_JSON = 'json' + RENDER_SVG = 'svg' + + CHOICES = ( + (RENDER_JSON, 'json'), + (RENDER_SVG, 'svg') + ) + + +# +# DeviceTypes +# + +class SubdeviceRoleChoices(ChoiceSet): + + ROLE_PARENT = 'parent' + ROLE_CHILD = 'child' + + CHOICES = ( + (ROLE_PARENT, 'Parent'), + (ROLE_CHILD, 'Child'), + ) + + LEGACY_MAP = { + ROLE_PARENT: True, + ROLE_CHILD: False, + } + + +# +# Devices +# + +class DeviceFaceChoices(ChoiceSet): + + FACE_FRONT = 'front' + FACE_REAR = 'rear' + + CHOICES = ( + (FACE_FRONT, 'Front'), + (FACE_REAR, 'Rear'), + ) + + LEGACY_MAP = { + FACE_FRONT: 0, + FACE_REAR: 1, + } + + +class DeviceStatusChoices(ChoiceSet): + + STATUS_OFFLINE = 'offline' + STATUS_ACTIVE = 'active' + STATUS_PLANNED = 'planned' + STATUS_STAGED = 'staged' + STATUS_FAILED = 'failed' + STATUS_INVENTORY = 'inventory' + STATUS_DECOMMISSIONING = 'decommissioning' + + CHOICES = ( + (STATUS_OFFLINE, 'Offline'), + (STATUS_ACTIVE, 'Active'), + (STATUS_PLANNED, 'Planned'), + (STATUS_STAGED, 'Staged'), + (STATUS_FAILED, 'Failed'), + (STATUS_INVENTORY, 'Inventory'), + (STATUS_DECOMMISSIONING, 'Decommissioning'), + ) + + LEGACY_MAP = { + STATUS_OFFLINE: 0, + STATUS_ACTIVE: 1, + STATUS_PLANNED: 2, + STATUS_STAGED: 3, + STATUS_FAILED: 4, + STATUS_INVENTORY: 5, + STATUS_DECOMMISSIONING: 6, + } + + +# +# ConsolePorts +# + +class ConsolePortTypeChoices(ChoiceSet): + + TYPE_DE9 = 'de-9' + TYPE_DB25 = 'db-25' + TYPE_RJ12 = 'rj-12' + TYPE_RJ45 = 'rj-45' + TYPE_USB_A = 'usb-a' + TYPE_USB_B = 'usb-b' + TYPE_USB_C = 'usb-c' + TYPE_USB_MINI_A = 'usb-mini-a' + TYPE_USB_MINI_B = 'usb-mini-b' + TYPE_USB_MICRO_A = 'usb-micro-a' + TYPE_USB_MICRO_B = 'usb-micro-b' + TYPE_OTHER = 'other' + + CHOICES = ( + ('Serial', ( + (TYPE_DE9, 'DE-9'), + (TYPE_DB25, 'DB-25'), + (TYPE_RJ12, 'RJ-12'), + (TYPE_RJ45, 'RJ-45'), + )), + ('USB', ( + (TYPE_USB_A, 'USB Type A'), + (TYPE_USB_B, 'USB Type B'), + (TYPE_USB_C, 'USB Type C'), + (TYPE_USB_MINI_A, 'USB Mini A'), + (TYPE_USB_MINI_B, 'USB Mini B'), + (TYPE_USB_MICRO_A, 'USB Micro A'), + (TYPE_USB_MICRO_B, 'USB Micro B'), + )), + ('Other', ( + (TYPE_OTHER, 'Other'), + )), + ) + + +# +# PowerPorts +# + +class PowerPortTypeChoices(ChoiceSet): + + # IEC 60320 + TYPE_IEC_C6 = 'iec-60320-c6' + TYPE_IEC_C8 = 'iec-60320-c8' + TYPE_IEC_C14 = 'iec-60320-c14' + TYPE_IEC_C16 = 'iec-60320-c16' + TYPE_IEC_C20 = 'iec-60320-c20' + # IEC 60309 + TYPE_IEC_PNE4H = 'iec-60309-p-n-e-4h' + TYPE_IEC_PNE6H = 'iec-60309-p-n-e-6h' + TYPE_IEC_PNE9H = 'iec-60309-p-n-e-9h' + TYPE_IEC_2PE4H = 'iec-60309-2p-e-4h' + TYPE_IEC_2PE6H = 'iec-60309-2p-e-6h' + TYPE_IEC_2PE9H = 'iec-60309-2p-e-9h' + TYPE_IEC_3PE4H = 'iec-60309-3p-e-4h' + TYPE_IEC_3PE6H = 'iec-60309-3p-e-6h' + TYPE_IEC_3PE9H = 'iec-60309-3p-e-9h' + TYPE_IEC_3PNE4H = 'iec-60309-3p-n-e-4h' + TYPE_IEC_3PNE6H = 'iec-60309-3p-n-e-6h' + TYPE_IEC_3PNE9H = 'iec-60309-3p-n-e-9h' + # NEMA non-locking + TYPE_NEMA_515P = 'nema-5-15p' + TYPE_NEMA_520P = 'nema-5-20p' + TYPE_NEMA_530P = 'nema-5-30p' + TYPE_NEMA_550P = 'nema-5-50p' + TYPE_NEMA_615P = 'nema-6-15p' + TYPE_NEMA_620P = 'nema-6-20p' + TYPE_NEMA_630P = 'nema-6-30p' + TYPE_NEMA_650P = 'nema-6-50p' + # NEMA locking + TYPE_NEMA_L515P = 'nema-l5-15p' + TYPE_NEMA_L520P = 'nema-l5-20p' + TYPE_NEMA_L530P = 'nema-l5-30p' + TYPE_NEMA_L615P = 'nema-l5-50p' + TYPE_NEMA_L620P = 'nema-l6-20p' + TYPE_NEMA_L630P = 'nema-l6-30p' + TYPE_NEMA_L650P = 'nema-l6-50p' + # California style + TYPE_CS6361C = 'cs6361c' + TYPE_CS6365C = 'cs6365c' + TYPE_CS8165C = 'cs8165c' + TYPE_CS8265C = 'cs8265c' + TYPE_CS8365C = 'cs8365c' + TYPE_CS8465C = 'cs8465c' + # ITA/international + TYPE_ITA_E = 'ita-e' + TYPE_ITA_F = 'ita-f' + TYPE_ITA_EF = 'ita-ef' + TYPE_ITA_G = 'ita-g' + TYPE_ITA_H = 'ita-h' + TYPE_ITA_I = 'ita-i' + TYPE_ITA_J = 'ita-j' + TYPE_ITA_K = 'ita-k' + TYPE_ITA_L = 'ita-l' + TYPE_ITA_M = 'ita-m' + TYPE_ITA_N = 'ita-n' + TYPE_ITA_O = 'ita-o' + + CHOICES = ( + ('IEC 60320', ( + (TYPE_IEC_C6, 'C6'), + (TYPE_IEC_C8, 'C8'), + (TYPE_IEC_C14, 'C14'), + (TYPE_IEC_C16, 'C16'), + (TYPE_IEC_C20, 'C20'), + )), + ('IEC 60309', ( + (TYPE_IEC_PNE4H, 'P+N+E 4H'), + (TYPE_IEC_PNE6H, 'P+N+E 6H'), + (TYPE_IEC_PNE9H, 'P+N+E 9H'), + (TYPE_IEC_2PE4H, '2P+E 4H'), + (TYPE_IEC_2PE6H, '2P+E 6H'), + (TYPE_IEC_2PE9H, '2P+E 9H'), + (TYPE_IEC_3PE4H, '3P+E 4H'), + (TYPE_IEC_3PE6H, '3P+E 6H'), + (TYPE_IEC_3PE9H, '3P+E 9H'), + (TYPE_IEC_3PNE4H, '3P+N+E 4H'), + (TYPE_IEC_3PNE6H, '3P+N+E 6H'), + (TYPE_IEC_3PNE9H, '3P+N+E 9H'), + )), + ('NEMA (Non-locking)', ( + (TYPE_NEMA_515P, 'NEMA 5-15P'), + (TYPE_NEMA_520P, 'NEMA 5-20P'), + (TYPE_NEMA_530P, 'NEMA 5-30P'), + (TYPE_NEMA_550P, 'NEMA 5-50P'), + (TYPE_NEMA_615P, 'NEMA 6-15P'), + (TYPE_NEMA_620P, 'NEMA 6-20P'), + (TYPE_NEMA_630P, 'NEMA 6-30P'), + (TYPE_NEMA_650P, 'NEMA 6-50P'), + )), + ('NEMA (Locking)', ( + (TYPE_NEMA_L515P, 'NEMA L5-15P'), + (TYPE_NEMA_L520P, 'NEMA L5-20P'), + (TYPE_NEMA_L530P, 'NEMA L5-30P'), + (TYPE_NEMA_L615P, 'NEMA L6-15P'), + (TYPE_NEMA_L620P, 'NEMA L6-20P'), + (TYPE_NEMA_L630P, 'NEMA L6-30P'), + (TYPE_NEMA_L650P, 'NEMA L6-50P'), + )), + ('California Style', ( + (TYPE_CS6361C, 'CS6361C'), + (TYPE_CS6365C, 'CS6365C'), + (TYPE_CS8165C, 'CS8165C'), + (TYPE_CS8265C, 'CS8265C'), + (TYPE_CS8365C, 'CS8365C'), + (TYPE_CS8465C, 'CS8465C'), + )), + ('International/ITA', ( + (TYPE_ITA_E, 'ITA Type E (CEE 7/5)'), + (TYPE_ITA_F, 'ITA Type F (CEE 7/4)'), + (TYPE_ITA_EF, 'ITA Type E/F (CEE 7/7)'), + (TYPE_ITA_G, 'ITA Type G (BS 1363)'), + (TYPE_ITA_H, 'ITA Type H'), + (TYPE_ITA_I, 'ITA Type I'), + (TYPE_ITA_J, 'ITA Type J'), + (TYPE_ITA_K, 'ITA Type K'), + (TYPE_ITA_L, 'ITA Type L (CEI 23-50)'), + (TYPE_ITA_M, 'ITA Type M (BS 546)'), + (TYPE_ITA_N, 'ITA Type N'), + (TYPE_ITA_O, 'ITA Type O'), + )), + ) + + +# +# PowerOutlets +# + +class PowerOutletTypeChoices(ChoiceSet): + + # IEC 60320 + TYPE_IEC_C5 = 'iec-60320-c5' + TYPE_IEC_C7 = 'iec-60320-c7' + TYPE_IEC_C13 = 'iec-60320-c13' + TYPE_IEC_C15 = 'iec-60320-c15' + TYPE_IEC_C19 = 'iec-60320-c19' + # IEC 60309 + TYPE_IEC_PNE4H = 'iec-60309-p-n-e-4h' + TYPE_IEC_PNE6H = 'iec-60309-p-n-e-6h' + TYPE_IEC_PNE9H = 'iec-60309-p-n-e-9h' + TYPE_IEC_2PE4H = 'iec-60309-2p-e-4h' + TYPE_IEC_2PE6H = 'iec-60309-2p-e-6h' + TYPE_IEC_2PE9H = 'iec-60309-2p-e-9h' + TYPE_IEC_3PE4H = 'iec-60309-3p-e-4h' + TYPE_IEC_3PE6H = 'iec-60309-3p-e-6h' + TYPE_IEC_3PE9H = 'iec-60309-3p-e-9h' + TYPE_IEC_3PNE4H = 'iec-60309-3p-n-e-4h' + TYPE_IEC_3PNE6H = 'iec-60309-3p-n-e-6h' + TYPE_IEC_3PNE9H = 'iec-60309-3p-n-e-9h' + # NEMA non-locking + TYPE_NEMA_515R = 'nema-5-15r' + TYPE_NEMA_520R = 'nema-5-20r' + TYPE_NEMA_530R = 'nema-5-30r' + TYPE_NEMA_550R = 'nema-5-50r' + TYPE_NEMA_615R = 'nema-6-15r' + TYPE_NEMA_620R = 'nema-6-20r' + TYPE_NEMA_630R = 'nema-6-30r' + TYPE_NEMA_650R = 'nema-6-50r' + # NEMA locking + TYPE_NEMA_L515R = 'nema-l5-15r' + TYPE_NEMA_L520R = 'nema-l5-20r' + TYPE_NEMA_L530R = 'nema-l5-30r' + TYPE_NEMA_L615R = 'nema-l5-50r' + TYPE_NEMA_L620R = 'nema-l6-20r' + TYPE_NEMA_L630R = 'nema-l6-30r' + TYPE_NEMA_L650R = 'nema-l6-50r' + # California style + TYPE_CS6360C = 'CS6360C' + TYPE_CS6364C = 'CS6364C' + TYPE_CS8164C = 'CS8164C' + TYPE_CS8264C = 'CS8264C' + TYPE_CS8364C = 'CS8364C' + TYPE_CS8464C = 'CS8464C' + # ITA/international + TYPE_ITA_E = 'ita-e' + TYPE_ITA_F = 'ita-f' + TYPE_ITA_G = 'ita-g' + TYPE_ITA_H = 'ita-h' + TYPE_ITA_I = 'ita-i' + TYPE_ITA_J = 'ita-j' + TYPE_ITA_K = 'ita-k' + TYPE_ITA_L = 'ita-l' + TYPE_ITA_M = 'ita-m' + TYPE_ITA_N = 'ita-n' + TYPE_ITA_O = 'ita-o' + + CHOICES = ( + ('IEC 60320', ( + (TYPE_IEC_C5, 'C5'), + (TYPE_IEC_C7, 'C7'), + (TYPE_IEC_C13, 'C13'), + (TYPE_IEC_C15, 'C15'), + (TYPE_IEC_C19, 'C19'), + )), + ('IEC 60309', ( + (TYPE_IEC_PNE4H, 'P+N+E 4H'), + (TYPE_IEC_PNE6H, 'P+N+E 6H'), + (TYPE_IEC_PNE9H, 'P+N+E 9H'), + (TYPE_IEC_2PE4H, '2P+E 4H'), + (TYPE_IEC_2PE6H, '2P+E 6H'), + (TYPE_IEC_2PE9H, '2P+E 9H'), + (TYPE_IEC_3PE4H, '3P+E 4H'), + (TYPE_IEC_3PE6H, '3P+E 6H'), + (TYPE_IEC_3PE9H, '3P+E 9H'), + (TYPE_IEC_3PNE4H, '3P+N+E 4H'), + (TYPE_IEC_3PNE6H, '3P+N+E 6H'), + (TYPE_IEC_3PNE9H, '3P+N+E 9H'), + )), + ('NEMA (Non-locking)', ( + (TYPE_NEMA_515R, 'NEMA 5-15R'), + (TYPE_NEMA_520R, 'NEMA 5-20R'), + (TYPE_NEMA_530R, 'NEMA 5-30R'), + (TYPE_NEMA_550R, 'NEMA 5-50R'), + (TYPE_NEMA_615R, 'NEMA 6-15R'), + (TYPE_NEMA_620R, 'NEMA 6-20R'), + (TYPE_NEMA_630R, 'NEMA 6-30R'), + (TYPE_NEMA_650R, 'NEMA 6-50R'), + )), + ('NEMA (Locking)', ( + (TYPE_NEMA_L515R, 'NEMA L5-15R'), + (TYPE_NEMA_L520R, 'NEMA L5-20R'), + (TYPE_NEMA_L530R, 'NEMA L5-30R'), + (TYPE_NEMA_L615R, 'NEMA L6-15R'), + (TYPE_NEMA_L620R, 'NEMA L6-20R'), + (TYPE_NEMA_L630R, 'NEMA L6-30R'), + (TYPE_NEMA_L650R, 'NEMA L6-50R'), + )), + ('California Style', ( + (TYPE_CS6360C, 'CS6360C'), + (TYPE_CS6364C, 'CS6364C'), + (TYPE_CS8164C, 'CS8164C'), + (TYPE_CS8264C, 'CS8264C'), + (TYPE_CS8364C, 'CS8364C'), + (TYPE_CS8464C, 'CS8464C'), + )), + ('ITA/International', ( + (TYPE_ITA_E, 'ITA Type E (CEE7/5)'), + (TYPE_ITA_F, 'ITA Type F (CEE7/3)'), + (TYPE_ITA_G, 'ITA Type G (BS 1363)'), + (TYPE_ITA_H, 'ITA Type H'), + (TYPE_ITA_I, 'ITA Type I'), + (TYPE_ITA_J, 'ITA Type J'), + (TYPE_ITA_K, 'ITA Type K'), + (TYPE_ITA_L, 'ITA Type L (CEI 23-50)'), + (TYPE_ITA_M, 'ITA Type M (BS 546)'), + (TYPE_ITA_N, 'ITA Type N'), + (TYPE_ITA_O, 'ITA Type O'), + )), + ) + + +class PowerOutletFeedLegChoices(ChoiceSet): + + FEED_LEG_A = 'A' + FEED_LEG_B = 'B' + FEED_LEG_C = 'C' + + CHOICES = ( + (FEED_LEG_A, 'A'), + (FEED_LEG_B, 'B'), + (FEED_LEG_C, 'C'), + ) + + LEGACY_MAP = { + FEED_LEG_A: 1, + FEED_LEG_B: 2, + FEED_LEG_C: 3, + } + + +# +# Interfaces +# + +class InterfaceTypeChoices(ChoiceSet): + + # Virtual + TYPE_VIRTUAL = 'virtual' + TYPE_LAG = 'lag' + + # Ethernet + TYPE_100ME_FIXED = '100base-tx' + TYPE_1GE_FIXED = '1000base-t' + TYPE_1GE_GBIC = '1000base-x-gbic' + TYPE_1GE_SFP = '1000base-x-sfp' + TYPE_2GE_FIXED = '2.5gbase-t' + TYPE_5GE_FIXED = '5gbase-t' + TYPE_10GE_FIXED = '10gbase-t' + TYPE_10GE_CX4 = '10gbase-cx4' + TYPE_10GE_SFP_PLUS = '10gbase-x-sfpp' + TYPE_10GE_XFP = '10gbase-x-xfp' + TYPE_10GE_XENPAK = '10gbase-x-xenpak' + TYPE_10GE_X2 = '10gbase-x-x2' + TYPE_25GE_SFP28 = '25gbase-x-sfp28' + TYPE_40GE_QSFP_PLUS = '40gbase-x-qsfpp' + TYPE_50GE_QSFP28 = '50gbase-x-sfp28' + TYPE_100GE_CFP = '100gbase-x-cfp' + TYPE_100GE_CFP2 = '100gbase-x-cfp2' + TYPE_100GE_CFP4 = '100gbase-x-cfp4' + TYPE_100GE_CPAK = '100gbase-x-cpak' + TYPE_100GE_QSFP28 = '100gbase-x-qsfp28' + TYPE_200GE_CFP2 = '200gbase-x-cfp2' + TYPE_200GE_QSFP56 = '200gbase-x-qsfp56' + TYPE_400GE_QSFP_DD = '400gbase-x-qsfpdd' + TYPE_400GE_OSFP = '400gbase-x-osfp' + + # Wireless + TYPE_80211A = 'ieee802.11a' + TYPE_80211G = 'ieee802.11g' + TYPE_80211N = 'ieee802.11n' + TYPE_80211AC = 'ieee802.11ac' + TYPE_80211AD = 'ieee802.11ad' + + # Cellular + TYPE_GSM = 'gsm' + TYPE_CDMA = 'cdma' + TYPE_LTE = 'lte' + + # SONET + TYPE_SONET_OC3 = 'sonet-oc3' + TYPE_SONET_OC12 = 'sonet-oc12' + TYPE_SONET_OC48 = 'sonet-oc48' + TYPE_SONET_OC192 = 'sonet-oc192' + TYPE_SONET_OC768 = 'sonet-oc768' + TYPE_SONET_OC1920 = 'sonet-oc1920' + TYPE_SONET_OC3840 = 'sonet-oc3840' + + # Fibrechannel + TYPE_1GFC_SFP = '1gfc-sfp' + TYPE_2GFC_SFP = '2gfc-sfp' + TYPE_4GFC_SFP = '4gfc-sfp' + TYPE_8GFC_SFP_PLUS = '8gfc-sfpp' + TYPE_16GFC_SFP_PLUS = '16gfc-sfpp' + TYPE_32GFC_SFP28 = '32gfc-sfp28' + TYPE_128GFC_QSFP28 = '128gfc-sfp28' + + # InfiniBand + TYPE_INFINIBAND_SDR = 'inifiband-sdr' + TYPE_INFINIBAND_DDR = 'inifiband-ddr' + TYPE_INFINIBAND_QDR = 'inifiband-qdr' + TYPE_INFINIBAND_FDR10 = 'inifiband-fdr10' + TYPE_INFINIBAND_FDR = 'inifiband-fdr' + TYPE_INFINIBAND_EDR = 'inifiband-edr' + TYPE_INFINIBAND_HDR = 'inifiband-hdr' + TYPE_INFINIBAND_NDR = 'inifiband-ndr' + TYPE_INFINIBAND_XDR = 'inifiband-xdr' + + # Serial + TYPE_T1 = 't1' + TYPE_E1 = 'e1' + TYPE_T3 = 't3' + TYPE_E3 = 'e3' + + # Stacking + TYPE_STACKWISE = 'cisco-stackwise' + TYPE_STACKWISE_PLUS = 'cisco-stackwise-plus' + TYPE_FLEXSTACK = 'cisco-flexstack' + TYPE_FLEXSTACK_PLUS = 'cisco-flexstack-plus' + TYPE_JUNIPER_VCP = 'juniper-vcp' + TYPE_SUMMITSTACK = 'extreme-summitstack' + TYPE_SUMMITSTACK128 = 'extreme-summitstack-128' + TYPE_SUMMITSTACK256 = 'extreme-summitstack-256' + TYPE_SUMMITSTACK512 = 'extreme-summitstack-512' + + # Other + TYPE_OTHER = 'other' + + CHOICES = ( + ( + 'Virtual interfaces', + ( + (TYPE_VIRTUAL, 'Virtual'), + (TYPE_LAG, 'Link Aggregation Group (LAG)'), + ), + ), + ( + 'Ethernet (fixed)', + ( + (TYPE_100ME_FIXED, '100BASE-TX (10/100ME)'), + (TYPE_1GE_FIXED, '1000BASE-T (1GE)'), + (TYPE_2GE_FIXED, '2.5GBASE-T (2.5GE)'), + (TYPE_5GE_FIXED, '5GBASE-T (5GE)'), + (TYPE_10GE_FIXED, '10GBASE-T (10GE)'), + (TYPE_10GE_CX4, '10GBASE-CX4 (10GE)'), + ) + ), + ( + 'Ethernet (modular)', + ( + (TYPE_1GE_GBIC, 'GBIC (1GE)'), + (TYPE_1GE_SFP, 'SFP (1GE)'), + (TYPE_10GE_SFP_PLUS, 'SFP+ (10GE)'), + (TYPE_10GE_XFP, 'XFP (10GE)'), + (TYPE_10GE_XENPAK, 'XENPAK (10GE)'), + (TYPE_10GE_X2, 'X2 (10GE)'), + (TYPE_25GE_SFP28, 'SFP28 (25GE)'), + (TYPE_40GE_QSFP_PLUS, 'QSFP+ (40GE)'), + (TYPE_50GE_QSFP28, 'QSFP28 (50GE)'), + (TYPE_100GE_CFP, 'CFP (100GE)'), + (TYPE_100GE_CFP2, 'CFP2 (100GE)'), + (TYPE_200GE_CFP2, 'CFP2 (200GE)'), + (TYPE_100GE_CFP4, 'CFP4 (100GE)'), + (TYPE_100GE_CPAK, 'Cisco CPAK (100GE)'), + (TYPE_100GE_QSFP28, 'QSFP28 (100GE)'), + (TYPE_200GE_QSFP56, 'QSFP56 (200GE)'), + (TYPE_400GE_QSFP_DD, 'QSFP-DD (400GE)'), + (TYPE_400GE_OSFP, 'OSFP (400GE)'), + ) + ), + ( + 'Wireless', + ( + (TYPE_80211A, 'IEEE 802.11a'), + (TYPE_80211G, 'IEEE 802.11b/g'), + (TYPE_80211N, 'IEEE 802.11n'), + (TYPE_80211AC, 'IEEE 802.11ac'), + (TYPE_80211AD, 'IEEE 802.11ad'), + ) + ), + ( + 'Cellular', + ( + (TYPE_GSM, 'GSM'), + (TYPE_CDMA, 'CDMA'), + (TYPE_LTE, 'LTE'), + ) + ), + ( + 'SONET', + ( + (TYPE_SONET_OC3, 'OC-3/STM-1'), + (TYPE_SONET_OC12, 'OC-12/STM-4'), + (TYPE_SONET_OC48, 'OC-48/STM-16'), + (TYPE_SONET_OC192, 'OC-192/STM-64'), + (TYPE_SONET_OC768, 'OC-768/STM-256'), + (TYPE_SONET_OC1920, 'OC-1920/STM-640'), + (TYPE_SONET_OC3840, 'OC-3840/STM-1234'), + ) + ), + ( + 'FibreChannel', + ( + (TYPE_1GFC_SFP, 'SFP (1GFC)'), + (TYPE_2GFC_SFP, 'SFP (2GFC)'), + (TYPE_4GFC_SFP, 'SFP (4GFC)'), + (TYPE_8GFC_SFP_PLUS, 'SFP+ (8GFC)'), + (TYPE_16GFC_SFP_PLUS, 'SFP+ (16GFC)'), + (TYPE_32GFC_SFP28, 'SFP28 (32GFC)'), + (TYPE_128GFC_QSFP28, 'QSFP28 (128GFC)'), + ) + ), + ( + 'InfiniBand', + ( + (TYPE_INFINIBAND_SDR, 'SDR (2 Gbps)'), + (TYPE_INFINIBAND_DDR, 'DDR (4 Gbps)'), + (TYPE_INFINIBAND_QDR, 'QDR (8 Gbps)'), + (TYPE_INFINIBAND_FDR10, 'FDR10 (10 Gbps)'), + (TYPE_INFINIBAND_FDR, 'FDR (13.5 Gbps)'), + (TYPE_INFINIBAND_EDR, 'EDR (25 Gbps)'), + (TYPE_INFINIBAND_HDR, 'HDR (50 Gbps)'), + (TYPE_INFINIBAND_NDR, 'NDR (100 Gbps)'), + (TYPE_INFINIBAND_XDR, 'XDR (250 Gbps)'), + ) + ), + ( + 'Serial', + ( + (TYPE_T1, 'T1 (1.544 Mbps)'), + (TYPE_E1, 'E1 (2.048 Mbps)'), + (TYPE_T3, 'T3 (45 Mbps)'), + (TYPE_E3, 'E3 (34 Mbps)'), + ) + ), + ( + 'Stacking', + ( + (TYPE_STACKWISE, 'Cisco StackWise'), + (TYPE_STACKWISE_PLUS, 'Cisco StackWise Plus'), + (TYPE_FLEXSTACK, 'Cisco FlexStack'), + (TYPE_FLEXSTACK_PLUS, 'Cisco FlexStack Plus'), + (TYPE_JUNIPER_VCP, 'Juniper VCP'), + (TYPE_SUMMITSTACK, 'Extreme SummitStack'), + (TYPE_SUMMITSTACK128, 'Extreme SummitStack-128'), + (TYPE_SUMMITSTACK256, 'Extreme SummitStack-256'), + (TYPE_SUMMITSTACK512, 'Extreme SummitStack-512'), + ) + ), + ( + 'Other', + ( + (TYPE_OTHER, 'Other'), + ) + ), + ) + + LEGACY_MAP = { + TYPE_VIRTUAL: 0, + TYPE_LAG: 200, + TYPE_100ME_FIXED: 800, + TYPE_1GE_FIXED: 1000, + TYPE_1GE_GBIC: 1050, + TYPE_1GE_SFP: 1100, + TYPE_2GE_FIXED: 1120, + TYPE_5GE_FIXED: 1130, + TYPE_10GE_FIXED: 1150, + TYPE_10GE_CX4: 1170, + TYPE_10GE_SFP_PLUS: 1200, + TYPE_10GE_XFP: 1300, + TYPE_10GE_XENPAK: 1310, + TYPE_10GE_X2: 1320, + TYPE_25GE_SFP28: 1350, + TYPE_40GE_QSFP_PLUS: 1400, + TYPE_50GE_QSFP28: 1420, + TYPE_100GE_CFP: 1500, + TYPE_100GE_CFP2: 1510, + TYPE_100GE_CFP4: 1520, + TYPE_100GE_CPAK: 1550, + TYPE_100GE_QSFP28: 1600, + TYPE_200GE_CFP2: 1650, + TYPE_200GE_QSFP56: 1700, + TYPE_400GE_QSFP_DD: 1750, + TYPE_400GE_OSFP: 1800, + TYPE_80211A: 2600, + TYPE_80211G: 2610, + TYPE_80211N: 2620, + TYPE_80211AC: 2630, + TYPE_80211AD: 2640, + TYPE_GSM: 2810, + TYPE_CDMA: 2820, + TYPE_LTE: 2830, + TYPE_SONET_OC3: 6100, + TYPE_SONET_OC12: 6200, + TYPE_SONET_OC48: 6300, + TYPE_SONET_OC192: 6400, + TYPE_SONET_OC768: 6500, + TYPE_SONET_OC1920: 6600, + TYPE_SONET_OC3840: 6700, + TYPE_1GFC_SFP: 3010, + TYPE_2GFC_SFP: 3020, + TYPE_4GFC_SFP: 3040, + TYPE_8GFC_SFP_PLUS: 3080, + TYPE_16GFC_SFP_PLUS: 3160, + TYPE_32GFC_SFP28: 3320, + TYPE_128GFC_QSFP28: 3400, + TYPE_INFINIBAND_SDR: 7010, + TYPE_INFINIBAND_DDR: 7020, + TYPE_INFINIBAND_QDR: 7030, + TYPE_INFINIBAND_FDR10: 7040, + TYPE_INFINIBAND_FDR: 7050, + TYPE_INFINIBAND_EDR: 7060, + TYPE_INFINIBAND_HDR: 7070, + TYPE_INFINIBAND_NDR: 7080, + TYPE_INFINIBAND_XDR: 7090, + TYPE_T1: 4000, + TYPE_E1: 4010, + TYPE_T3: 4040, + TYPE_E3: 4050, + TYPE_STACKWISE: 5000, + TYPE_STACKWISE_PLUS: 5050, + TYPE_FLEXSTACK: 5100, + TYPE_FLEXSTACK_PLUS: 5150, + TYPE_JUNIPER_VCP: 5200, + TYPE_SUMMITSTACK: 5300, + TYPE_SUMMITSTACK128: 5310, + TYPE_SUMMITSTACK256: 5320, + TYPE_SUMMITSTACK512: 5330, + } + + +class InterfaceModeChoices(ChoiceSet): + + MODE_ACCESS = 'access' + MODE_TAGGED = 'tagged' + MODE_TAGGED_ALL = 'tagged-all' + + CHOICES = ( + (MODE_ACCESS, 'Access'), + (MODE_TAGGED, 'Tagged'), + (MODE_TAGGED_ALL, 'Tagged (All)'), + ) + + LEGACY_MAP = { + MODE_ACCESS: 100, + MODE_TAGGED: 200, + MODE_TAGGED_ALL: 300, + } + + +# +# FrontPorts/RearPorts +# + +class PortTypeChoices(ChoiceSet): + + TYPE_8P8C = '8p8c' + TYPE_110_PUNCH = '110-punch' + TYPE_BNC = 'bnc' + TYPE_ST = 'st' + TYPE_SC = 'sc' + TYPE_SC_APC = 'sc-apc' + TYPE_FC = 'fc' + TYPE_LC = 'lc' + TYPE_LC_APC = 'lc-apc' + TYPE_MTRJ = 'mtrj' + TYPE_MPO = 'mpo' + TYPE_LSH = 'lsh' + TYPE_LSH_APC = 'lsh-apc' + + CHOICES = ( + ( + 'Copper', + ( + (TYPE_8P8C, '8P8C'), + (TYPE_110_PUNCH, '110 Punch'), + (TYPE_BNC, 'BNC'), + ), + ), + ( + 'Fiber Optic', + ( + (TYPE_FC, 'FC'), + (TYPE_LC, 'LC'), + (TYPE_LC_APC, 'LC/APC'), + (TYPE_LSH, 'LSH'), + (TYPE_LSH_APC, 'LSH/APC'), + (TYPE_MPO, 'MPO'), + (TYPE_MTRJ, 'MTRJ'), + (TYPE_SC, 'SC'), + (TYPE_SC_APC, 'SC/APC'), + (TYPE_ST, 'ST'), + ) + ) + ) + + LEGACY_MAP = { + TYPE_8P8C: 1000, + TYPE_110_PUNCH: 1100, + TYPE_BNC: 1200, + TYPE_ST: 2000, + TYPE_SC: 2100, + TYPE_SC_APC: 2110, + TYPE_FC: 2200, + TYPE_LC: 2300, + TYPE_LC_APC: 2310, + TYPE_MTRJ: 2400, + TYPE_MPO: 2500, + TYPE_LSH: 2600, + TYPE_LSH_APC: 2610, + } + + +# +# Cables +# + +class CableTypeChoices(ChoiceSet): + + TYPE_CAT3 = 'cat3' + TYPE_CAT5 = 'cat5' + TYPE_CAT5E = 'cat5e' + TYPE_CAT6 = 'cat6' + TYPE_CAT6A = 'cat6a' + TYPE_CAT7 = 'cat7' + TYPE_DAC_ACTIVE = 'dac-active' + TYPE_DAC_PASSIVE = 'dac-passive' + TYPE_COAXIAL = 'coaxial' + TYPE_MMF = 'mmf' + TYPE_MMF_OM1 = 'mmf-om1' + TYPE_MMF_OM2 = 'mmf-om2' + TYPE_MMF_OM3 = 'mmf-om3' + TYPE_MMF_OM4 = 'mmf-om4' + TYPE_SMF = 'smf' + TYPE_SMF_OS1 = 'smf-os1' + TYPE_SMF_OS2 = 'smf-os2' + TYPE_AOC = 'aoc' + TYPE_POWER = 'power' + + CHOICES = ( + ( + 'Copper', ( + (TYPE_CAT3, 'CAT3'), + (TYPE_CAT5, 'CAT5'), + (TYPE_CAT5E, 'CAT5e'), + (TYPE_CAT6, 'CAT6'), + (TYPE_CAT6A, 'CAT6a'), + (TYPE_CAT7, 'CAT7'), + (TYPE_DAC_ACTIVE, 'Direct Attach Copper (Active)'), + (TYPE_DAC_PASSIVE, 'Direct Attach Copper (Passive)'), + (TYPE_COAXIAL, 'Coaxial'), + ), + ), + ( + 'Fiber', ( + (TYPE_MMF, 'Multimode Fiber'), + (TYPE_MMF_OM1, 'Multimode Fiber (OM1)'), + (TYPE_MMF_OM2, 'Multimode Fiber (OM2)'), + (TYPE_MMF_OM3, 'Multimode Fiber (OM3)'), + (TYPE_MMF_OM4, 'Multimode Fiber (OM4)'), + (TYPE_SMF, 'Singlemode Fiber'), + (TYPE_SMF_OS1, 'Singlemode Fiber (OS1)'), + (TYPE_SMF_OS2, 'Singlemode Fiber (OS2)'), + (TYPE_AOC, 'Active Optical Cabling (AOC)'), + ), + ), + (TYPE_POWER, 'Power'), + ) + + LEGACY_MAP = { + TYPE_CAT3: 1300, + TYPE_CAT5: 1500, + TYPE_CAT5E: 1510, + TYPE_CAT6: 1600, + TYPE_CAT6A: 1610, + TYPE_CAT7: 1700, + TYPE_DAC_ACTIVE: 1800, + TYPE_DAC_PASSIVE: 1810, + TYPE_COAXIAL: 1900, + TYPE_MMF: 3000, + TYPE_MMF_OM1: 3010, + TYPE_MMF_OM2: 3020, + TYPE_MMF_OM3: 3030, + TYPE_MMF_OM4: 3040, + TYPE_SMF: 3500, + TYPE_SMF_OS1: 3510, + TYPE_SMF_OS2: 3520, + TYPE_AOC: 3800, + TYPE_POWER: 5000, + } + + +class CableStatusChoices(ChoiceSet): + + STATUS_CONNECTED = 'connected' + STATUS_PLANNED = 'planned' + + CHOICES = ( + (STATUS_CONNECTED, 'Connected'), + (STATUS_PLANNED, 'Planned'), + ) + + LEGACY_MAP = { + STATUS_CONNECTED: True, + STATUS_PLANNED: False, + } + + +class CableLengthUnitChoices(ChoiceSet): + + UNIT_METER = 'm' + UNIT_CENTIMETER = 'cm' + UNIT_FOOT = 'ft' + UNIT_INCH = 'in' + + CHOICES = ( + (UNIT_METER, 'Meters'), + (UNIT_CENTIMETER, 'Centimeters'), + (UNIT_FOOT, 'Feet'), + (UNIT_INCH, 'Inches'), + ) + + LEGACY_MAP = { + UNIT_METER: 1200, + UNIT_CENTIMETER: 1100, + UNIT_FOOT: 2100, + UNIT_INCH: 2000, + } + + +# +# PowerFeeds +# + +class PowerFeedStatusChoices(ChoiceSet): + + STATUS_OFFLINE = 'offline' + STATUS_ACTIVE = 'active' + STATUS_PLANNED = 'planned' + STATUS_FAILED = 'failed' + + CHOICES = ( + (STATUS_OFFLINE, 'Offline'), + (STATUS_ACTIVE, 'Active'), + (STATUS_PLANNED, 'Planned'), + (STATUS_FAILED, 'Failed'), + ) + + LEGACY_MAP = { + STATUS_OFFLINE: 0, + STATUS_ACTIVE: 1, + STATUS_PLANNED: 2, + STATUS_FAILED: 4, + } + + +class PowerFeedTypeChoices(ChoiceSet): + + TYPE_PRIMARY = 'primary' + TYPE_REDUNDANT = 'redundant' + + CHOICES = ( + (TYPE_PRIMARY, 'Primary'), + (TYPE_REDUNDANT, 'Redundant'), + ) + + LEGACY_MAP = { + TYPE_PRIMARY: 1, + TYPE_REDUNDANT: 2, + } + + +class PowerFeedSupplyChoices(ChoiceSet): + + SUPPLY_AC = 'ac' + SUPPLY_DC = 'dc' + + CHOICES = ( + (SUPPLY_AC, 'AC'), + (SUPPLY_DC, 'DC'), + ) + + LEGACY_MAP = { + SUPPLY_AC: 1, + SUPPLY_DC: 2, + } + + +class PowerFeedPhaseChoices(ChoiceSet): + + PHASE_SINGLE = 'single-phase' + PHASE_3PHASE = 'three-phase' + + CHOICES = ( + (PHASE_SINGLE, 'Single phase'), + (PHASE_3PHASE, 'Three-phase'), + ) + + LEGACY_MAP = { + PHASE_SINGLE: 1, + PHASE_3PHASE: 3, + } diff --git a/netbox/dcim/constants.py b/netbox/dcim/constants.py index f325e34d4..3a6f8e5e9 100644 --- a/netbox/dcim/constants.py +++ b/netbox/dcim/constants.py @@ -1,383 +1,41 @@ +from django.db.models import Q -# BGP ASN bounds -BGP_ASN_MIN = 1 -BGP_ASN_MAX = 2**32 - 1 +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 -RACK_WIDTH_19IN = 19 -RACK_WIDTH_23IN = 23 -RACK_WIDTH_CHOICES = ( - (RACK_WIDTH_19IN, '19 inches'), - (RACK_WIDTH_23IN, '23 inches'), -) +# +# Rack elevation rendering +# -# Rack faces -RACK_FACE_FRONT = 0 -RACK_FACE_REAR = 1 -RACK_FACE_CHOICES = [ - [RACK_FACE_FRONT, 'Front'], - [RACK_FACE_REAR, 'Rear'], -] +RACK_ELEVATION_UNIT_WIDTH_DEFAULT = 230 +RACK_ELEVATION_UNIT_HEIGHT_DEFAULT = 20 -# 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'], - ] - ], -] +# +# Interface type groups +# VIRTUAL_IFACE_TYPES = [ - IFACE_TYPE_VIRTUAL, - IFACE_TYPE_LAG, + InterfaceTypeChoices.TYPE_VIRTUAL, + InterfaceTypeChoices.TYPE_LAG, ] WIRELESS_IFACE_TYPES = [ - IFACE_TYPE_80211A, - IFACE_TYPE_80211G, - IFACE_TYPE_80211N, - IFACE_TYPE_80211AC, - IFACE_TYPE_80211AD, + InterfaceTypeChoices.TYPE_80211A, + InterfaceTypeChoices.TYPE_80211G, + InterfaceTypeChoices.TYPE_80211N, + InterfaceTypeChoices.TYPE_80211AC, + InterfaceTypeChoices.TYPE_80211AD, ] 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', -} +# +# Cabling and connections +# +# TODO: Replace with CableStatusChoices? # Console/power/interface connection statuses CONNECTION_STATUS_PLANNED = False CONNECTION_STATUS_CONNECTED = True @@ -387,72 +45,22 @@ CONNECTION_STATUS_CHOICES = [ ] # Cable endpoint types -CABLE_TERMINATION_TYPES = [ - 'consoleport', 'consoleserverport', 'interface', 'poweroutlet', 'powerport', 'frontport', 'rearport', - '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_MODELS = Q( + Q(app_label='circuits', model__in=( + 'circuittermination', + )) | + Q(app_label='dcim', model__in=( + 'consoleport', + 'consoleserverport', + 'frontport', + 'interface', + 'powerfeed', + 'poweroutlet', + 'powerport', + 'rearport', + )) ) -CABLE_TERMINATION_TYPE_CHOICES = { - # (API endpoint, human-friendly name) - 'consoleport': ('console-ports', 'Console port'), - 'consoleserverport': ('console-server-ports', 'Console server port'), - 'powerport': ('power-ports', 'Power port'), - 'poweroutlet': ('power-outlets', 'Power outlet'), - 'interface': ('interfaces', 'Interface'), - 'frontport': ('front-ports', 'Front panel port'), - 'rearport': ('rear-ports', 'Rear panel port'), -} - COMPATIBLE_TERMINATION_TYPES = { 'consoleport': ['consoleserverport', 'frontport', 'rearport'], 'consoleserverport': ['consoleport', 'frontport', 'rearport'], @@ -463,57 +71,3 @@ COMPATIBLE_TERMINATION_TYPES = { 'rearport': ['consoleport', 'consoleserverport', 'interface', 'frontport', 'rearport', 'circuittermination'], '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'), -) - -# Power feeds -POWERFEED_TYPE_PRIMARY = 1 -POWERFEED_TYPE_REDUNDANT = 2 -POWERFEED_TYPE_CHOICES = ( - (POWERFEED_TYPE_PRIMARY, 'Primary'), - (POWERFEED_TYPE_REDUNDANT, 'Redundant'), -) -POWERFEED_SUPPLY_AC = 1 -POWERFEED_SUPPLY_DC = 2 -POWERFEED_SUPPLY_CHOICES = ( - (POWERFEED_SUPPLY_AC, 'AC'), - (POWERFEED_SUPPLY_DC, 'DC'), -) -POWERFEED_PHASE_SINGLE = 1 -POWERFEED_PHASE_3PHASE = 3 -POWERFEED_PHASE_CHOICES = ( - (POWERFEED_PHASE_SINGLE, 'Single phase'), - (POWERFEED_PHASE_3PHASE, 'Three-phase'), -) -POWERFEED_STATUS_OFFLINE = 0 -POWERFEED_STATUS_ACTIVE = 1 -POWERFEED_STATUS_PLANNED = 2 -POWERFEED_STATUS_FAILED = 4 -POWERFEED_STATUS_CHOICES = ( - (POWERFEED_STATUS_ACTIVE, 'Active'), - (POWERFEED_STATUS_OFFLINE, 'Offline'), - (POWERFEED_STATUS_PLANNED, 'Planned'), - (POWERFEED_STATUS_FAILED, 'Failed'), -) -POWERFEED_LEG_A = 1 -POWERFEED_LEG_B = 2 -POWERFEED_LEG_C = 3 -POWERFEED_LEG_CHOICES = ( - (POWERFEED_LEG_A, 'A'), - (POWERFEED_LEG_B, 'B'), - (POWERFEED_LEG_C, 'C'), -) diff --git a/netbox/dcim/fields.py b/netbox/dcim/fields.py index 719b6755a..3acd0d4a1 100644 --- a/netbox/dcim/fields.py +++ b/netbox/dcim/fields.py @@ -3,7 +3,7 @@ from django.core.validators import MinValueValidator, MaxValueValidator from django.db import models from netaddr import AddrFormatError, EUI, mac_unix_expanded -from .constants import * +from ipam.constants import BGP_ASN_MAX, BGP_ASN_MIN class ASNField(models.BigIntegerField): @@ -14,7 +14,10 @@ class ASNField(models.BigIntegerField): ] def formfield(self, **kwargs): - defaults = {'min_value': BGP_ASN_MIN, 'max_value': BGP_ASN_MAX} + defaults = { + 'min_value': BGP_ASN_MIN, + 'max_value': BGP_ASN_MAX, + } defaults.update(**kwargs) return super().formfield(**defaults) @@ -29,7 +32,7 @@ class MACAddressField(models.Field): def python_type(self): return EUI - def from_db_value(self, value, expression, connection, context): + def from_db_value(self, value, expression, connection): return self.to_python(value) def to_python(self, value): diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index 29604491d..cf100af00 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -2,8 +2,8 @@ import django_filters from django.contrib.auth.models import User from django.db.models import Q -from extras.filters import CustomFieldFilterSet, LocalConfigContextFilter, CreatedUpdatedFilterSet -from tenancy.filtersets import TenancyFilterSet +from extras.filters import CustomFieldFilterSet, LocalConfigContextFilterSet, CreatedUpdatedFilterSet +from tenancy.filters import TenancyFilterSet from tenancy.models import Tenant from utilities.constants import COLOR_CHOICES from utilities.filters import ( @@ -11,6 +11,7 @@ from utilities.filters import ( TagFilter, TreeNodeMultipleChoiceFilter, ) from virtualization.models import Cluster +from .choices import * from .constants import * from .models import ( Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, @@ -22,45 +23,45 @@ from .models import ( __all__ = ( - 'CableFilter', - 'ConsoleConnectionFilter', - 'ConsolePortFilter', - 'ConsolePortTemplateFilter', - 'ConsoleServerPortFilter', - 'ConsoleServerPortTemplateFilter', - 'DeviceBayFilter', - 'DeviceBayTemplateFilter', - 'DeviceFilter', - 'DeviceRoleFilter', - 'DeviceTypeFilter', - 'FrontPortFilter', - 'FrontPortTemplateFilter', - 'InterfaceConnectionFilter', - 'InterfaceFilter', - 'InterfaceTemplateFilter', - 'InventoryItemFilter', - 'ManufacturerFilter', - 'PlatformFilter', - 'PowerConnectionFilter', - 'PowerFeedFilter', - 'PowerOutletFilter', - 'PowerOutletTemplateFilter', - 'PowerPanelFilter', - 'PowerPortFilter', - 'PowerPortTemplateFilter', - 'RackFilter', - 'RackGroupFilter', - 'RackReservationFilter', - 'RackRoleFilter', - 'RearPortFilter', - 'RearPortTemplateFilter', - 'RegionFilter', - 'SiteFilter', - 'VirtualChassisFilter', + 'CableFilterSet', + 'ConsoleConnectionFilterSet', + 'ConsolePortFilterSet', + 'ConsolePortTemplateFilterSet', + 'ConsoleServerPortFilterSet', + 'ConsoleServerPortTemplateFilterSet', + 'DeviceBayFilterSet', + 'DeviceBayTemplateFilterSet', + 'DeviceFilterSet', + 'DeviceRoleFilterSet', + 'DeviceTypeFilterSet', + 'FrontPortFilterSet', + 'FrontPortTemplateFilterSet', + 'InterfaceConnectionFilterSet', + 'InterfaceFilterSet', + 'InterfaceTemplateFilterSet', + 'InventoryItemFilterSet', + 'ManufacturerFilterSet', + 'PlatformFilterSet', + 'PowerConnectionFilterSet', + 'PowerFeedFilterSet', + 'PowerOutletFilterSet', + 'PowerOutletTemplateFilterSet', + 'PowerPanelFilterSet', + 'PowerPortFilterSet', + 'PowerPortTemplateFilterSet', + 'RackFilterSet', + 'RackGroupFilterSet', + 'RackReservationFilterSet', + 'RackRoleFilterSet', + 'RearPortFilterSet', + 'RearPortTemplateFilterSet', + 'RegionFilterSet', + 'SiteFilterSet', + 'VirtualChassisFilterSet', ) -class RegionFilter(NameSlugSearchFilterSet): +class RegionFilterSet(NameSlugSearchFilterSet): parent_id = django_filters.ModelMultipleChoiceFilter( queryset=Region.objects.all(), label='Parent region (ID)', @@ -77,7 +78,7 @@ class RegionFilter(NameSlugSearchFilterSet): fields = ['id', 'name', 'slug'] -class SiteFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet): +class SiteFilterSet(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet): id__in = NumericInFilter( field_name='id', lookup_expr='in' @@ -87,7 +88,7 @@ class SiteFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet label='Search', ) status = django_filters.MultipleChoiceFilter( - choices=SITE_STATUS_CHOICES, + choices=SiteStatusChoices, null_value=None ) region_id = TreeNodeMultipleChoiceFilter( @@ -131,7 +132,7 @@ class SiteFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet return queryset.filter(qs_filter) -class RackGroupFilter(NameSlugSearchFilterSet): +class RackGroupFilterSet(NameSlugSearchFilterSet): region_id = TreeNodeMultipleChoiceFilter( queryset=Region.objects.all(), field_name='site__region__in', @@ -159,14 +160,14 @@ class RackGroupFilter(NameSlugSearchFilterSet): fields = ['id', 'name', 'slug'] -class RackRoleFilter(NameSlugSearchFilterSet): +class RackRoleFilterSet(NameSlugSearchFilterSet): class Meta: model = RackRole fields = ['id', 'name', 'slug', 'color'] -class RackFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet): +class RackFilterSet(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet): id__in = NumericInFilter( field_name='id', lookup_expr='in' @@ -207,7 +208,7 @@ class RackFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet label='Group', ) status = django_filters.MultipleChoiceFilter( - choices=RACK_STATUS_CHOICES, + choices=RackStatusChoices, null_value=None ) role_id = django_filters.ModelMultipleChoiceFilter( @@ -244,7 +245,7 @@ class RackFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet ) -class RackReservationFilter(TenancyFilterSet): +class RackReservationFilterSet(TenancyFilterSet): id__in = NumericInFilter( field_name='id', lookup_expr='in' @@ -305,14 +306,14 @@ class RackReservationFilter(TenancyFilterSet): ) -class ManufacturerFilter(NameSlugSearchFilterSet): +class ManufacturerFilterSet(NameSlugSearchFilterSet): class Meta: model = Manufacturer fields = ['id', 'name', 'slug'] -class DeviceTypeFilter(CustomFieldFilterSet, CreatedUpdatedFilterSet): +class DeviceTypeFilterSet(CustomFieldFilterSet, CreatedUpdatedFilterSet): id__in = NumericInFilter( field_name='id', lookup_expr='in' @@ -403,70 +404,70 @@ class DeviceTypeComponentFilterSet(NameSlugSearchFilterSet): ) -class ConsolePortTemplateFilter(DeviceTypeComponentFilterSet): +class ConsolePortTemplateFilterSet(DeviceTypeComponentFilterSet): class Meta: model = ConsolePortTemplate - fields = ['id', 'name'] + fields = ['id', 'name', 'type'] -class ConsoleServerPortTemplateFilter(DeviceTypeComponentFilterSet): +class ConsoleServerPortTemplateFilterSet(DeviceTypeComponentFilterSet): class Meta: model = ConsoleServerPortTemplate - fields = ['id', 'name'] + fields = ['id', 'name', 'type'] -class PowerPortTemplateFilter(DeviceTypeComponentFilterSet): +class PowerPortTemplateFilterSet(DeviceTypeComponentFilterSet): class Meta: model = PowerPortTemplate - fields = ['id', 'name', 'maximum_draw', 'allocated_draw'] + fields = ['id', 'name', 'type', 'maximum_draw', 'allocated_draw'] -class PowerOutletTemplateFilter(DeviceTypeComponentFilterSet): +class PowerOutletTemplateFilterSet(DeviceTypeComponentFilterSet): class Meta: model = PowerOutletTemplate - fields = ['id', 'name', 'feed_leg'] + fields = ['id', 'name', 'type', 'feed_leg'] -class InterfaceTemplateFilter(DeviceTypeComponentFilterSet): +class InterfaceTemplateFilterSet(DeviceTypeComponentFilterSet): class Meta: model = InterfaceTemplate fields = ['id', 'name', 'type', 'mgmt_only'] -class FrontPortTemplateFilter(DeviceTypeComponentFilterSet): +class FrontPortTemplateFilterSet(DeviceTypeComponentFilterSet): class Meta: model = FrontPortTemplate fields = ['id', 'name', 'type'] -class RearPortTemplateFilter(DeviceTypeComponentFilterSet): +class RearPortTemplateFilterSet(DeviceTypeComponentFilterSet): class Meta: model = RearPortTemplate fields = ['id', 'name', 'type', 'positions'] -class DeviceBayTemplateFilter(DeviceTypeComponentFilterSet): +class DeviceBayTemplateFilterSet(DeviceTypeComponentFilterSet): class Meta: model = DeviceBayTemplate fields = ['id', 'name'] -class DeviceRoleFilter(NameSlugSearchFilterSet): +class DeviceRoleFilterSet(NameSlugSearchFilterSet): class Meta: model = DeviceRole fields = ['id', 'name', 'slug', 'color', 'vm_role'] -class PlatformFilter(NameSlugSearchFilterSet): +class PlatformFilterSet(NameSlugSearchFilterSet): manufacturer_id = django_filters.ModelMultipleChoiceFilter( field_name='manufacturer', queryset=Manufacturer.objects.all(), @@ -484,7 +485,7 @@ class PlatformFilter(NameSlugSearchFilterSet): fields = ['id', 'name', 'slug', 'napalm_driver'] -class DeviceFilter(LocalConfigContextFilter, TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet): +class DeviceFilterSet(LocalConfigContextFilterSet, TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet): id__in = NumericInFilter( field_name='id', lookup_expr='in' @@ -571,7 +572,7 @@ class DeviceFilter(LocalConfigContextFilter, TenancyFilterSet, CustomFieldFilter label='Device model (slug)', ) status = django_filters.MultipleChoiceFilter( - choices=DEVICE_STATUS_CHOICES, + choices=DeviceStatusChoices, null_value=None ) is_full_depth = django_filters.BooleanFilter( @@ -681,6 +682,26 @@ class DeviceComponentFilterSet(django_filters.FilterSet): method='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( queryset=Device.objects.all(), label='Device (ID)', @@ -702,7 +723,11 @@ class DeviceComponentFilterSet(django_filters.FilterSet): ) -class ConsolePortFilter(DeviceComponentFilterSet): +class ConsolePortFilterSet(DeviceComponentFilterSet): + type = django_filters.MultipleChoiceFilter( + choices=ConsolePortTypeChoices, + null_value=None + ) cabled = django_filters.BooleanFilter( field_name='cable', lookup_expr='isnull', @@ -714,7 +739,11 @@ class ConsolePortFilter(DeviceComponentFilterSet): fields = ['id', 'name', 'description', 'connection_status'] -class ConsoleServerPortFilter(DeviceComponentFilterSet): +class ConsoleServerPortFilterSet(DeviceComponentFilterSet): + type = django_filters.MultipleChoiceFilter( + choices=ConsolePortTypeChoices, + null_value=None + ) cabled = django_filters.BooleanFilter( field_name='cable', lookup_expr='isnull', @@ -726,7 +755,11 @@ class ConsoleServerPortFilter(DeviceComponentFilterSet): fields = ['id', 'name', 'description', 'connection_status'] -class PowerPortFilter(DeviceComponentFilterSet): +class PowerPortFilterSet(DeviceComponentFilterSet): + type = django_filters.MultipleChoiceFilter( + choices=PowerPortTypeChoices, + null_value=None + ) cabled = django_filters.BooleanFilter( field_name='cable', lookup_expr='isnull', @@ -738,7 +771,11 @@ class PowerPortFilter(DeviceComponentFilterSet): fields = ['id', 'name', 'maximum_draw', 'allocated_draw', 'description', 'connection_status'] -class PowerOutletFilter(DeviceComponentFilterSet): +class PowerOutletFilterSet(DeviceComponentFilterSet): + type = django_filters.MultipleChoiceFilter( + choices=PowerOutletTypeChoices, + null_value=None + ) cabled = django_filters.BooleanFilter( field_name='cable', lookup_expr='isnull', @@ -750,7 +787,7 @@ class PowerOutletFilter(DeviceComponentFilterSet): fields = ['id', 'name', 'feed_leg', 'description', 'connection_status'] -class InterfaceFilter(django_filters.FilterSet): +class InterfaceFilterSet(django_filters.FilterSet): """ Not using DeviceComponentFilterSet for Interfaces because we need to check for VirtualChassis membership. """ @@ -758,6 +795,27 @@ class InterfaceFilter(django_filters.FilterSet): method='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', + to_field_name='slug', + queryset=Site.objects.all(), + label='Site name (slug)', + ) device = MultiValueCharFilter( method='filter_device', field_name='name', @@ -793,7 +851,7 @@ class InterfaceFilter(django_filters.FilterSet): label='Assigned VID' ) type = django_filters.MultipleChoiceFilter( - choices=IFACE_TYPE_CHOICES, + choices=InterfaceTypeChoices, null_value=None ) @@ -857,7 +915,7 @@ class InterfaceFilter(django_filters.FilterSet): }.get(value, queryset.none()) -class FrontPortFilter(DeviceComponentFilterSet): +class FrontPortFilterSet(DeviceComponentFilterSet): cabled = django_filters.BooleanFilter( field_name='cable', lookup_expr='isnull', @@ -869,7 +927,7 @@ class FrontPortFilter(DeviceComponentFilterSet): fields = ['id', 'name', 'type', 'description'] -class RearPortFilter(DeviceComponentFilterSet): +class RearPortFilterSet(DeviceComponentFilterSet): cabled = django_filters.BooleanFilter( field_name='cable', lookup_expr='isnull', @@ -881,14 +939,14 @@ class RearPortFilter(DeviceComponentFilterSet): fields = ['id', 'name', 'type', 'positions', 'description'] -class DeviceBayFilter(DeviceComponentFilterSet): +class DeviceBayFilterSet(DeviceComponentFilterSet): class Meta: model = DeviceBay fields = ['id', 'name', 'description'] -class InventoryItemFilter(DeviceComponentFilterSet): +class InventoryItemFilterSet(DeviceComponentFilterSet): q = django_filters.CharFilter( method='search', label='Search', @@ -959,7 +1017,7 @@ class InventoryItemFilter(DeviceComponentFilterSet): return queryset.filter(qs_filter) -class VirtualChassisFilter(django_filters.FilterSet): +class VirtualChassisFilterSet(django_filters.FilterSet): q = django_filters.CharFilter( method='search', label='Search', @@ -1013,16 +1071,16 @@ class VirtualChassisFilter(django_filters.FilterSet): return queryset.filter(qs_filter) -class CableFilter(django_filters.FilterSet): +class CableFilterSet(django_filters.FilterSet): q = django_filters.CharFilter( method='search', label='Search', ) type = django_filters.MultipleChoiceFilter( - choices=CABLE_TYPE_CHOICES + choices=CableTypeChoices ) status = django_filters.MultipleChoiceFilter( - choices=CONNECTION_STATUS_CHOICES + choices=CableStatusChoices ) color = django_filters.MultipleChoiceFilter( choices=COLOR_CHOICES @@ -1076,7 +1134,7 @@ class CableFilter(django_filters.FilterSet): return queryset -class ConsoleConnectionFilter(django_filters.FilterSet): +class ConsoleConnectionFilterSet(django_filters.FilterSet): site = django_filters.CharFilter( method='filter_site', label='Site (slug)', @@ -1107,7 +1165,7 @@ class ConsoleConnectionFilter(django_filters.FilterSet): ) -class PowerConnectionFilter(django_filters.FilterSet): +class PowerConnectionFilterSet(django_filters.FilterSet): site = django_filters.CharFilter( method='filter_site', label='Site (slug)', @@ -1138,7 +1196,7 @@ class PowerConnectionFilter(django_filters.FilterSet): ) -class InterfaceConnectionFilter(django_filters.FilterSet): +class InterfaceConnectionFilterSet(django_filters.FilterSet): site = django_filters.CharFilter( method='filter_site', label='Site (slug)', @@ -1172,7 +1230,7 @@ class InterfaceConnectionFilter(django_filters.FilterSet): ) -class PowerPanelFilter(django_filters.FilterSet): +class PowerPanelFilterSet(django_filters.FilterSet): id__in = NumericInFilter( field_name='id', lookup_expr='in' @@ -1221,7 +1279,7 @@ class PowerPanelFilter(django_filters.FilterSet): return queryset.filter(qs_filter) -class PowerFeedFilter(CustomFieldFilterSet, CreatedUpdatedFilterSet): +class PowerFeedFilterSet(CustomFieldFilterSet, CreatedUpdatedFilterSet): id__in = NumericInFilter( field_name='id', lookup_expr='in' diff --git a/netbox/dcim/fixtures/dcim.json b/netbox/dcim/fixtures/dcim.json index ece19a83c..b9f41edb5 100644 --- a/netbox/dcim/fixtures/dcim.json +++ b/netbox/dcim/fixtures/dcim.json @@ -1910,7 +1910,7 @@ "site": 1, "rack": 1, "position": 1, - "face": 0, + "face": "front", "status": true, "primary_ip4": 1, "primary_ip6": null, @@ -1931,7 +1931,7 @@ "site": 1, "rack": 1, "position": 17, - "face": 0, + "face": "rear", "status": true, "primary_ip4": 5, "primary_ip6": null, @@ -1952,7 +1952,7 @@ "site": 1, "rack": 1, "position": 33, - "face": 0, + "face": "rear", "status": true, "primary_ip4": null, "primary_ip6": null, @@ -1973,7 +1973,7 @@ "site": 1, "rack": 1, "position": 34, - "face": 0, + "face": "rear", "status": true, "primary_ip4": null, "primary_ip6": null, @@ -1994,7 +1994,7 @@ "site": 1, "rack": 2, "position": 34, - "face": 0, + "face": "rear", "status": true, "primary_ip4": null, "primary_ip6": null, @@ -2015,7 +2015,7 @@ "site": 1, "rack": 2, "position": 33, - "face": 0, + "face": "rear", "status": true, "primary_ip4": null, "primary_ip6": null, @@ -2036,7 +2036,7 @@ "site": 1, "rack": 2, "position": 1, - "face": 0, + "face": "rear", "status": true, "primary_ip4": 3, "primary_ip6": null, @@ -2057,7 +2057,7 @@ "site": 1, "rack": 2, "position": 17, - "face": 0, + "face": "rear", "status": true, "primary_ip4": 19, "primary_ip6": null, @@ -2078,7 +2078,7 @@ "site": 1, "rack": 1, "position": 42, - "face": 0, + "face": "rear", "status": true, "primary_ip4": null, "primary_ip6": null, @@ -2099,7 +2099,7 @@ "site": 1, "rack": 1, "position": null, - "face": null, + "face": "", "status": true, "primary_ip4": null, "primary_ip6": null, @@ -2120,7 +2120,7 @@ "site": 1, "rack": 2, "position": null, - "face": null, + "face": "", "status": true, "primary_ip4": null, "primary_ip6": null, diff --git a/netbox/dcim/fixtures/initial_data.json b/netbox/dcim/fixtures/initial_data.json deleted file mode 100644 index 83f79e3a3..000000000 --- a/netbox/dcim/fixtures/initial_data.json +++ /dev/null @@ -1,195 +0,0 @@ -[ -{ - "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" - } -} -] diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 4b5dd33cf..dda949824 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -16,16 +16,18 @@ from circuits.models import Circuit, Provider from extras.forms import ( AddRemoveTagsForm, CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm, LocalConfigContextFilterForm ) -from ipam.models import IPAddress, VLAN, VLANGroup +from ipam.constants import BGP_ASN_MAX, BGP_ASN_MIN +from ipam.models import IPAddress, VLAN from tenancy.forms import TenancyFilterForm, TenancyForm from tenancy.models import Tenant, TenantGroup from utilities.forms import ( APISelect, APISelectMultiple, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ChainedFieldsMixin, ChainedModelChoiceField, ColorSelect, CommentField, ComponentForm, ConfirmationForm, CSVChoiceField, ExpandableNameField, FilterChoiceField, FlexibleModelChoiceField, JSONField, - SelectWithPK, SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple, BOOLEAN_WITH_BLANK_CHOICES + SelectWithPK, SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple, BOOLEAN_WITH_BLANK_CHOICES, ) -from virtualization.models import Cluster, ClusterGroup +from virtualization.models import Cluster, ClusterGroup, VirtualMachine +from .choices import * from .constants import * from .models import ( Cable, DeviceBay, DeviceBayTemplate, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, @@ -55,6 +57,33 @@ def get_device_by_name_or_pk(name): return device +class DeviceComponentFilterForm(BootstrapMixin, forms.Form): + + field_order = [ + 'q', 'region', 'site' + ] + q = forms.CharField( + required=False, + label='Search' + ) + region = TreeNodeChoiceField( + queryset=Region.objects.all(), + required=False, + widget=APISelect( + api_url="/api/dcim/regions/" + ) + ) + site = forms.ModelChoiceField( + queryset=Site.objects.all(), + to_field_name='slug', + required=False, + help_text='Name of parent site', + error_messages={ + 'invalid_choice': 'Site not found.', + } + ) + + class InterfaceCommonForm: def clean(self): @@ -65,17 +94,17 @@ class InterfaceCommonForm: tagged_vlans = self.cleaned_data['tagged_vlans'] # Untagged interfaces cannot be assigned tagged VLANs - if self.cleaned_data['mode'] == IFACE_MODE_ACCESS and tagged_vlans: + if self.cleaned_data['mode'] == InterfaceModeChoices.MODE_ACCESS and tagged_vlans: raise forms.ValidationError({ 'mode': "An access interface cannot have tagged VLANs assigned." }) # Remove all tagged VLAN assignments from "tagged all" interfaces - elif self.cleaned_data['mode'] == IFACE_MODE_TAGGED_ALL: + elif self.cleaned_data['mode'] == InterfaceModeChoices.MODE_TAGGED_ALL: self.cleaned_data['tagged_vlans'] = [] # Validate tagged VLANs; must be a global VLAN or in the same site - elif self.cleaned_data['mode'] == IFACE_MODE_TAGGED: + elif self.cleaned_data['mode'] == InterfaceModeChoices.MODE_TAGGED: valid_sites = [None, self.cleaned_data['device'].site] invalid_vlans = [str(v) for v in tagged_vlans if v.site not in valid_sites] @@ -233,7 +262,7 @@ class SiteForm(BootstrapMixin, TenancyForm, CustomFieldForm): class SiteCSVForm(forms.ModelForm): status = CSVChoiceField( - choices=SITE_STATUS_CHOICES, + choices=SiteStatusChoices, required=False, help_text='Operational status' ) @@ -272,7 +301,7 @@ class SiteBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor widget=forms.MultipleHiddenInput ) status = forms.ChoiceField( - choices=add_blank_choice(SITE_STATUS_CHOICES), + choices=add_blank_choice(SiteStatusChoices), required=False, initial='', widget=StaticSelect2() @@ -321,7 +350,7 @@ class SiteFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): label='Search' ) status = forms.MultipleChoiceField( - choices=SITE_STATUS_CHOICES, + choices=SiteStatusChoices, required=False, widget=StaticSelect2Multiple() ) @@ -407,7 +436,7 @@ class RackRoleForm(BootstrapMixin, forms.ModelForm): class Meta: model = RackRole fields = [ - 'name', 'slug', 'color', + 'name', 'slug', 'color', 'description', ] @@ -495,7 +524,7 @@ class RackCSVForm(forms.ModelForm): } ) status = CSVChoiceField( - choices=RACK_STATUS_CHOICES, + choices=RackStatusChoices, required=False, help_text='Operational status' ) @@ -509,19 +538,16 @@ class RackCSVForm(forms.ModelForm): } ) type = CSVChoiceField( - choices=RACK_TYPE_CHOICES, + choices=RackTypeChoices, required=False, help_text='Rack type' ) width = forms.ChoiceField( - choices=( - (RACK_WIDTH_19IN, '19'), - (RACK_WIDTH_23IN, '23'), - ), + choices=RackWidthChoices, help_text='Rail-to-rail width (in inches)' ) outer_unit = CSVChoiceField( - choices=RACK_DIMENSION_UNIT_CHOICES, + choices=RackDimensionUnitChoices, required=False, help_text='Unit for outer dimensions' ) @@ -593,7 +619,7 @@ class RackBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor ) ) status = forms.ChoiceField( - choices=add_blank_choice(RACK_STATUS_CHOICES), + choices=add_blank_choice(RackStatusChoices), required=False, initial='', widget=StaticSelect2() @@ -615,12 +641,12 @@ class RackBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor required=False ) type = forms.ChoiceField( - choices=add_blank_choice(RACK_TYPE_CHOICES), + choices=add_blank_choice(RackTypeChoices), required=False, widget=StaticSelect2() ) width = forms.ChoiceField( - choices=add_blank_choice(RACK_WIDTH_CHOICES), + choices=add_blank_choice(RackWidthChoices), required=False, widget=StaticSelect2() ) @@ -642,7 +668,7 @@ class RackBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor min_value=1 ) outer_unit = forms.ChoiceField( - choices=add_blank_choice(RACK_DIMENSION_UNIT_CHOICES), + choices=add_blank_choice(RackDimensionUnitChoices), required=False, widget=StaticSelect2() ) @@ -698,7 +724,7 @@ class RackFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): ) ) status = forms.MultipleChoiceField( - choices=RACK_STATUS_CHOICES, + choices=RackStatusChoices, required=False, widget=StaticSelect2Multiple() ) @@ -890,29 +916,17 @@ class DeviceTypeForm(BootstrapMixin, CustomFieldForm): } -class DeviceTypeCSVForm(forms.ModelForm): +class DeviceTypeImportForm(BootstrapMixin, forms.ModelForm): manufacturer = forms.ModelChoiceField( queryset=Manufacturer.objects.all(), - required=True, - to_field_name='name', - help_text='Manufacturer name', - error_messages={ - 'invalid_choice': 'Manufacturer not found.', - } - ) - subdevice_role = CSVChoiceField( - choices=SUBDEVICE_ROLE_CHOICES, - required=False, - help_text='Parent/child status' + to_field_name='name' ) class Meta: model = DeviceType - fields = DeviceType.csv_headers - help_texts = { - 'model': 'Model name', - 'slug': 'URL-friendly slug', - } + fields = [ + 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role', + ] class DeviceTypeBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm): @@ -955,12 +969,10 @@ class DeviceTypeFilterForm(BootstrapMixin, CustomFieldFilterForm): value_field="slug", ) ) - subdevice_role = forms.NullBooleanField( + subdevice_role = forms.MultipleChoiceField( + choices=add_blank_choice(SubdeviceRoleChoices), required=False, - label='Subdevice role', - widget=StaticSelect2( - choices=add_blank_choice(SUBDEVICE_ROLE_CHOICES) - ) + widget=StaticSelect2Multiple() ) console_ports = forms.NullBooleanField( required=False, @@ -1015,7 +1027,7 @@ class ConsolePortTemplateForm(BootstrapMixin, forms.ModelForm): class Meta: model = ConsolePortTemplate fields = [ - 'device_type', 'name', + 'device_type', 'name', 'type', ] widgets = { 'device_type': forms.HiddenInput(), @@ -1026,6 +1038,10 @@ class ConsolePortTemplateCreateForm(ComponentForm): name_pattern = ExpandableNameField( label='Name' ) + type = forms.ChoiceField( + choices=ConsolePortTypeChoices, + widget=StaticSelect2() + ) class ConsoleServerPortTemplateForm(BootstrapMixin, forms.ModelForm): @@ -1033,7 +1049,7 @@ class ConsoleServerPortTemplateForm(BootstrapMixin, forms.ModelForm): class Meta: model = ConsoleServerPortTemplate fields = [ - 'device_type', 'name', + 'device_type', 'name', 'type', ] widgets = { 'device_type': forms.HiddenInput(), @@ -1044,6 +1060,10 @@ class ConsoleServerPortTemplateCreateForm(ComponentForm): name_pattern = ExpandableNameField( label='Name' ) + type = forms.ChoiceField( + choices=add_blank_choice(ConsolePortTypeChoices), + widget=StaticSelect2() + ) class PowerPortTemplateForm(BootstrapMixin, forms.ModelForm): @@ -1051,7 +1071,7 @@ class PowerPortTemplateForm(BootstrapMixin, forms.ModelForm): class Meta: model = PowerPortTemplate fields = [ - 'device_type', 'name', 'maximum_draw', 'allocated_draw', + 'device_type', 'name', 'type', 'maximum_draw', 'allocated_draw', ] widgets = { 'device_type': forms.HiddenInput(), @@ -1062,15 +1082,19 @@ class PowerPortTemplateCreateForm(ComponentForm): name_pattern = ExpandableNameField( label='Name' ) + type = forms.ChoiceField( + choices=add_blank_choice(PowerPortTypeChoices), + required=False + ) maximum_draw = forms.IntegerField( min_value=1, required=False, - help_text="Maximum current draw (watts)" + help_text="Maximum power draw (watts)" ) allocated_draw = forms.IntegerField( min_value=1, required=False, - help_text="Allocated current draw (watts)" + help_text="Allocated power draw (watts)" ) @@ -1079,7 +1103,7 @@ class PowerOutletTemplateForm(BootstrapMixin, forms.ModelForm): class Meta: model = PowerOutletTemplate fields = [ - 'device_type', 'name', 'power_port', 'feed_leg', + 'device_type', 'name', 'type', 'power_port', 'feed_leg', ] widgets = { 'device_type': forms.HiddenInput(), @@ -1100,12 +1124,16 @@ class PowerOutletTemplateCreateForm(ComponentForm): name_pattern = ExpandableNameField( label='Name' ) + type = forms.ChoiceField( + choices=add_blank_choice(PowerOutletTypeChoices), + required=False + ) power_port = forms.ModelChoiceField( queryset=PowerPortTemplate.objects.all(), required=False ) feed_leg = forms.ChoiceField( - choices=add_blank_choice(POWERFEED_LEG_CHOICES), + choices=add_blank_choice(PowerOutletFeedLegChoices), required=False, widget=StaticSelect2() ) @@ -1138,7 +1166,7 @@ class InterfaceTemplateCreateForm(ComponentForm): label='Name' ) type = forms.ChoiceField( - choices=IFACE_TYPE_CHOICES, + choices=InterfaceTypeChoices, widget=StaticSelect2() ) mgmt_only = forms.BooleanField( @@ -1153,7 +1181,7 @@ class InterfaceTemplateBulkEditForm(BootstrapMixin, BulkEditForm): widget=forms.MultipleHiddenInput() ) type = forms.ChoiceField( - choices=add_blank_choice(IFACE_TYPE_CHOICES), + choices=add_blank_choice(InterfaceTypeChoices), required=False, widget=StaticSelect2() ) @@ -1195,7 +1223,7 @@ class FrontPortTemplateCreateForm(ComponentForm): label='Name' ) type = forms.ChoiceField( - choices=PORT_TYPE_CHOICES, + choices=PortTypeChoices, widget=StaticSelect2() ) rear_port_set = forms.MultipleChoiceField( @@ -1265,7 +1293,7 @@ class RearPortTemplateCreateForm(ComponentForm): label='Name' ) type = forms.ChoiceField( - choices=PORT_TYPE_CHOICES, + choices=PortTypeChoices, widget=StaticSelect2(), ) positions = forms.IntegerField( @@ -1294,6 +1322,124 @@ class DeviceBayTemplateCreateForm(ComponentForm): ) +# +# Component template import forms +# + +class ComponentTemplateImportForm(BootstrapMixin, forms.ModelForm): + + def __init__(self, device_type, data=None, *args, **kwargs): + + # Must pass the parent DeviceType on form initialization + data.update({ + 'device_type': device_type.pk, + }) + + super().__init__(data, *args, **kwargs) + + def clean_device_type(self): + + data = self.cleaned_data['device_type'] + + # Limit fields referencing other components to the parent DeviceType + for field_name, field in self.fields.items(): + if isinstance(field, forms.ModelChoiceField) and field_name != 'device_type': + field.queryset = field.queryset.filter(device_type=data) + + return data + + +class ConsolePortTemplateImportForm(ComponentTemplateImportForm): + + class Meta: + model = ConsolePortTemplate + fields = [ + 'device_type', 'name', 'type', + ] + + +class ConsoleServerPortTemplateImportForm(ComponentTemplateImportForm): + + class Meta: + model = ConsoleServerPortTemplate + fields = [ + 'device_type', 'name', 'type', + ] + + +class PowerPortTemplateImportForm(ComponentTemplateImportForm): + + class Meta: + model = PowerPortTemplate + fields = [ + 'device_type', 'name', 'type', 'maximum_draw', 'allocated_draw', + ] + + +class PowerOutletTemplateImportForm(ComponentTemplateImportForm): + power_port = forms.ModelChoiceField( + queryset=PowerPortTemplate.objects.all(), + to_field_name='name', + required=False + ) + + class Meta: + model = PowerOutletTemplate + fields = [ + 'device_type', 'name', 'type', 'power_port', 'feed_leg', + ] + + +class InterfaceTemplateImportForm(ComponentTemplateImportForm): + type = forms.ChoiceField( + choices=InterfaceTypeChoices.CHOICES + ) + + class Meta: + model = InterfaceTemplate + fields = [ + 'device_type', 'name', 'type', 'mgmt_only', + ] + + +class FrontPortTemplateImportForm(ComponentTemplateImportForm): + type = forms.ChoiceField( + choices=PortTypeChoices.CHOICES + ) + rear_port = forms.ModelChoiceField( + queryset=RearPortTemplate.objects.all(), + to_field_name='name', + required=False + ) + + class Meta: + model = FrontPortTemplate + fields = [ + 'device_type', 'name', 'type', 'rear_port', 'rear_port_position', + ] + + +class RearPortTemplateImportForm(ComponentTemplateImportForm): + type = forms.ChoiceField( + choices=PortTypeChoices.CHOICES + ) + + class Meta: + model = RearPortTemplate + fields = [ + 'device_type', 'name', 'type', 'positions', + ] + + +class DeviceBayTemplateImportForm(ComponentTemplateImportForm): + + class Meta: + model = DeviceBayTemplate + fields = [ + 'device_type', 'name', + ] + + # # Device roles # @@ -1304,7 +1450,7 @@ class DeviceRoleForm(BootstrapMixin, forms.ModelForm): class Meta: model = DeviceRole fields = [ - 'name', 'slug', 'color', 'vm_role', + 'name', 'slug', 'color', 'vm_role', 'description', ] @@ -1392,7 +1538,7 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldForm): empty_value=None, help_text="The lowest-numbered unit occupied by the device", widget=APISelect( - api_url='/api/dcim/racks/{{rack}}/units/', + api_url='/api/dcim/racks/{{rack}}/elevation/', disabled_indicator='device' ) ) @@ -1603,7 +1749,7 @@ class BaseDeviceCSVForm(forms.ModelForm): } ) status = CSVChoiceField( - choices=DEVICE_STATUS_CHOICES, + choices=DeviceStatusChoices, help_text='Operational status' ) @@ -1647,7 +1793,7 @@ class DeviceCSVForm(BaseDeviceCSVForm): help_text='Name of parent rack' ) face = CSVChoiceField( - choices=RACK_FACE_CHOICES, + choices=DeviceFaceChoices, required=False, help_text='Mounted rack face' ) @@ -1771,7 +1917,7 @@ class DeviceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF ) ) status = forms.ChoiceField( - choices=add_blank_choice(DEVICE_STATUS_CHOICES), + choices=add_blank_choice(DeviceStatusChoices), required=False, initial='', widget=StaticSelect2() @@ -1882,7 +2028,7 @@ class DeviceFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilt ) ) status = forms.MultipleChoiceField( - choices=DEVICE_STATUS_CHOICES, + choices=DeviceStatusChoices, required=False, widget=StaticSelect2Multiple() ) @@ -1964,7 +2110,7 @@ class DeviceBulkAddComponentForm(BootstrapMixin, forms.Form): class DeviceBulkAddInterfaceForm(DeviceBulkAddComponentForm): type = forms.ChoiceField( - choices=IFACE_TYPE_CHOICES, + choices=InterfaceTypeChoices, widget=StaticSelect2() ) enabled = forms.BooleanField( @@ -1991,6 +2137,11 @@ class DeviceBulkAddInterfaceForm(DeviceBulkAddComponentForm): # Console ports # + +class ConsolePortFilterForm(DeviceComponentFilterForm): + model = ConsolePort + + class ConsolePortForm(BootstrapMixin, forms.ModelForm): tags = TagField( required=False @@ -1999,7 +2150,7 @@ class ConsolePortForm(BootstrapMixin, forms.ModelForm): class Meta: model = ConsolePort fields = [ - 'device', 'name', 'description', 'tags', + 'device', 'name', 'type', 'description', 'tags', ] widgets = { 'device': forms.HiddenInput(), @@ -2010,6 +2161,11 @@ class ConsolePortCreateForm(ComponentForm): name_pattern = ExpandableNameField( label='Name' ) + type = forms.ChoiceField( + choices=add_blank_choice(ConsolePortTypeChoices), + required=False, + widget=StaticSelect2() + ) description = forms.CharField( max_length=100, required=False @@ -2019,10 +2175,30 @@ class ConsolePortCreateForm(ComponentForm): ) +class ConsolePortCSVForm(forms.ModelForm): + device = FlexibleModelChoiceField( + queryset=Device.objects.all(), + to_field_name='name', + help_text='Name or ID of device', + error_messages={ + 'invalid_choice': 'Device not found.', + } + ) + + class Meta: + model = ConsolePort + fields = ConsolePort.csv_headers + + # # Console server ports # + +class ConsoleServerPortFilterForm(DeviceComponentFilterForm): + model = ConsoleServerPort + + class ConsoleServerPortForm(BootstrapMixin, forms.ModelForm): tags = TagField( required=False @@ -2031,7 +2207,7 @@ class ConsoleServerPortForm(BootstrapMixin, forms.ModelForm): class Meta: model = ConsoleServerPort fields = [ - 'device', 'name', 'description', 'tags', + 'device', 'name', 'type', 'description', 'tags', ] widgets = { 'device': forms.HiddenInput(), @@ -2042,6 +2218,11 @@ class ConsoleServerPortCreateForm(ComponentForm): name_pattern = ExpandableNameField( label='Name' ) + type = forms.ChoiceField( + choices=add_blank_choice(ConsolePortTypeChoices), + required=False, + widget=StaticSelect2() + ) description = forms.CharField( max_length=100, required=False @@ -2056,6 +2237,11 @@ class ConsoleServerPortBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditF queryset=ConsoleServerPort.objects.all(), widget=forms.MultipleHiddenInput() ) + type = forms.ChoiceField( + choices=add_blank_choice(ConsolePortTypeChoices), + required=False, + widget=StaticSelect2() + ) description = forms.CharField( max_length=100, required=False @@ -2081,10 +2267,30 @@ class ConsoleServerPortBulkDisconnectForm(ConfirmationForm): ) +class ConsoleServerPortCSVForm(forms.ModelForm): + device = FlexibleModelChoiceField( + queryset=Device.objects.all(), + to_field_name='name', + help_text='Name or ID of device', + error_messages={ + 'invalid_choice': 'Device not found.', + } + ) + + class Meta: + model = ConsoleServerPort + fields = ConsoleServerPort.csv_headers + + # # Power ports # + +class PowerPortFilterForm(DeviceComponentFilterForm): + model = PowerPort + + class PowerPortForm(BootstrapMixin, forms.ModelForm): tags = TagField( required=False @@ -2093,7 +2299,7 @@ class PowerPortForm(BootstrapMixin, forms.ModelForm): class Meta: model = PowerPort fields = [ - 'device', 'name', 'maximum_draw', 'allocated_draw', 'description', 'tags', + 'device', 'name', 'type', 'maximum_draw', 'allocated_draw', 'description', 'tags', ] widgets = { 'device': forms.HiddenInput(), @@ -2104,6 +2310,11 @@ class PowerPortCreateForm(ComponentForm): name_pattern = ExpandableNameField( label='Name' ) + type = forms.ChoiceField( + choices=add_blank_choice(PowerPortTypeChoices), + required=False, + widget=StaticSelect2() + ) maximum_draw = forms.IntegerField( min_value=1, required=False, @@ -2123,10 +2334,30 @@ class PowerPortCreateForm(ComponentForm): ) +class PowerPortCSVForm(forms.ModelForm): + device = FlexibleModelChoiceField( + queryset=Device.objects.all(), + to_field_name='name', + help_text='Name or ID of device', + error_messages={ + 'invalid_choice': 'Device not found.', + } + ) + + class Meta: + model = PowerPort + fields = PowerPort.csv_headers + + # # Power outlets # + +class PowerOutletFilterForm(DeviceComponentFilterForm): + model = PowerOutlet + + class PowerOutletForm(BootstrapMixin, forms.ModelForm): power_port = forms.ModelChoiceField( queryset=PowerPort.objects.all(), @@ -2139,7 +2370,7 @@ class PowerOutletForm(BootstrapMixin, forms.ModelForm): class Meta: model = PowerOutlet fields = [ - 'device', 'name', 'power_port', 'feed_leg', 'description', 'tags', + 'device', 'name', 'type', 'power_port', 'feed_leg', 'description', 'tags', ] widgets = { 'device': forms.HiddenInput(), @@ -2159,12 +2390,17 @@ class PowerOutletCreateForm(ComponentForm): name_pattern = ExpandableNameField( label='Name' ) + type = forms.ChoiceField( + choices=add_blank_choice(PowerOutletTypeChoices), + required=False, + widget=StaticSelect2() + ) power_port = forms.ModelChoiceField( queryset=PowerPort.objects.all(), required=False ) feed_leg = forms.ChoiceField( - choices=add_blank_choice(POWERFEED_LEG_CHOICES), + choices=add_blank_choice(PowerOutletFeedLegChoices), required=False ) description = forms.CharField( @@ -2183,13 +2419,67 @@ class PowerOutletCreateForm(ComponentForm): self.fields['power_port'].queryset = PowerPort.objects.filter(device=self.parent) +class PowerOutletCSVForm(forms.ModelForm): + device = FlexibleModelChoiceField( + queryset=Device.objects.all(), + to_field_name='name', + help_text='Name or ID of device', + error_messages={ + 'invalid_choice': 'Device not found.', + } + ) + power_port = FlexibleModelChoiceField( + queryset=PowerPort.objects.all(), + required=False, + to_field_name='name', + help_text='Name or ID of Power Port', + error_messages={ + 'invalid_choice': 'Power Port not found.', + } + ) + feed_leg = CSVChoiceField( + choices=PowerOutletFeedLegChoices, + required=False, + ) + + class Meta: + model = PowerOutlet + fields = PowerOutlet.csv_headers + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # Limit PowerPort choices to those belonging to this device (or VC master) + if self.is_bound: + try: + device = self.fields['device'].to_python(self.data['device']) + except forms.ValidationError: + device = None + else: + try: + device = self.instance.device + except Device.DoesNotExist: + device = None + + if device: + self.fields['power_port'].queryset = PowerPort.objects.filter( + device__in=[device, device.get_vc_master()] + ) + else: + self.fields['power_port'].queryset = PowerPort.objects.none() + + class PowerOutletBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=PowerOutlet.objects.all(), widget=forms.MultipleHiddenInput() ) + type = forms.ChoiceField( + choices=PowerOutletTypeChoices, + required=False + ) feed_leg = forms.ChoiceField( - choices=add_blank_choice(POWERFEED_LEG_CHOICES), + choices=add_blank_choice(PowerOutletFeedLegChoices), required=False, ) power_port = forms.ModelChoiceField( @@ -2203,7 +2493,7 @@ class PowerOutletBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm): class Meta: nullable_fields = [ - 'feed_leg', 'power_port', 'description', + 'type', 'feed_leg', 'power_port', 'description', ] def __init__(self, *args, **kwargs): @@ -2231,6 +2521,11 @@ class PowerOutletBulkDisconnectForm(ConfirmationForm): # Interfaces # + +class InterfaceFilterForm(DeviceComponentFilterForm): + model = Interface + + class InterfaceForm(InterfaceCommonForm, BootstrapMixin, forms.ModelForm): untagged_vlan = forms.ModelChoiceField( queryset=VLAN.objects.all(), @@ -2287,12 +2582,14 @@ class InterfaceForm(InterfaceCommonForm, BootstrapMixin, forms.ModelForm): if self.is_bound: device = Device.objects.get(pk=self.data['device']) self.fields['lag'].queryset = Interface.objects.filter( - device__in=[device, device.get_vc_master()], type=IFACE_TYPE_LAG + device__in=[device, device.get_vc_master()], + type=InterfaceTypeChoices.TYPE_LAG ) else: device = self.instance.device self.fields['lag'].queryset = Interface.objects.filter( - device__in=[self.instance.device, self.instance.device.get_vc_master()], type=IFACE_TYPE_LAG + device__in=[self.instance.device, self.instance.device.get_vc_master()], + type=InterfaceTypeChoices.TYPE_LAG ) # Add current site to VLANs query params @@ -2305,7 +2602,7 @@ class InterfaceCreateForm(InterfaceCommonForm, ComponentForm, forms.Form): label='Name' ) type = forms.ChoiceField( - choices=IFACE_TYPE_CHOICES, + choices=InterfaceTypeChoices, widget=StaticSelect2(), ) enabled = forms.BooleanField( @@ -2337,7 +2634,7 @@ class InterfaceCreateForm(InterfaceCommonForm, ComponentForm, forms.Form): required=False ) mode = forms.ChoiceField( - choices=add_blank_choice(IFACE_MODE_CHOICES), + choices=add_blank_choice(InterfaceModeChoices), required=False, widget=StaticSelect2(), ) @@ -2380,7 +2677,8 @@ class InterfaceCreateForm(InterfaceCommonForm, ComponentForm, forms.Form): # Limit LAG choices to interfaces belonging to this device (or its VC master) if self.parent is not None: self.fields['lag'].queryset = Interface.objects.filter( - device__in=[self.parent, self.parent.get_vc_master()], type=IFACE_TYPE_LAG + device__in=[self.parent, self.parent.get_vc_master()], + type=InterfaceTypeChoices.TYPE_LAG ) # Add current site to VLANs query params @@ -2390,13 +2688,80 @@ class InterfaceCreateForm(InterfaceCommonForm, ComponentForm, forms.Form): self.fields['lag'].queryset = Interface.objects.none() +class InterfaceCSVForm(forms.ModelForm): + device = FlexibleModelChoiceField( + queryset=Device.objects.all(), + required=False, + to_field_name='name', + help_text='Name or ID of device', + error_messages={ + 'invalid_choice': 'Device not found.', + } + ) + virtual_machine = FlexibleModelChoiceField( + queryset=VirtualMachine.objects.all(), + required=False, + to_field_name='name', + help_text='Name or ID of virtual machine', + error_messages={ + 'invalid_choice': 'Virtual machine not found.', + } + ) + lag = FlexibleModelChoiceField( + queryset=Interface.objects.all(), + required=False, + to_field_name='name', + help_text='Name or ID of LAG interface', + error_messages={ + 'invalid_choice': 'LAG interface not found.', + } + ) + type = CSVChoiceField( + choices=InterfaceTypeChoices, + ) + mode = CSVChoiceField( + choices=InterfaceModeChoices, + required=False, + ) + + class Meta: + model = Interface + fields = Interface.csv_headers + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # Limit LAG choices to interfaces belonging to this device (or VC master) + if self.is_bound: + try: + device = self.fields['device'].to_python(self.data['device']) + except forms.ValidationError: + device = None + else: + device = self.instance.device + + if device: + self.fields['lag'].queryset = Interface.objects.filter( + device__in=[device, device.get_vc_master()], type=InterfaceTypeChoices.TYPE_LAG + ) + else: + self.fields['lag'].queryset = Interface.objects.none() + + def clean_enabled(self): + # Make sure enabled is True when it's not included in the uploaded data + if 'enabled' not in self.data: + return True + else: + return self.cleaned_data['enabled'] + + class InterfaceBulkEditForm(InterfaceCommonForm, BootstrapMixin, AddRemoveTagsForm, BulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=Interface.objects.all(), widget=forms.MultipleHiddenInput() ) type = forms.ChoiceField( - choices=add_blank_choice(IFACE_TYPE_CHOICES), + choices=add_blank_choice(InterfaceTypeChoices), required=False, widget=StaticSelect2() ) @@ -2430,7 +2795,7 @@ class InterfaceBulkEditForm(InterfaceCommonForm, BootstrapMixin, AddRemoveTagsFo required=False ) mode = forms.ChoiceField( - choices=add_blank_choice(IFACE_MODE_CHOICES), + choices=add_blank_choice(InterfaceModeChoices), required=False, widget=StaticSelect2() ) @@ -2472,7 +2837,7 @@ class InterfaceBulkEditForm(InterfaceCommonForm, BootstrapMixin, AddRemoveTagsFo if device is not None: self.fields['lag'].queryset = Interface.objects.filter( device__in=[device, device.get_vc_master()], - type=IFACE_TYPE_LAG + type=InterfaceTypeChoices.TYPE_LAG ) # Add current site to VLANs query params @@ -2500,6 +2865,10 @@ class InterfaceBulkDisconnectForm(ConfirmationForm): # Front pass-through ports # +class FrontPortFilterForm(DeviceComponentFilterForm): + model = FrontPort + + class FrontPortForm(BootstrapMixin, forms.ModelForm): tags = TagField( required=False @@ -2532,7 +2901,7 @@ class FrontPortCreateForm(ComponentForm): label='Name' ) type = forms.ChoiceField( - choices=PORT_TYPE_CHOICES, + choices=PortTypeChoices, widget=StaticSelect2(), ) rear_port_set = forms.MultipleChoiceField( @@ -2586,13 +2955,61 @@ class FrontPortCreateForm(ComponentForm): } +class FrontPortCSVForm(forms.ModelForm): + device = FlexibleModelChoiceField( + queryset=Device.objects.all(), + to_field_name='name', + help_text='Name or ID of device', + error_messages={ + 'invalid_choice': 'Device not found.', + } + ) + rear_port = FlexibleModelChoiceField( + queryset=RearPort.objects.all(), + to_field_name='name', + help_text='Name or ID of Rear Port', + error_messages={ + 'invalid_choice': 'Rear Port not found.', + } + ) + type = CSVChoiceField( + choices=PortTypeChoices, + ) + + class Meta: + model = FrontPort + fields = FrontPort.csv_headers + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # Limit RearPort choices to those belonging to this device (or VC master) + if self.is_bound: + try: + device = self.fields['device'].to_python(self.data['device']) + except forms.ValidationError: + device = None + else: + try: + device = self.instance.device + except Device.DoesNotExist: + device = None + + if device: + self.fields['rear_port'].queryset = RearPort.objects.filter( + device__in=[device, device.get_vc_master()] + ) + else: + self.fields['rear_port'].queryset = RearPort.objects.none() + + class FrontPortBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=FrontPort.objects.all(), widget=forms.MultipleHiddenInput() ) type = forms.ChoiceField( - choices=add_blank_choice(PORT_TYPE_CHOICES), + choices=add_blank_choice(PortTypeChoices), required=False, widget=StaticSelect2() ) @@ -2625,6 +3042,10 @@ class FrontPortBulkDisconnectForm(ConfirmationForm): # Rear pass-through ports # +class RearPortFilterForm(DeviceComponentFilterForm): + model = RearPort + + class RearPortForm(BootstrapMixin, forms.ModelForm): tags = TagField( required=False @@ -2646,7 +3067,7 @@ class RearPortCreateForm(ComponentForm): label='Name' ) type = forms.ChoiceField( - choices=PORT_TYPE_CHOICES, + choices=PortTypeChoices, widget=StaticSelect2(), ) positions = forms.IntegerField( @@ -2660,13 +3081,31 @@ class RearPortCreateForm(ComponentForm): ) +class RearPortCSVForm(forms.ModelForm): + device = FlexibleModelChoiceField( + queryset=Device.objects.all(), + to_field_name='name', + help_text='Name or ID of device', + error_messages={ + 'invalid_choice': 'Device not found.', + } + ) + type = CSVChoiceField( + choices=PortTypeChoices, + ) + + class Meta: + model = RearPort + fields = RearPort.csv_headers + + class RearPortBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=RearPort.objects.all(), widget=forms.MultipleHiddenInput() ) type = forms.ChoiceField( - choices=add_blank_choice(PORT_TYPE_CHOICES), + choices=add_blank_choice(PortTypeChoices), required=False, widget=StaticSelect2() ) @@ -2965,9 +3404,7 @@ class CableCSVForm(forms.ModelForm): ) side_a_type = forms.ModelChoiceField( queryset=ContentType.objects.all(), - limit_choices_to={ - 'model__in': CABLE_TERMINATION_TYPES, - }, + limit_choices_to=CABLE_TERMINATION_MODELS, to_field_name='model', help_text='Side A type' ) @@ -2986,9 +3423,7 @@ class CableCSVForm(forms.ModelForm): ) side_b_type = forms.ModelChoiceField( queryset=ContentType.objects.all(), - limit_choices_to={ - 'model__in': CABLE_TERMINATION_TYPES, - }, + limit_choices_to=CABLE_TERMINATION_MODELS, to_field_name='model', help_text='Side B type' ) @@ -2998,17 +3433,17 @@ class CableCSVForm(forms.ModelForm): # Cable attributes status = CSVChoiceField( - choices=CONNECTION_STATUS_CHOICES, + choices=CableStatusChoices, required=False, help_text='Connection status' ) type = CSVChoiceField( - choices=CABLE_TYPE_CHOICES, + choices=CableTypeChoices, required=False, help_text='Cable type' ) length_unit = CSVChoiceField( - choices=CABLE_LENGTH_UNIT_CHOICES, + choices=CableLengthUnitChoices, required=False, help_text='Length unit' ) @@ -3088,13 +3523,13 @@ class CableBulkEditForm(BootstrapMixin, BulkEditForm): widget=forms.MultipleHiddenInput ) type = forms.ChoiceField( - choices=add_blank_choice(CABLE_TYPE_CHOICES), + choices=add_blank_choice(CableTypeChoices), required=False, initial='', widget=StaticSelect2() ) status = forms.ChoiceField( - choices=add_blank_choice(CONNECTION_STATUS_CHOICES), + choices=add_blank_choice(CableStatusChoices), required=False, widget=StaticSelect2(), initial='' @@ -3113,7 +3548,7 @@ class CableBulkEditForm(BootstrapMixin, BulkEditForm): required=False ) length_unit = forms.ChoiceField( - choices=add_blank_choice(CABLE_LENGTH_UNIT_CHOICES), + choices=add_blank_choice(CableLengthUnitChoices), required=False, initial='', widget=StaticSelect2() @@ -3173,13 +3608,13 @@ class CableFilterForm(BootstrapMixin, forms.Form): ) ) type = forms.MultipleChoiceField( - choices=add_blank_choice(CABLE_TYPE_CHOICES), + choices=add_blank_choice(CableTypeChoices), required=False, widget=StaticSelect2() ) status = forms.ChoiceField( required=False, - choices=add_blank_choice(CONNECTION_STATUS_CHOICES), + choices=add_blank_choice(CableStatusChoices), widget=StaticSelect2() ) color = forms.CharField( @@ -3201,6 +3636,10 @@ class CableFilterForm(BootstrapMixin, forms.Form): # Device bays # +class DeviceBayFilterForm(DeviceComponentFilterForm): + model = DeviceBay + + class DeviceBayForm(BootstrapMixin, forms.ModelForm): tags = TagField( required=False @@ -3242,10 +3681,60 @@ class PopulateDeviceBayForm(BootstrapMixin, forms.Form): rack=device_bay.device.rack, parent_bay__isnull=True, device_type__u_height=0, - device_type__subdevice_role=SUBDEVICE_ROLE_CHILD + device_type__subdevice_role=SubdeviceRoleChoices.ROLE_CHILD ).exclude(pk=device_bay.device.pk) +class DeviceBayCSVForm(forms.ModelForm): + device = FlexibleModelChoiceField( + queryset=Device.objects.all(), + to_field_name='name', + help_text='Name or ID of device', + error_messages={ + 'invalid_choice': 'Device not found.', + } + ) + installed_device = FlexibleModelChoiceField( + queryset=Device.objects.all(), + required=False, + to_field_name='name', + help_text='Name or ID of device', + error_messages={ + 'invalid_choice': 'Child device not found.', + } + ) + + class Meta: + model = DeviceBay + fields = DeviceBay.csv_headers + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # Limit installed device choices to devices of the correct type and location + if self.is_bound: + try: + device = self.fields['device'].to_python(self.data['device']) + except forms.ValidationError: + device = None + else: + try: + device = self.instance.device + except Device.DoesNotExist: + device = None + + if device: + self.fields['installed_device'].queryset = Device.objects.filter( + site=device.site, + rack=device.rack, + parent_bay__isnull=True, + device_type__u_height=0, + device_type__subdevice_role=SubdeviceRoleChoices.ROLE_CHILD + ).exclude(pk=device.pk) + else: + self.fields['installed_device'].queryset = Interface.objects.none() + + class DeviceBayBulkRenameForm(BulkRenameForm): pk = forms.ModelMultipleChoiceField( queryset=DeviceBay.objects.all(), @@ -3817,22 +4306,22 @@ class PowerFeedCSVForm(forms.ModelForm): help_text="Rack name (optional)" ) status = CSVChoiceField( - choices=POWERFEED_STATUS_CHOICES, + choices=PowerFeedStatusChoices, required=False, help_text='Operational status' ) type = CSVChoiceField( - choices=POWERFEED_TYPE_CHOICES, + choices=PowerFeedTypeChoices, required=False, help_text='Primary or redundant' ) supply = CSVChoiceField( - choices=POWERFEED_SUPPLY_CHOICES, + choices=PowerFeedSupplyChoices, required=False, help_text='AC/DC' ) phase = CSVChoiceField( - choices=POWERFEED_PHASE_CHOICES, + choices=PowerFeedPhaseChoices, required=False, help_text='Single or three-phase' ) @@ -3892,25 +4381,25 @@ class PowerFeedBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEd ) ) status = forms.ChoiceField( - choices=add_blank_choice(POWERFEED_STATUS_CHOICES), + choices=add_blank_choice(PowerFeedStatusChoices), required=False, initial='', widget=StaticSelect2() ) type = forms.ChoiceField( - choices=add_blank_choice(POWERFEED_TYPE_CHOICES), + choices=add_blank_choice(PowerFeedTypeChoices), required=False, initial='', widget=StaticSelect2() ) supply = forms.ChoiceField( - choices=add_blank_choice(POWERFEED_SUPPLY_CHOICES), + choices=add_blank_choice(PowerFeedSupplyChoices), required=False, initial='', widget=StaticSelect2() ) phase = forms.ChoiceField( - choices=add_blank_choice(POWERFEED_PHASE_CHOICES), + choices=add_blank_choice(PowerFeedPhaseChoices), required=False, initial='', widget=StaticSelect2() @@ -3983,22 +4472,22 @@ class PowerFeedFilterForm(BootstrapMixin, CustomFieldFilterForm): ) ) status = forms.MultipleChoiceField( - choices=POWERFEED_STATUS_CHOICES, + choices=PowerFeedStatusChoices, required=False, widget=StaticSelect2Multiple() ) type = forms.ChoiceField( - choices=add_blank_choice(POWERFEED_TYPE_CHOICES), + choices=add_blank_choice(PowerFeedTypeChoices), required=False, widget=StaticSelect2() ) supply = forms.ChoiceField( - choices=add_blank_choice(POWERFEED_SUPPLY_CHOICES), + choices=add_blank_choice(PowerFeedSupplyChoices), required=False, widget=StaticSelect2() ) phase = forms.ChoiceField( - choices=add_blank_choice(POWERFEED_PHASE_CHOICES), + choices=add_blank_choice(PowerFeedPhaseChoices), required=False, widget=StaticSelect2() ) diff --git a/netbox/dcim/migrations/0002_auto_20160622_1821_squashed_0022_color_names_to_rgb.py b/netbox/dcim/migrations/0002_auto_20160622_1821_squashed_0022_color_names_to_rgb.py deleted file mode 100644 index c3412cf10..000000000 --- a/netbox/dcim/migrations/0002_auto_20160622_1821_squashed_0022_color_names_to_rgb.py +++ /dev/null @@ -1,259 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11.14 on 2018-07-31 02:06 -import dcim.fields -import django.core.validators -from django.db import migrations, models -import django.db.models.deletion -import utilities.fields - - -class Migration(migrations.Migration): - - replaces = [('dcim', '0002_auto_20160622_1821'), ('dcim', '0003_auto_20160628_1721'), ('dcim', '0004_auto_20160701_2049'), ('dcim', '0005_auto_20160706_1722'), ('dcim', '0006_add_device_primary_ip4_ip6'), ('dcim', '0007_device_copy_primary_ip'), ('dcim', '0008_device_remove_primary_ip'), ('dcim', '0009_site_32bit_asn_support'), ('dcim', '0010_devicebay_installed_device_set_null'), ('dcim', '0011_devicetype_part_number'), ('dcim', '0012_site_rack_device_add_tenant'), ('dcim', '0013_add_interface_form_factors'), ('dcim', '0014_rack_add_type_width'), ('dcim', '0015_rack_add_u_height_validator'), ('dcim', '0016_module_add_manufacturer'), ('dcim', '0017_rack_add_role'), ('dcim', '0018_device_add_asset_tag'), ('dcim', '0019_new_iface_form_factors'), ('dcim', '0020_rack_desc_units'), ('dcim', '0021_add_ff_flexstack'), ('dcim', '0022_color_names_to_rgb')] - - dependencies = [ - ('dcim', '0001_initial'), - ('ipam', '0001_initial'), - ('tenancy', '0001_initial'), - ] - - operations = [ - migrations.AddField( - model_name='device', - name='rack', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='devices', to='dcim.Rack'), - ), - migrations.AddField( - model_name='consoleserverporttemplate', - name='device_type', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='cs_port_templates', to='dcim.DeviceType'), - ), - migrations.AddField( - model_name='consoleserverport', - name='device', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='cs_ports', to='dcim.Device'), - ), - migrations.AddField( - model_name='consoleporttemplate', - name='device_type', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='console_port_templates', to='dcim.DeviceType'), - ), - migrations.AddField( - model_name='consoleport', - name='cs_port', - field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='connected_console', to='dcim.ConsoleServerPort', verbose_name=b'Console server port'), - ), - migrations.AddField( - model_name='consoleport', - name='device', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='console_ports', to='dcim.Device'), - ), - migrations.AlterUniqueTogether( - name='rackgroup', - unique_together=set([('site', 'name'), ('site', 'slug')]), - ), - migrations.AlterUniqueTogether( - name='rack', - unique_together=set([('site', 'facility_id'), ('site', 'name')]), - ), - migrations.AlterUniqueTogether( - name='powerporttemplate', - unique_together=set([('device_type', 'name')]), - ), - migrations.AlterUniqueTogether( - name='powerport', - unique_together=set([('device', 'name')]), - ), - migrations.AlterUniqueTogether( - name='poweroutlettemplate', - unique_together=set([('device_type', 'name')]), - ), - migrations.AlterUniqueTogether( - name='poweroutlet', - unique_together=set([('device', 'name')]), - ), - migrations.AlterUniqueTogether( - name='module', - unique_together=set([('device', 'parent', 'name')]), - ), - migrations.AlterUniqueTogether( - name='interfacetemplate', - unique_together=set([('device_type', 'name')]), - ), - migrations.AddField( - model_name='interface', - name='mac_address', - field=dcim.fields.MACAddressField(blank=True, null=True, verbose_name=b'MAC Address'), - ), - migrations.AlterUniqueTogether( - name='interface', - unique_together=set([('device', 'name')]), - ), - migrations.AddField( - model_name='devicetype', - name='subdevice_role', - field=models.NullBooleanField(choices=[(None, b'None'), (True, b'Parent'), (False, b'Child')], default=None, help_text=b'Parent devices house child devices in device bays. Select "None" if this device type is neither a parent nor a child.', verbose_name=b'Parent/child status'), - ), - migrations.AlterUniqueTogether( - name='devicetype', - unique_together=set([('manufacturer', 'slug'), ('manufacturer', 'model')]), - ), - migrations.AlterUniqueTogether( - name='device', - unique_together=set([('rack', 'position', 'face')]), - ), - migrations.AlterUniqueTogether( - name='consoleserverporttemplate', - unique_together=set([('device_type', 'name')]), - ), - migrations.AlterUniqueTogether( - name='consoleserverport', - unique_together=set([('device', 'name')]), - ), - migrations.AlterUniqueTogether( - name='consoleporttemplate', - unique_together=set([('device_type', 'name')]), - ), - migrations.AlterUniqueTogether( - name='consoleport', - unique_together=set([('device', 'name')]), - ), - migrations.CreateModel( - name='DeviceBay', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=50, verbose_name=b'Name')), - ('device', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='device_bays', to='dcim.Device')), - ('installed_device', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='parent_bay', to='dcim.Device')), - ], - options={ - 'ordering': ['device', 'name'], - }, - ), - migrations.CreateModel( - name='DeviceBayTemplate', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=30)), - ('device_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='device_bay_templates', to='dcim.DeviceType')), - ], - options={ - 'ordering': ['device_type', 'name'], - }, - ), - migrations.AlterUniqueTogether( - name='devicebaytemplate', - unique_together=set([('device_type', 'name')]), - ), - migrations.AlterUniqueTogether( - name='devicebay', - unique_together=set([('device', 'name')]), - ), - migrations.AddField( - model_name='device', - name='primary_ip4', - field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='primary_ip4_for', to='ipam.IPAddress', verbose_name=b'Primary IPv4'), - ), - migrations.AddField( - model_name='device', - name='primary_ip6', - field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='primary_ip6_for', to='ipam.IPAddress', verbose_name=b'Primary IPv6'), - ), - migrations.AlterField( - model_name='site', - name='asn', - field=dcim.fields.ASNField(blank=True, null=True, verbose_name=b'ASN'), - ), - migrations.AlterField( - model_name='devicebay', - name='installed_device', - field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='parent_bay', to='dcim.Device'), - ), - migrations.AddField( - model_name='devicetype', - name='part_number', - field=models.CharField(blank=True, help_text=b'Discrete part number (optional)', max_length=50), - ), - migrations.AddField( - model_name='device', - name='tenant', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='devices', to='tenancy.Tenant'), - ), - migrations.AddField( - model_name='rack', - name='tenant', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='racks', to='tenancy.Tenant'), - ), - migrations.AddField( - model_name='site', - name='tenant', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='sites', to='tenancy.Tenant'), - ), - migrations.AddField( - model_name='rack', - name='type', - field=models.PositiveSmallIntegerField(blank=True, choices=[(100, b'2-post frame'), (200, b'4-post frame'), (300, b'4-post cabinet'), (1000, b'Wall-mounted frame'), (1100, b'Wall-mounted cabinet')], null=True, verbose_name=b'Type'), - ), - migrations.AddField( - model_name='rack', - name='width', - field=models.PositiveSmallIntegerField(choices=[(19, b'19 inches'), (23, b'23 inches')], default=19, help_text=b'Rail-to-rail width', verbose_name=b'Width'), - ), - migrations.AlterField( - model_name='rack', - name='u_height', - field=models.PositiveSmallIntegerField(default=42, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)], verbose_name=b'Height (U)'), - ), - migrations.AddField( - model_name='module', - name='manufacturer', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='modules', to='dcim.Manufacturer'), - ), - migrations.CreateModel( - name='RackRole', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=50, unique=True)), - ('slug', models.SlugField(unique=True)), - ('color', utilities.fields.ColorField(max_length=6)), - ], - options={ - 'ordering': ['name'], - }, - ), - migrations.AddField( - model_name='rack', - name='role', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='racks', to='dcim.RackRole'), - ), - migrations.AddField( - model_name='device', - name='asset_tag', - field=utilities.fields.NullableCharField(blank=True, help_text=b'A unique tag used to identify this device', max_length=50, null=True, unique=True, verbose_name=b'Asset tag'), - ), - migrations.AddField( - model_name='rack', - name='desc_units', - field=models.BooleanField(default=False, help_text=b'Units are numbered top-to-bottom', verbose_name=b'Descending units'), - ), - migrations.AlterField( - model_name='device', - name='position', - field=models.PositiveSmallIntegerField(blank=True, help_text=b'The lowest-numbered unit occupied by the device', null=True, validators=[django.core.validators.MinValueValidator(1)], verbose_name=b'Position (U)'), - ), - migrations.AlterField( - model_name='interface', - name='form_factor', - field=models.PositiveSmallIntegerField(choices=[[b'Virtual interfaces', [[0, b'Virtual']]], [b'Ethernet (fixed)', [[800, b'100BASE-TX (10/100ME)'], [1000, b'1000BASE-T (1GE)'], [1150, b'10GBASE-T (10GE)']]], [b'Ethernet (modular)', [[1050, b'GBIC (1GE)'], [1100, b'SFP (1GE)'], [1200, b'SFP+ (10GE)'], [1300, b'XFP (10GE)'], [1310, b'XENPAK (10GE)'], [1320, b'X2 (10GE)'], [1350, b'SFP28 (25GE)'], [1400, b'QSFP+ (40GE)'], [1500, b'CFP (100GE)'], [1600, b'QSFP28 (100GE)']]], [b'FibreChannel', [[3010, b'SFP (1GFC)'], [3020, b'SFP (2GFC)'], [3040, b'SFP (4GFC)'], [3080, b'SFP+ (8GFC)'], [3160, b'SFP+ (16GFC)']]], [b'Serial', [[4000, b'T1 (1.544 Mbps)'], [4010, b'E1 (2.048 Mbps)'], [4040, b'T3 (45 Mbps)'], [4050, b'E3 (34 Mbps)']]], [b'Stacking', [[5000, b'Cisco StackWise'], [5050, b'Cisco StackWise Plus'], [5100, b'Cisco FlexStack'], [5150, b'Cisco FlexStack Plus']]], [b'Other', [[32767, b'Other']]]], default=1200), - ), - migrations.AlterField( - model_name='interfacetemplate', - name='form_factor', - field=models.PositiveSmallIntegerField(choices=[[b'Virtual interfaces', [[0, b'Virtual']]], [b'Ethernet (fixed)', [[800, b'100BASE-TX (10/100ME)'], [1000, b'1000BASE-T (1GE)'], [1150, b'10GBASE-T (10GE)']]], [b'Ethernet (modular)', [[1050, b'GBIC (1GE)'], [1100, b'SFP (1GE)'], [1200, b'SFP+ (10GE)'], [1300, b'XFP (10GE)'], [1310, b'XENPAK (10GE)'], [1320, b'X2 (10GE)'], [1350, b'SFP28 (25GE)'], [1400, b'QSFP+ (40GE)'], [1500, b'CFP (100GE)'], [1600, b'QSFP28 (100GE)']]], [b'FibreChannel', [[3010, b'SFP (1GFC)'], [3020, b'SFP (2GFC)'], [3040, b'SFP (4GFC)'], [3080, b'SFP+ (8GFC)'], [3160, b'SFP+ (16GFC)']]], [b'Serial', [[4000, b'T1 (1.544 Mbps)'], [4010, b'E1 (2.048 Mbps)'], [4040, b'T3 (45 Mbps)'], [4050, b'E3 (34 Mbps)']]], [b'Stacking', [[5000, b'Cisco StackWise'], [5050, b'Cisco StackWise Plus'], [5100, b'Cisco FlexStack'], [5150, b'Cisco FlexStack Plus']]], [b'Other', [[32767, b'Other']]]], default=1200), - ), - migrations.AlterField( - model_name='devicerole', - name='color', - field=utilities.fields.ColorField(max_length=6), - ), - ] diff --git a/netbox/dcim/migrations/0003_auto_20160628_1721_squashed_0010_devicebay_installed_device_set_null.py b/netbox/dcim/migrations/0003_auto_20160628_1721_squashed_0010_devicebay_installed_device_set_null.py new file mode 100644 index 000000000..a9f80f49b --- /dev/null +++ b/netbox/dcim/migrations/0003_auto_20160628_1721_squashed_0010_devicebay_installed_device_set_null.py @@ -0,0 +1,101 @@ +import django.db.models.deletion +from django.db import migrations, models + +import dcim.fields + + +def copy_primary_ip(apps, schema_editor): + Device = apps.get_model('dcim', 'Device') + for d in Device.objects.select_related('primary_ip'): + if not d.primary_ip: + continue + if d.primary_ip.family == 4: + d.primary_ip4 = d.primary_ip + elif d.primary_ip.family == 6: + d.primary_ip6 = d.primary_ip + d.save() + + +class Migration(migrations.Migration): + + replaces = [('dcim', '0003_auto_20160628_1721'), ('dcim', '0004_auto_20160701_2049'), ('dcim', '0005_auto_20160706_1722'), ('dcim', '0006_add_device_primary_ip4_ip6'), ('dcim', '0007_device_copy_primary_ip'), ('dcim', '0008_device_remove_primary_ip'), ('dcim', '0009_site_32bit_asn_support'), ('dcim', '0010_devicebay_installed_device_set_null')] + + dependencies = [ + ('ipam', '0001_initial'), + ('dcim', '0002_auto_20160622_1821'), + ] + + operations = [ + migrations.AlterField( + model_name='interface', + name='form_factor', + field=models.PositiveSmallIntegerField(choices=[[0, b'Virtual'], [800, b'10/100M (100BASE-TX)'], [1000, b'1GE (1000BASE-T)'], [1100, b'1GE (SFP)'], [1150, b'10GE (10GBASE-T)'], [1200, b'10GE (SFP+)'], [1300, b'10GE (XFP)'], [1400, b'40GE (QSFP+)']], default=1200), + ), + migrations.AlterField( + model_name='interfacetemplate', + name='form_factor', + field=models.PositiveSmallIntegerField(choices=[[0, b'Virtual'], [800, b'10/100M (100BASE-TX)'], [1000, b'1GE (1000BASE-T)'], [1100, b'1GE (SFP)'], [1150, b'10GE (10GBASE-T)'], [1200, b'10GE (SFP+)'], [1300, b'10GE (XFP)'], [1400, b'40GE (QSFP+)']], default=1200), + ), + migrations.AddField( + model_name='devicetype', + name='subdevice_role', + field=models.NullBooleanField(choices=[(None, b'None'), (True, b'Parent'), (False, b'Child')], default=None, help_text=b'Parent devices house child devices in device bays. Select "None" if this device type is neither a parent nor a child.', verbose_name=b'Parent/child status'), + ), + migrations.CreateModel( + name='DeviceBayTemplate', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=30)), + ('device_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='device_bay_templates', to='dcim.DeviceType')), + ], + options={ + 'ordering': ['device_type', 'name'], + 'unique_together': {('device_type', 'name')}, + }, + ), + migrations.CreateModel( + name='DeviceBay', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=50, verbose_name=b'Name')), + ('device', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='device_bays', to='dcim.Device')), + ('installed_device', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='parent_bay', to='dcim.Device')), + ], + options={ + 'ordering': ['device', 'name'], + 'unique_together': {('device', 'name')}, + }, + ), + migrations.AddField( + model_name='interface', + name='mac_address', + field=dcim.fields.MACAddressField(blank=True, null=True, verbose_name=b'MAC Address'), + ), + migrations.AddField( + model_name='device', + name='primary_ip4', + field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='primary_ip4_for', to='ipam.IPAddress', verbose_name=b'Primary IPv4'), + ), + migrations.AddField( + model_name='device', + name='primary_ip6', + field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='primary_ip6_for', to='ipam.IPAddress', verbose_name=b'Primary IPv6'), + ), + migrations.RunPython( + code=copy_primary_ip, + ), + migrations.RemoveField( + model_name='device', + name='primary_ip', + ), + migrations.AlterField( + model_name='site', + name='asn', + field=dcim.fields.ASNField(blank=True, null=True, verbose_name=b'ASN'), + ), + migrations.AlterField( + model_name='devicebay', + name='installed_device', + field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='parent_bay', to='dcim.Device'), + ), + ] diff --git a/netbox/dcim/migrations/0011_devicetype_part_number_squashed_0022_color_names_to_rgb.py b/netbox/dcim/migrations/0011_devicetype_part_number_squashed_0022_color_names_to_rgb.py new file mode 100644 index 000000000..dac983398 --- /dev/null +++ b/netbox/dcim/migrations/0011_devicetype_part_number_squashed_0022_color_names_to_rgb.py @@ -0,0 +1,154 @@ +import django.core.validators +import django.db.models.deletion +from django.db import migrations, models + +import utilities.fields + +COLOR_CONVERSION = { + 'teal': '009688', + 'green': '4caf50', + 'blue': '2196f3', + 'purple': '9c27b0', + 'yellow': 'ffeb3b', + 'orange': 'ff9800', + 'red': 'f44336', + 'light_gray': 'c0c0c0', + 'medium_gray': '9e9e9e', + 'dark_gray': '607d8b', +} + + +def color_names_to_rgb(apps, schema_editor): + RackRole = apps.get_model('dcim', 'RackRole') + DeviceRole = apps.get_model('dcim', 'DeviceRole') + for color_name, color_rgb in COLOR_CONVERSION.items(): + RackRole.objects.filter(color=color_name).update(color=color_rgb) + DeviceRole.objects.filter(color=color_name).update(color=color_rgb) + + +class Migration(migrations.Migration): + + replaces = [('dcim', '0011_devicetype_part_number'), ('dcim', '0012_site_rack_device_add_tenant'), ('dcim', '0013_add_interface_form_factors'), ('dcim', '0014_rack_add_type_width'), ('dcim', '0015_rack_add_u_height_validator'), ('dcim', '0016_module_add_manufacturer'), ('dcim', '0017_rack_add_role'), ('dcim', '0018_device_add_asset_tag'), ('dcim', '0019_new_iface_form_factors'), ('dcim', '0020_rack_desc_units'), ('dcim', '0021_add_ff_flexstack'), ('dcim', '0022_color_names_to_rgb')] + + dependencies = [ + ('dcim', '0010_devicebay_installed_device_set_null'), + ('tenancy', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='devicetype', + name='part_number', + field=models.CharField(blank=True, help_text=b'Discrete part number (optional)', max_length=50), + ), + migrations.AddField( + model_name='device', + name='tenant', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='devices', to='tenancy.Tenant'), + ), + migrations.AddField( + model_name='rack', + name='tenant', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='racks', to='tenancy.Tenant'), + ), + migrations.AddField( + model_name='site', + name='tenant', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='sites', to='tenancy.Tenant'), + ), + migrations.AlterField( + model_name='interface', + name='form_factor', + field=models.PositiveSmallIntegerField(choices=[[b'Virtual interfaces', [[0, b'Virtual']]], [b'Ethernet', [[800, b'100BASE-TX (10/100M)'], [1000, b'1000BASE-T (1GE)'], [1150, b'10GBASE-T (10GE)']]], [b'Modular', [[1050, b'GBIC (1GE)'], [1100, b'SFP (1GE)'], [1300, b'XFP (10GE)'], [1200, b'SFP+ (10GE)'], [1400, b'QSFP+ (40GE)'], [1500, b'CFP (100GE)'], [1600, b'QSFP28 (100GE)']]], [b'Serial', [[4000, b'T1 (1.544 Mbps)'], [4010, b'E1 (2.048 Mbps)'], [4040, b'T3 (45 Mbps)'], [4050, b'E3 (34 Mbps)']]], [b'Stacking', [[5000, b'Cisco StackWise'], [5050, b'Cisco StackWise Plus']]]], default=1200), + ), + migrations.AlterField( + model_name='interfacetemplate', + name='form_factor', + field=models.PositiveSmallIntegerField(choices=[[b'Virtual interfaces', [[0, b'Virtual']]], [b'Ethernet', [[800, b'100BASE-TX (10/100M)'], [1000, b'1000BASE-T (1GE)'], [1150, b'10GBASE-T (10GE)']]], [b'Modular', [[1050, b'GBIC (1GE)'], [1100, b'SFP (1GE)'], [1300, b'XFP (10GE)'], [1200, b'SFP+ (10GE)'], [1400, b'QSFP+ (40GE)'], [1500, b'CFP (100GE)'], [1600, b'QSFP28 (100GE)']]], [b'Serial', [[4000, b'T1 (1.544 Mbps)'], [4010, b'E1 (2.048 Mbps)'], [4040, b'T3 (45 Mbps)'], [4050, b'E3 (34 Mbps)']]], [b'Stacking', [[5000, b'Cisco StackWise'], [5050, b'Cisco StackWise Plus']]]], default=1200), + ), + migrations.AddField( + model_name='rack', + name='type', + field=models.PositiveSmallIntegerField(blank=True, choices=[(100, b'2-post frame'), (200, b'4-post frame'), (300, b'4-post cabinet'), (1000, b'Wall-mounted frame'), (1100, b'Wall-mounted cabinet')], null=True, verbose_name=b'Type'), + ), + migrations.AddField( + model_name='rack', + name='width', + field=models.PositiveSmallIntegerField(choices=[(19, b'19 inches'), (23, b'23 inches')], default=19, help_text=b'Rail-to-rail width', verbose_name=b'Width'), + ), + migrations.AlterField( + model_name='rack', + name='u_height', + field=models.PositiveSmallIntegerField(default=42, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)], verbose_name=b'Height (U)'), + ), + migrations.AddField( + model_name='module', + name='manufacturer', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='modules', to='dcim.Manufacturer'), + ), + migrations.CreateModel( + name='RackRole', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=50, unique=True)), + ('slug', models.SlugField(unique=True)), + ('color', models.CharField(choices=[[b'teal', b'Teal'], [b'green', b'Green'], [b'blue', b'Blue'], [b'purple', b'Purple'], [b'yellow', b'Yellow'], [b'orange', b'Orange'], [b'red', b'Red'], [b'light_gray', b'Light Gray'], [b'medium_gray', b'Medium Gray'], [b'dark_gray', b'Dark Gray']], max_length=30)), + ], + options={ + 'ordering': ['name'], + }, + ), + migrations.AddField( + model_name='rack', + name='role', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='racks', to='dcim.RackRole'), + ), + migrations.AddField( + model_name='device', + name='asset_tag', + field=utilities.fields.NullableCharField(blank=True, help_text=b'A unique tag used to identify this device', max_length=50, null=True, unique=True, verbose_name=b'Asset tag'), + ), + migrations.AlterField( + model_name='interface', + name='form_factor', + field=models.PositiveSmallIntegerField(choices=[[b'Virtual interfaces', [[0, b'Virtual']]], [b'Ethernet (fixed)', [[800, b'100BASE-TX (10/100ME)'], [1000, b'1000BASE-T (1GE)'], [1150, b'10GBASE-T (10GE)']]], [b'Ethernet (modular)', [[1050, b'GBIC (1GE)'], [1100, b'SFP (1GE)'], [1200, b'SFP+ (10GE)'], [1300, b'XFP (10GE)'], [1310, b'XENPAK (10GE)'], [1320, b'X2 (10GE)'], [1350, b'SFP28 (25GE)'], [1400, b'QSFP+ (40GE)'], [1500, b'CFP (100GE)'], [1600, b'QSFP28 (100GE)']]], [b'FibreChannel', [[3010, b'SFP (1GFC)'], [3020, b'SFP (2GFC)'], [3040, b'SFP (4GFC)'], [3080, b'SFP+ (8GFC)'], [3160, b'SFP+ (16GFC)']]], [b'Serial', [[4000, b'T1 (1.544 Mbps)'], [4010, b'E1 (2.048 Mbps)'], [4040, b'T3 (45 Mbps)'], [4050, b'E3 (34 Mbps)']]], [b'Stacking', [[5000, b'Cisco StackWise'], [5050, b'Cisco StackWise Plus']]], [b'Other', [[32767, b'Other']]]], default=1200), + ), + migrations.AlterField( + model_name='interfacetemplate', + name='form_factor', + field=models.PositiveSmallIntegerField(choices=[[b'Virtual interfaces', [[0, b'Virtual']]], [b'Ethernet (fixed)', [[800, b'100BASE-TX (10/100ME)'], [1000, b'1000BASE-T (1GE)'], [1150, b'10GBASE-T (10GE)']]], [b'Ethernet (modular)', [[1050, b'GBIC (1GE)'], [1100, b'SFP (1GE)'], [1200, b'SFP+ (10GE)'], [1300, b'XFP (10GE)'], [1310, b'XENPAK (10GE)'], [1320, b'X2 (10GE)'], [1350, b'SFP28 (25GE)'], [1400, b'QSFP+ (40GE)'], [1500, b'CFP (100GE)'], [1600, b'QSFP28 (100GE)']]], [b'FibreChannel', [[3010, b'SFP (1GFC)'], [3020, b'SFP (2GFC)'], [3040, b'SFP (4GFC)'], [3080, b'SFP+ (8GFC)'], [3160, b'SFP+ (16GFC)']]], [b'Serial', [[4000, b'T1 (1.544 Mbps)'], [4010, b'E1 (2.048 Mbps)'], [4040, b'T3 (45 Mbps)'], [4050, b'E3 (34 Mbps)']]], [b'Stacking', [[5000, b'Cisco StackWise'], [5050, b'Cisco StackWise Plus']]], [b'Other', [[32767, b'Other']]]], default=1200), + ), + migrations.AddField( + model_name='rack', + name='desc_units', + field=models.BooleanField(default=False, help_text=b'Units are numbered top-to-bottom', verbose_name=b'Descending units'), + ), + migrations.AlterField( + model_name='device', + name='position', + field=models.PositiveSmallIntegerField(blank=True, help_text=b'The lowest-numbered unit occupied by the device', null=True, validators=[django.core.validators.MinValueValidator(1)], verbose_name=b'Position (U)'), + ), + migrations.AlterField( + model_name='interface', + name='form_factor', + field=models.PositiveSmallIntegerField(choices=[[b'Virtual interfaces', [[0, b'Virtual']]], [b'Ethernet (fixed)', [[800, b'100BASE-TX (10/100ME)'], [1000, b'1000BASE-T (1GE)'], [1150, b'10GBASE-T (10GE)']]], [b'Ethernet (modular)', [[1050, b'GBIC (1GE)'], [1100, b'SFP (1GE)'], [1200, b'SFP+ (10GE)'], [1300, b'XFP (10GE)'], [1310, b'XENPAK (10GE)'], [1320, b'X2 (10GE)'], [1350, b'SFP28 (25GE)'], [1400, b'QSFP+ (40GE)'], [1500, b'CFP (100GE)'], [1600, b'QSFP28 (100GE)']]], [b'FibreChannel', [[3010, b'SFP (1GFC)'], [3020, b'SFP (2GFC)'], [3040, b'SFP (4GFC)'], [3080, b'SFP+ (8GFC)'], [3160, b'SFP+ (16GFC)']]], [b'Serial', [[4000, b'T1 (1.544 Mbps)'], [4010, b'E1 (2.048 Mbps)'], [4040, b'T3 (45 Mbps)'], [4050, b'E3 (34 Mbps)']]], [b'Stacking', [[5000, b'Cisco StackWise'], [5050, b'Cisco StackWise Plus'], [5100, b'Cisco FlexStack'], [5150, b'Cisco FlexStack Plus']]], [b'Other', [[32767, b'Other']]]], default=1200), + ), + migrations.AlterField( + model_name='interfacetemplate', + name='form_factor', + field=models.PositiveSmallIntegerField(choices=[[b'Virtual interfaces', [[0, b'Virtual']]], [b'Ethernet (fixed)', [[800, b'100BASE-TX (10/100ME)'], [1000, b'1000BASE-T (1GE)'], [1150, b'10GBASE-T (10GE)']]], [b'Ethernet (modular)', [[1050, b'GBIC (1GE)'], [1100, b'SFP (1GE)'], [1200, b'SFP+ (10GE)'], [1300, b'XFP (10GE)'], [1310, b'XENPAK (10GE)'], [1320, b'X2 (10GE)'], [1350, b'SFP28 (25GE)'], [1400, b'QSFP+ (40GE)'], [1500, b'CFP (100GE)'], [1600, b'QSFP28 (100GE)']]], [b'FibreChannel', [[3010, b'SFP (1GFC)'], [3020, b'SFP (2GFC)'], [3040, b'SFP (4GFC)'], [3080, b'SFP+ (8GFC)'], [3160, b'SFP+ (16GFC)']]], [b'Serial', [[4000, b'T1 (1.544 Mbps)'], [4010, b'E1 (2.048 Mbps)'], [4040, b'T3 (45 Mbps)'], [4050, b'E3 (34 Mbps)']]], [b'Stacking', [[5000, b'Cisco StackWise'], [5050, b'Cisco StackWise Plus'], [5100, b'Cisco FlexStack'], [5150, b'Cisco FlexStack Plus']]], [b'Other', [[32767, b'Other']]]], default=1200), + ), + migrations.RunPython( + code=color_names_to_rgb, + ), + migrations.AlterField( + model_name='devicerole', + name='color', + field=utilities.fields.ColorField(max_length=6), + ), + migrations.AlterField( + model_name='rackrole', + name='color', + field=utilities.fields.ColorField(max_length=6), + ), + ] diff --git a/netbox/dcim/migrations/0023_devicetype_comments_squashed_0043_device_component_name_lengths.py b/netbox/dcim/migrations/0023_devicetype_comments_squashed_0043_device_component_name_lengths.py index 4d4cfb603..064832e80 100644 --- a/netbox/dcim/migrations/0023_devicetype_comments_squashed_0043_device_component_name_lengths.py +++ b/netbox/dcim/migrations/0023_devicetype_comments_squashed_0043_device_component_name_lengths.py @@ -1,12 +1,11 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11.14 on 2018-07-31 02:13 -import dcim.fields -from django.conf import settings import django.contrib.postgres.fields import django.core.validators -from django.db import migrations, models import django.db.models.deletion import mptt.fields +from django.conf import settings +from django.db import migrations, models + +import dcim.fields import utilities.fields @@ -32,8 +31,8 @@ class Migration(migrations.Migration): replaces = [('dcim', '0023_devicetype_comments'), ('dcim', '0024_site_add_contact_fields'), ('dcim', '0025_devicetype_add_interface_ordering'), ('dcim', '0026_add_rack_reservations'), ('dcim', '0027_device_add_site'), ('dcim', '0028_device_copy_rack_to_site'), ('dcim', '0029_allow_rackless_devices'), ('dcim', '0030_interface_add_lag'), ('dcim', '0031_regions'), ('dcim', '0032_device_increase_name_length'), ('dcim', '0033_rackreservation_rack_editable'), ('dcim', '0034_rename_module_to_inventoryitem'), ('dcim', '0035_device_expand_status_choices'), ('dcim', '0036_add_ff_juniper_vcp'), ('dcim', '0037_unicode_literals'), ('dcim', '0038_wireless_interfaces'), ('dcim', '0039_interface_add_enabled_mtu'), ('dcim', '0040_inventoryitem_add_asset_tag_description'), ('dcim', '0041_napalm_integration'), ('dcim', '0042_interface_ff_10ge_cx4'), ('dcim', '0043_device_component_name_lengths')] dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), ('dcim', '0022_color_names_to_rgb'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ @@ -94,10 +93,15 @@ class Migration(migrations.Migration): name='site', field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='devices', to='dcim.Site'), ), - migrations.AddField( + migrations.AlterField( model_name='interface', - name='lag', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='member_interfaces', to='dcim.Interface', verbose_name='Parent LAG'), + name='form_factor', + field=models.PositiveSmallIntegerField(choices=[[b'Virtual interfaces', [[0, b'Virtual'], [200, b'Link Aggregation Group (LAG)']]], [b'Ethernet (fixed)', [[800, b'100BASE-TX (10/100ME)'], [1000, b'1000BASE-T (1GE)'], [1150, b'10GBASE-T (10GE)']]], [b'Ethernet (modular)', [[1050, b'GBIC (1GE)'], [1100, b'SFP (1GE)'], [1200, b'SFP+ (10GE)'], [1300, b'XFP (10GE)'], [1310, b'XENPAK (10GE)'], [1320, b'X2 (10GE)'], [1350, b'SFP28 (25GE)'], [1400, b'QSFP+ (40GE)'], [1500, b'CFP (100GE)'], [1600, b'QSFP28 (100GE)']]], [b'FibreChannel', [[3010, b'SFP (1GFC)'], [3020, b'SFP (2GFC)'], [3040, b'SFP (4GFC)'], [3080, b'SFP+ (8GFC)'], [3160, b'SFP+ (16GFC)']]], [b'Serial', [[4000, b'T1 (1.544 Mbps)'], [4010, b'E1 (2.048 Mbps)'], [4040, b'T3 (45 Mbps)'], [4050, b'E3 (34 Mbps)'], [4050, b'E3 (34 Mbps)']]], [b'Stacking', [[5000, b'Cisco StackWise'], [5050, b'Cisco StackWise Plus'], [5100, b'Cisco FlexStack'], [5150, b'Cisco FlexStack Plus']]], [b'Other', [[32767, b'Other']]]], default=1200), + ), + migrations.AlterField( + model_name='interfacetemplate', + name='form_factor', + field=models.PositiveSmallIntegerField(choices=[[b'Virtual interfaces', [[0, b'Virtual'], [200, b'Link Aggregation Group (LAG)']]], [b'Ethernet (fixed)', [[800, b'100BASE-TX (10/100ME)'], [1000, b'1000BASE-T (1GE)'], [1150, b'10GBASE-T (10GE)']]], [b'Ethernet (modular)', [[1050, b'GBIC (1GE)'], [1100, b'SFP (1GE)'], [1200, b'SFP+ (10GE)'], [1300, b'XFP (10GE)'], [1310, b'XENPAK (10GE)'], [1320, b'X2 (10GE)'], [1350, b'SFP28 (25GE)'], [1400, b'QSFP+ (40GE)'], [1500, b'CFP (100GE)'], [1600, b'QSFP28 (100GE)']]], [b'FibreChannel', [[3010, b'SFP (1GFC)'], [3020, b'SFP (2GFC)'], [3040, b'SFP (4GFC)'], [3080, b'SFP+ (8GFC)'], [3160, b'SFP+ (16GFC)']]], [b'Serial', [[4000, b'T1 (1.544 Mbps)'], [4010, b'E1 (2.048 Mbps)'], [4040, b'T3 (45 Mbps)'], [4050, b'E3 (34 Mbps)'], [4050, b'E3 (34 Mbps)']]], [b'Stacking', [[5000, b'Cisco StackWise'], [5050, b'Cisco StackWise Plus'], [5100, b'Cisco FlexStack'], [5150, b'Cisco FlexStack Plus']]], [b'Other', [[32767, b'Other']]]], default=1200), ), migrations.CreateModel( name='Region', @@ -157,7 +161,17 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='device', name='status', - field=models.PositiveSmallIntegerField(choices=[[1, 'Active'], [0, 'Offline'], [2, 'Planned'], [3, 'Staged'], [4, 'Failed'], [5, 'Inventory']], default=1, verbose_name='Status'), + field=models.PositiveSmallIntegerField(choices=[[1, b'Active'], [0, b'Offline'], [2, b'Planned'], [3, b'Staged'], [4, b'Failed'], [5, b'Inventory']], default=1, verbose_name=b'Status'), + ), + migrations.AlterField( + model_name='interface', + name='form_factor', + field=models.PositiveSmallIntegerField(choices=[[b'Virtual interfaces', [[0, b'Virtual'], [200, b'Link Aggregation Group (LAG)']]], [b'Ethernet (fixed)', [[800, b'100BASE-TX (10/100ME)'], [1000, b'1000BASE-T (1GE)'], [1150, b'10GBASE-T (10GE)']]], [b'Ethernet (modular)', [[1050, b'GBIC (1GE)'], [1100, b'SFP (1GE)'], [1200, b'SFP+ (10GE)'], [1300, b'XFP (10GE)'], [1310, b'XENPAK (10GE)'], [1320, b'X2 (10GE)'], [1350, b'SFP28 (25GE)'], [1400, b'QSFP+ (40GE)'], [1500, b'CFP (100GE)'], [1600, b'QSFP28 (100GE)']]], [b'FibreChannel', [[3010, b'SFP (1GFC)'], [3020, b'SFP (2GFC)'], [3040, b'SFP (4GFC)'], [3080, b'SFP+ (8GFC)'], [3160, b'SFP+ (16GFC)']]], [b'Serial', [[4000, b'T1 (1.544 Mbps)'], [4010, b'E1 (2.048 Mbps)'], [4040, b'T3 (45 Mbps)'], [4050, b'E3 (34 Mbps)'], [4050, b'E3 (34 Mbps)']]], [b'Stacking', [[5000, b'Cisco StackWise'], [5050, b'Cisco StackWise Plus'], [5100, b'Cisco FlexStack'], [5150, b'Cisco FlexStack Plus'], [5200, b'Juniper VCP']]], [b'Other', [[32767, b'Other']]]], default=1200), + ), + migrations.AlterField( + model_name='interfacetemplate', + name='form_factor', + field=models.PositiveSmallIntegerField(choices=[[b'Virtual interfaces', [[0, b'Virtual'], [200, b'Link Aggregation Group (LAG)']]], [b'Ethernet (fixed)', [[800, b'100BASE-TX (10/100ME)'], [1000, b'1000BASE-T (1GE)'], [1150, b'10GBASE-T (10GE)']]], [b'Ethernet (modular)', [[1050, b'GBIC (1GE)'], [1100, b'SFP (1GE)'], [1200, b'SFP+ (10GE)'], [1300, b'XFP (10GE)'], [1310, b'XENPAK (10GE)'], [1320, b'X2 (10GE)'], [1350, b'SFP28 (25GE)'], [1400, b'QSFP+ (40GE)'], [1500, b'CFP (100GE)'], [1600, b'QSFP28 (100GE)']]], [b'FibreChannel', [[3010, b'SFP (1GFC)'], [3020, b'SFP (2GFC)'], [3040, b'SFP (4GFC)'], [3080, b'SFP+ (8GFC)'], [3160, b'SFP+ (16GFC)']]], [b'Serial', [[4000, b'T1 (1.544 Mbps)'], [4010, b'E1 (2.048 Mbps)'], [4040, b'T3 (45 Mbps)'], [4050, b'E3 (34 Mbps)'], [4050, b'E3 (34 Mbps)']]], [b'Stacking', [[5000, b'Cisco StackWise'], [5050, b'Cisco StackWise Plus'], [5100, b'Cisco FlexStack'], [5150, b'Cisco FlexStack Plus'], [5200, b'Juniper VCP']]], [b'Other', [[32767, b'Other']]]], default=1200), ), migrations.AlterField( model_name='consoleport', @@ -199,6 +213,11 @@ class Migration(migrations.Migration): name='serial', field=models.CharField(blank=True, max_length=50, verbose_name='Serial number'), ), + migrations.AlterField( + model_name='device', + name='status', + field=models.PositiveSmallIntegerField(choices=[[1, 'Active'], [0, 'Offline'], [2, 'Planned'], [3, 'Staged'], [4, 'Failed'], [5, 'Inventory']], default=1, verbose_name='Status'), + ), migrations.AlterField( model_name='devicebay', name='name', @@ -244,6 +263,16 @@ class Migration(migrations.Migration): name='u_height', field=models.PositiveSmallIntegerField(default=1, verbose_name='Height (U)'), ), + migrations.AlterField( + model_name='interface', + name='form_factor', + field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1600, 'QSFP28 (100GE)']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[5000, 'Cisco StackWise'], [5050, 'Cisco StackWise Plus'], [5100, 'Cisco FlexStack'], [5150, 'Cisco FlexStack Plus'], [5200, 'Juniper VCP']]], ['Other', [[32767, 'Other']]]], default=1200), + ), + migrations.AddField( + model_name='interface', + name='lag', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='member_interfaces', to='dcim.Interface', verbose_name='Parent LAG'), + ), migrations.AlterField( model_name='interface', name='mac_address', @@ -259,6 +288,11 @@ class Migration(migrations.Migration): name='connection_status', field=models.BooleanField(choices=[[False, 'Planned'], [True, 'Connected']], default=True, verbose_name='Status'), ), + migrations.AlterField( + model_name='interfacetemplate', + name='form_factor', + field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1600, 'QSFP28 (100GE)']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[5000, 'Cisco StackWise'], [5050, 'Cisco StackWise Plus'], [5100, 'Cisco FlexStack'], [5150, 'Cisco FlexStack Plus'], [5200, 'Juniper VCP']]], ['Other', [[32767, 'Other']]]], default=1200), + ), migrations.AlterField( model_name='interfacetemplate', name='mgmt_only', @@ -329,6 +363,16 @@ class Migration(migrations.Migration): name='contact_email', field=models.EmailField(blank=True, max_length=254, verbose_name='Contact E-mail'), ), + migrations.AlterField( + model_name='interface', + name='form_factor', + field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1600, 'QSFP28 (100GE)']]], ['Wireless', [[2600, 'IEEE 802.11a'], [2610, 'IEEE 802.11b/g'], [2620, 'IEEE 802.11n'], [2630, 'IEEE 802.11ac'], [2640, 'IEEE 802.11ad']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[5000, 'Cisco StackWise'], [5050, 'Cisco StackWise Plus'], [5100, 'Cisco FlexStack'], [5150, 'Cisco FlexStack Plus'], [5200, 'Juniper VCP']]], ['Other', [[32767, 'Other']]]], default=1200), + ), + migrations.AlterField( + model_name='interfacetemplate', + name='form_factor', + field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1600, 'QSFP28 (100GE)']]], ['Wireless', [[2600, 'IEEE 802.11a'], [2610, 'IEEE 802.11b/g'], [2620, 'IEEE 802.11n'], [2630, 'IEEE 802.11ac'], [2640, 'IEEE 802.11ad']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[5000, 'Cisco StackWise'], [5050, 'Cisco StackWise Plus'], [5100, 'Cisco FlexStack'], [5150, 'Cisco FlexStack Plus'], [5200, 'Juniper VCP']]], ['Other', [[32767, 'Other']]]], default=1200), + ), migrations.AddField( model_name='interface', name='enabled', diff --git a/netbox/dcim/migrations/0044_virtualization_squashed_0055_virtualchassis_ordering.py b/netbox/dcim/migrations/0044_virtualization_squashed_0055_virtualchassis_ordering.py deleted file mode 100644 index 78b4e3a41..000000000 --- a/netbox/dcim/migrations/0044_virtualization_squashed_0055_virtualchassis_ordering.py +++ /dev/null @@ -1,144 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11.14 on 2018-07-31 02:17 -from django.conf import settings -import django.core.validators -from django.db import migrations, models -import django.db.models.deletion -import timezone_field.fields -import utilities.fields - - -class Migration(migrations.Migration): - - replaces = [('dcim', '0044_virtualization'), ('dcim', '0045_devicerole_vm_role'), ('dcim', '0046_rack_lengthen_facility_id'), ('dcim', '0047_more_100ge_form_factors'), ('dcim', '0048_rack_serial'), ('dcim', '0049_rackreservation_change_user'), ('dcim', '0050_interface_vlan_tagging'), ('dcim', '0051_rackreservation_tenant'), ('dcim', '0052_virtual_chassis'), ('dcim', '0053_platform_manufacturer'), ('dcim', '0054_site_status_timezone_description'), ('dcim', '0055_virtualchassis_ordering')] - - dependencies = [ - ('dcim', '0043_device_component_name_lengths'), - ('ipam', '0020_ipaddress_add_role_carp'), - ('virtualization', '0001_virtualization'), - ('tenancy', '0003_unicode_literals'), - ] - - operations = [ - migrations.AddField( - model_name='device', - name='cluster', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='devices', to='virtualization.Cluster'), - ), - migrations.AddField( - model_name='interface', - name='virtual_machine', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='interfaces', to='virtualization.VirtualMachine'), - ), - migrations.AlterField( - model_name='interface', - name='device', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='interfaces', to='dcim.Device'), - ), - migrations.AddField( - model_name='devicerole', - name='vm_role', - field=models.BooleanField(default=True, help_text='Virtual machines may be assigned to this role', verbose_name='VM Role'), - ), - migrations.AlterField( - model_name='rack', - name='facility_id', - field=utilities.fields.NullableCharField(blank=True, max_length=50, null=True, verbose_name='Facility ID'), - ), - migrations.AlterField( - model_name='interface', - name='form_factor', - field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)'], [1170, '10GBASE-CX4 (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1510, 'CFP2 (100GE)'], [1520, 'CFP4 (100GE)'], [1550, 'Cisco CPAK (100GE)'], [1600, 'QSFP28 (100GE)']]], ['Wireless', [[2600, 'IEEE 802.11a'], [2610, 'IEEE 802.11b/g'], [2620, 'IEEE 802.11n'], [2630, 'IEEE 802.11ac'], [2640, 'IEEE 802.11ad']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[5000, 'Cisco StackWise'], [5050, 'Cisco StackWise Plus'], [5100, 'Cisco FlexStack'], [5150, 'Cisco FlexStack Plus'], [5200, 'Juniper VCP']]], ['Other', [[32767, 'Other']]]], default=1200), - ), - migrations.AlterField( - model_name='interfacetemplate', - name='form_factor', - field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)'], [1170, '10GBASE-CX4 (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1510, 'CFP2 (100GE)'], [1520, 'CFP4 (100GE)'], [1550, 'Cisco CPAK (100GE)'], [1600, 'QSFP28 (100GE)']]], ['Wireless', [[2600, 'IEEE 802.11a'], [2610, 'IEEE 802.11b/g'], [2620, 'IEEE 802.11n'], [2630, 'IEEE 802.11ac'], [2640, 'IEEE 802.11ad']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[5000, 'Cisco StackWise'], [5050, 'Cisco StackWise Plus'], [5100, 'Cisco FlexStack'], [5150, 'Cisco FlexStack Plus'], [5200, 'Juniper VCP']]], ['Other', [[32767, 'Other']]]], default=1200), - ), - migrations.AddField( - model_name='rack', - name='serial', - field=models.CharField(blank=True, max_length=50, verbose_name='Serial number'), - ), - migrations.AlterField( - model_name='rackreservation', - name='user', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL), - ), - migrations.AddField( - model_name='interface', - name='mode', - field=models.PositiveSmallIntegerField(blank=True, choices=[[100, 'Access'], [200, 'Tagged'], [300, 'Tagged All']], null=True), - ), - migrations.AddField( - model_name='interface', - name='tagged_vlans', - field=models.ManyToManyField(blank=True, related_name='interfaces_as_tagged', to='ipam.VLAN', verbose_name='Tagged VLANs'), - ), - migrations.AddField( - model_name='interface', - name='untagged_vlan', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='interfaces_as_untagged', to='ipam.VLAN', verbose_name='Untagged VLAN'), - ), - migrations.AddField( - model_name='rackreservation', - name='tenant', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='rackreservations', to='tenancy.Tenant'), - ), - migrations.CreateModel( - name='VirtualChassis', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('domain', models.CharField(blank=True, max_length=30)), - ('master', models.OneToOneField(on_delete=django.db.models.deletion.PROTECT, related_name='vc_master_for', to='dcim.Device')), - ], - options={ - 'verbose_name_plural': 'virtual chassis', - 'ordering': ['master'], - }, - ), - migrations.AddField( - model_name='device', - name='virtual_chassis', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='members', to='dcim.VirtualChassis'), - ), - migrations.AddField( - model_name='device', - name='vc_position', - field=models.PositiveSmallIntegerField(blank=True, null=True, validators=[django.core.validators.MaxValueValidator(255)]), - ), - migrations.AddField( - model_name='device', - name='vc_priority', - field=models.PositiveSmallIntegerField(blank=True, null=True, validators=[django.core.validators.MaxValueValidator(255)]), - ), - migrations.AlterUniqueTogether( - name='device', - unique_together=set([('rack', 'position', 'face'), ('virtual_chassis', 'vc_position')]), - ), - migrations.AddField( - model_name='platform', - name='manufacturer', - field=models.ForeignKey(blank=True, help_text='Optionally limit this platform to devices of a certain manufacturer', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='platforms', to='dcim.Manufacturer'), - ), - migrations.AlterField( - model_name='platform', - name='napalm_driver', - field=models.CharField(blank=True, help_text='The name of the NAPALM driver to use when interacting with devices', max_length=50, verbose_name='NAPALM driver'), - ), - migrations.AddField( - model_name='site', - name='description', - field=models.CharField(blank=True, max_length=100), - ), - migrations.AddField( - model_name='site', - name='status', - field=models.PositiveSmallIntegerField(choices=[[1, 'Active'], [2, 'Planned'], [4, 'Retired']], default=1), - ), - migrations.AddField( - model_name='site', - name='time_zone', - field=timezone_field.fields.TimeZoneField(blank=True), - ), - ] diff --git a/netbox/dcim/migrations/0044_virtualization_squashed_0061_platform_napalm_args.py b/netbox/dcim/migrations/0044_virtualization_squashed_0061_platform_napalm_args.py new file mode 100644 index 000000000..18ef39fe7 --- /dev/null +++ b/netbox/dcim/migrations/0044_virtualization_squashed_0061_platform_napalm_args.py @@ -0,0 +1,354 @@ +import django.contrib.postgres.fields.jsonb +import django.core.validators +import django.db.models.deletion +import taggit.managers +import timezone_field.fields +from django.conf import settings +from django.db import migrations, models + +import utilities.fields + + +class Migration(migrations.Migration): + + replaces = [('dcim', '0044_virtualization'), ('dcim', '0045_devicerole_vm_role'), ('dcim', '0046_rack_lengthen_facility_id'), ('dcim', '0047_more_100ge_form_factors'), ('dcim', '0048_rack_serial'), ('dcim', '0049_rackreservation_change_user'), ('dcim', '0050_interface_vlan_tagging'), ('dcim', '0051_rackreservation_tenant'), ('dcim', '0052_virtual_chassis'), ('dcim', '0053_platform_manufacturer'), ('dcim', '0054_site_status_timezone_description'), ('dcim', '0055_virtualchassis_ordering'), ('dcim', '0056_django2'), ('dcim', '0057_tags'), ('dcim', '0058_relax_rack_naming_constraints'), ('dcim', '0059_site_latitude_longitude'), ('dcim', '0060_change_logging'), ('dcim', '0061_platform_napalm_args')] + + dependencies = [ + ('virtualization', '0001_virtualization'), + ('tenancy', '0003_unicode_literals'), + ('ipam', '0020_ipaddress_add_role_carp'), + ('dcim', '0043_device_component_name_lengths'), + ('taggit', '0002_auto_20150616_2121'), + ] + + operations = [ + migrations.AddField( + model_name='device', + name='cluster', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='devices', to='virtualization.Cluster'), + ), + migrations.AddField( + model_name='interface', + name='virtual_machine', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='interfaces', to='virtualization.VirtualMachine'), + ), + migrations.AlterField( + model_name='interface', + name='device', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='interfaces', to='dcim.Device'), + ), + migrations.AddField( + model_name='devicerole', + name='vm_role', + field=models.BooleanField(default=True, help_text='Virtual machines may be assigned to this role', verbose_name='VM Role'), + ), + migrations.AlterField( + model_name='rack', + name='facility_id', + field=utilities.fields.NullableCharField(blank=True, max_length=50, null=True, verbose_name='Facility ID'), + ), + migrations.AlterField( + model_name='interface', + name='form_factor', + field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)'], [1170, '10GBASE-CX4 (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1510, 'CFP2 (100GE)'], [1520, 'CFP4 (100GE)'], [1550, 'Cisco CPAK (100GE)'], [1600, 'QSFP28 (100GE)']]], ['Wireless', [[2600, 'IEEE 802.11a'], [2610, 'IEEE 802.11b/g'], [2620, 'IEEE 802.11n'], [2630, 'IEEE 802.11ac'], [2640, 'IEEE 802.11ad']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[5000, 'Cisco StackWise'], [5050, 'Cisco StackWise Plus'], [5100, 'Cisco FlexStack'], [5150, 'Cisco FlexStack Plus'], [5200, 'Juniper VCP']]], ['Other', [[32767, 'Other']]]], default=1200), + ), + migrations.AlterField( + model_name='interfacetemplate', + name='form_factor', + field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)'], [1170, '10GBASE-CX4 (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1510, 'CFP2 (100GE)'], [1520, 'CFP4 (100GE)'], [1550, 'Cisco CPAK (100GE)'], [1600, 'QSFP28 (100GE)']]], ['Wireless', [[2600, 'IEEE 802.11a'], [2610, 'IEEE 802.11b/g'], [2620, 'IEEE 802.11n'], [2630, 'IEEE 802.11ac'], [2640, 'IEEE 802.11ad']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[5000, 'Cisco StackWise'], [5050, 'Cisco StackWise Plus'], [5100, 'Cisco FlexStack'], [5150, 'Cisco FlexStack Plus'], [5200, 'Juniper VCP']]], ['Other', [[32767, 'Other']]]], default=1200), + ), + migrations.AddField( + model_name='rack', + name='serial', + field=models.CharField(blank=True, max_length=50, verbose_name='Serial number'), + ), + migrations.AlterField( + model_name='rackreservation', + name='user', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='interface', + name='mode', + field=models.PositiveSmallIntegerField(blank=True, choices=[[100, 'Access'], [200, 'Tagged'], [300, 'Tagged All']], null=True), + ), + migrations.AddField( + model_name='interface', + name='tagged_vlans', + field=models.ManyToManyField(blank=True, related_name='interfaces_as_tagged', to='ipam.VLAN', verbose_name='Tagged VLANs'), + ), + migrations.AddField( + model_name='rackreservation', + name='tenant', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='rackreservations', to='tenancy.Tenant'), + ), + migrations.CreateModel( + name='VirtualChassis', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('domain', models.CharField(blank=True, max_length=30)), + ('master', models.OneToOneField(default=1, on_delete=django.db.models.deletion.PROTECT, related_name='vc_master_for', to='dcim.Device')), + ], + options={ + 'ordering': ['master'], + 'verbose_name_plural': 'virtual chassis', + }, + ), + migrations.AddField( + model_name='device', + name='virtual_chassis', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='members', to='dcim.VirtualChassis'), + ), + migrations.AddField( + model_name='device', + name='vc_position', + field=models.PositiveSmallIntegerField(blank=True, null=True, validators=[django.core.validators.MaxValueValidator(255)]), + ), + migrations.AddField( + model_name='device', + name='vc_priority', + field=models.PositiveSmallIntegerField(blank=True, null=True, validators=[django.core.validators.MaxValueValidator(255)]), + ), + migrations.AlterUniqueTogether( + name='device', + unique_together={('rack', 'position', 'face'), ('virtual_chassis', 'vc_position')}, + ), + migrations.AlterField( + model_name='platform', + name='napalm_driver', + field=models.CharField(blank=True, help_text='The name of the NAPALM driver to use when interacting with devices', max_length=50, verbose_name='NAPALM driver'), + ), + migrations.AddField( + model_name='site', + name='description', + field=models.CharField(blank=True, max_length=100), + ), + migrations.AddField( + model_name='site', + name='status', + field=models.PositiveSmallIntegerField(choices=[[1, 'Active'], [2, 'Planned'], [4, 'Retired']], default=1), + ), + migrations.AddField( + model_name='site', + name='time_zone', + field=timezone_field.fields.TimeZoneField(blank=True), + ), + migrations.AlterField( + model_name='virtualchassis', + name='master', + field=models.OneToOneField(on_delete=django.db.models.deletion.PROTECT, related_name='vc_master_for', to='dcim.Device'), + ), + migrations.AddField( + model_name='interface', + name='untagged_vlan', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='interfaces_as_untagged', to='ipam.VLAN', verbose_name='Untagged VLAN'), + ), + migrations.AddField( + model_name='platform', + name='manufacturer', + field=models.ForeignKey(blank=True, help_text='Optionally limit this platform to devices of a certain manufacturer', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='platforms', to='dcim.Manufacturer'), + ), + migrations.AddField( + model_name='device', + name='tags', + field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'), + ), + migrations.AddField( + model_name='devicetype', + name='tags', + field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'), + ), + migrations.AddField( + model_name='rack', + name='tags', + field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'), + ), + migrations.AddField( + model_name='site', + name='tags', + field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'), + ), + migrations.AddField( + model_name='consoleport', + name='tags', + field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'), + ), + migrations.AddField( + model_name='consoleserverport', + name='tags', + field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'), + ), + migrations.AddField( + model_name='devicebay', + name='tags', + field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'), + ), + migrations.AddField( + model_name='interface', + name='tags', + field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'), + ), + migrations.AddField( + model_name='inventoryitem', + name='tags', + field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'), + ), + migrations.AddField( + model_name='poweroutlet', + name='tags', + field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'), + ), + migrations.AddField( + model_name='powerport', + name='tags', + field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'), + ), + migrations.AddField( + model_name='virtualchassis', + name='tags', + field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'), + ), + migrations.AlterModelOptions( + name='rack', + options={'ordering': ['site', 'group', 'name']}, + ), + migrations.AlterUniqueTogether( + name='rack', + unique_together={('group', 'name'), ('group', 'facility_id')}, + ), + migrations.AddField( + model_name='site', + name='latitude', + field=models.DecimalField(blank=True, decimal_places=6, max_digits=8, null=True), + ), + migrations.AddField( + model_name='site', + name='longitude', + field=models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True), + ), + migrations.AddField( + model_name='devicerole', + name='created', + field=models.DateField(auto_now_add=True, null=True), + ), + migrations.AddField( + model_name='devicerole', + name='last_updated', + field=models.DateTimeField(auto_now=True, null=True), + ), + migrations.AddField( + model_name='devicetype', + name='created', + field=models.DateField(auto_now_add=True, null=True), + ), + migrations.AddField( + model_name='devicetype', + name='last_updated', + field=models.DateTimeField(auto_now=True, null=True), + ), + migrations.AddField( + model_name='manufacturer', + name='created', + field=models.DateField(auto_now_add=True, null=True), + ), + migrations.AddField( + model_name='manufacturer', + name='last_updated', + field=models.DateTimeField(auto_now=True, null=True), + ), + migrations.AddField( + model_name='platform', + name='created', + field=models.DateField(auto_now_add=True, null=True), + ), + migrations.AddField( + model_name='platform', + name='last_updated', + field=models.DateTimeField(auto_now=True, null=True), + ), + migrations.AddField( + model_name='rackgroup', + name='created', + field=models.DateField(auto_now_add=True, null=True), + ), + migrations.AddField( + model_name='rackgroup', + name='last_updated', + field=models.DateTimeField(auto_now=True, null=True), + ), + migrations.AddField( + model_name='rackreservation', + name='last_updated', + field=models.DateTimeField(auto_now=True, null=True), + ), + migrations.AddField( + model_name='rackrole', + name='created', + field=models.DateField(auto_now_add=True, null=True), + ), + migrations.AddField( + model_name='rackrole', + name='last_updated', + field=models.DateTimeField(auto_now=True, null=True), + ), + migrations.AddField( + model_name='region', + name='created', + field=models.DateField(auto_now_add=True, null=True), + ), + migrations.AddField( + model_name='region', + name='last_updated', + field=models.DateTimeField(auto_now=True, null=True), + ), + migrations.AddField( + model_name='virtualchassis', + name='created', + field=models.DateField(auto_now_add=True, null=True), + ), + migrations.AddField( + model_name='virtualchassis', + name='last_updated', + field=models.DateTimeField(auto_now=True, null=True), + ), + migrations.AlterField( + model_name='device', + name='created', + field=models.DateField(auto_now_add=True, null=True), + ), + migrations.AlterField( + model_name='device', + name='last_updated', + field=models.DateTimeField(auto_now=True, null=True), + ), + migrations.AlterField( + model_name='rack', + name='created', + field=models.DateField(auto_now_add=True, null=True), + ), + migrations.AlterField( + model_name='rack', + name='last_updated', + field=models.DateTimeField(auto_now=True, null=True), + ), + migrations.AlterField( + model_name='rackreservation', + name='created', + field=models.DateField(auto_now_add=True, null=True), + ), + migrations.AlterField( + model_name='site', + name='created', + field=models.DateField(auto_now_add=True, null=True), + ), + migrations.AlterField( + model_name='site', + name='last_updated', + field=models.DateTimeField(auto_now=True, null=True), + ), + migrations.AddField( + model_name='platform', + name='napalm_args', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Additional arguments to pass when initiating the NAPALM driver (JSON format)', null=True, verbose_name='NAPALM arguments'), + ), + ] diff --git a/netbox/dcim/migrations/0062_interface_mtu_squashed_0065_front_rear_ports.py b/netbox/dcim/migrations/0062_interface_mtu_squashed_0065_front_rear_ports.py new file mode 100644 index 000000000..71ce4191f --- /dev/null +++ b/netbox/dcim/migrations/0062_interface_mtu_squashed_0065_front_rear_ports.py @@ -0,0 +1,124 @@ +import django.contrib.postgres.fields.jsonb +import django.core.validators +import django.db.models.deletion +import taggit.managers +from django.db import migrations, models + + +class Migration(migrations.Migration): + + replaces = [('dcim', '0062_interface_mtu'), ('dcim', '0063_device_local_context_data'), ('dcim', '0064_remove_platform_rpc_client'), ('dcim', '0065_front_rear_ports')] + + dependencies = [ + ('taggit', '0002_auto_20150616_2121'), + ('dcim', '0061_platform_napalm_args'), + ] + + operations = [ + migrations.AlterField( + model_name='interface', + name='mtu', + field=models.PositiveIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(65536)], verbose_name='MTU'), + ), + migrations.AlterField( + model_name='interface', + name='form_factor', + field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)'], [1170, '10GBASE-CX4 (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1510, 'CFP2 (100GE)'], [1520, 'CFP4 (100GE)'], [1550, 'Cisco CPAK (100GE)'], [1600, 'QSFP28 (100GE)']]], ['Wireless', [[2600, 'IEEE 802.11a'], [2610, 'IEEE 802.11b/g'], [2620, 'IEEE 802.11n'], [2630, 'IEEE 802.11ac'], [2640, 'IEEE 802.11ad']]], ['SONET', [[6100, 'OC-3/STM-1'], [6200, 'OC-12/STM-4'], [6300, 'OC-48/STM-16'], [6400, 'OC-192/STM-64'], [6500, 'OC-768/STM-256'], [6600, 'OC-1920/STM-640'], [6700, 'OC-3840/STM-1234']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)'], [3320, 'SFP28 (32GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[5000, 'Cisco StackWise'], [5050, 'Cisco StackWise Plus'], [5100, 'Cisco FlexStack'], [5150, 'Cisco FlexStack Plus'], [5200, 'Juniper VCP'], [5300, 'Extreme SummitStack'], [5310, 'Extreme SummitStack-128'], [5320, 'Extreme SummitStack-256'], [5330, 'Extreme SummitStack-512']]], ['Other', [[32767, 'Other']]]], default=1200), + ), + migrations.AlterField( + model_name='interfacetemplate', + name='form_factor', + field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)'], [1170, '10GBASE-CX4 (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1510, 'CFP2 (100GE)'], [1520, 'CFP4 (100GE)'], [1550, 'Cisco CPAK (100GE)'], [1600, 'QSFP28 (100GE)']]], ['Wireless', [[2600, 'IEEE 802.11a'], [2610, 'IEEE 802.11b/g'], [2620, 'IEEE 802.11n'], [2630, 'IEEE 802.11ac'], [2640, 'IEEE 802.11ad']]], ['SONET', [[6100, 'OC-3/STM-1'], [6200, 'OC-12/STM-4'], [6300, 'OC-48/STM-16'], [6400, 'OC-192/STM-64'], [6500, 'OC-768/STM-256'], [6600, 'OC-1920/STM-640'], [6700, 'OC-3840/STM-1234']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)'], [3320, 'SFP28 (32GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[5000, 'Cisco StackWise'], [5050, 'Cisco StackWise Plus'], [5100, 'Cisco FlexStack'], [5150, 'Cisco FlexStack Plus'], [5200, 'Juniper VCP'], [5300, 'Extreme SummitStack'], [5310, 'Extreme SummitStack-128'], [5320, 'Extreme SummitStack-256'], [5330, 'Extreme SummitStack-512']]], ['Other', [[32767, 'Other']]]], default=1200), + ), + migrations.AddField( + model_name='device', + name='local_context_data', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True), + ), + migrations.RemoveField( + model_name='platform', + name='rpc_client', + ), + migrations.CreateModel( + name='RearPort', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)), + ('name', models.CharField(max_length=64)), + ('type', models.PositiveSmallIntegerField()), + ('positions', models.PositiveSmallIntegerField(default=1, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(64)])), + ('description', models.CharField(blank=True, max_length=100)), + ('device', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='rearports', to='dcim.Device')), + ('tags', taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags')), + ], + options={ + 'ordering': ['device', 'name'], + 'unique_together': {('device', 'name')}, + }, + ), + migrations.CreateModel( + name='RearPortTemplate', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)), + ('name', models.CharField(max_length=64)), + ('type', models.PositiveSmallIntegerField()), + ('positions', models.PositiveSmallIntegerField(default=1, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(64)])), + ('device_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='rearport_templates', to='dcim.DeviceType')), + ], + options={ + 'ordering': ['device_type', 'name'], + 'unique_together': {('device_type', 'name')}, + }, + ), + migrations.CreateModel( + name='FrontPortTemplate', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)), + ('name', models.CharField(max_length=64)), + ('type', models.PositiveSmallIntegerField()), + ('rear_port_position', models.PositiveSmallIntegerField(default=1, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(64)])), + ('device_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='frontport_templates', to='dcim.DeviceType')), + ('rear_port', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='frontport_templates', to='dcim.RearPortTemplate')), + ], + options={ + 'ordering': ['device_type', 'name'], + 'unique_together': {('rear_port', 'rear_port_position'), ('device_type', 'name')}, + }, + ), + migrations.CreateModel( + name='FrontPort', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)), + ('name', models.CharField(max_length=64)), + ('type', models.PositiveSmallIntegerField()), + ('rear_port_position', models.PositiveSmallIntegerField(default=1, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(64)])), + ('description', models.CharField(blank=True, max_length=100)), + ('device', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='frontports', to='dcim.Device')), + ('rear_port', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='frontports', to='dcim.RearPort')), + ('tags', taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags')), + ], + options={ + 'ordering': ['device', 'name'], + 'unique_together': {('device', 'name'), ('rear_port', 'rear_port_position')}, + }, + ), + migrations.AlterField( + model_name='consoleporttemplate', + name='device_type', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='consoleport_templates', to='dcim.DeviceType'), + ), + migrations.AlterField( + model_name='consoleserverporttemplate', + name='device_type', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='consoleserverport_templates', to='dcim.DeviceType'), + ), + migrations.AlterField( + model_name='poweroutlettemplate', + name='device_type', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='poweroutlet_templates', to='dcim.DeviceType'), + ), + migrations.AlterField( + model_name='powerporttemplate', + name='device_type', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='powerport_templates', to='dcim.DeviceType'), + ), + ] diff --git a/netbox/dcim/migrations/0067_device_type_remove_qualifiers_squashed_0070_custom_tag_models.py b/netbox/dcim/migrations/0067_device_type_remove_qualifiers_squashed_0070_custom_tag_models.py new file mode 100644 index 000000000..6fbf115d9 --- /dev/null +++ b/netbox/dcim/migrations/0067_device_type_remove_qualifiers_squashed_0070_custom_tag_models.py @@ -0,0 +1,146 @@ +import taggit.managers +from django.db import migrations, models + + +class Migration(migrations.Migration): + + replaces = [('dcim', '0067_device_type_remove_qualifiers'), ('dcim', '0068_rack_new_fields'), ('dcim', '0069_deprecate_nullablecharfield'), ('dcim', '0070_custom_tag_models')] + + dependencies = [ + ('extras', '0019_tag_taggeditem'), + ('dcim', '0066_cables'), + ] + + operations = [ + migrations.RemoveField( + model_name='devicetype', + name='is_console_server', + ), + migrations.RemoveField( + model_name='devicetype', + name='is_network_device', + ), + migrations.RemoveField( + model_name='devicetype', + name='is_pdu', + ), + migrations.RemoveField( + model_name='devicetype', + name='interface_ordering', + ), + migrations.AddField( + model_name='rack', + name='status', + field=models.PositiveSmallIntegerField(default=3), + ), + migrations.AddField( + model_name='rack', + name='outer_depth', + field=models.PositiveSmallIntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name='rack', + name='outer_unit', + field=models.PositiveSmallIntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name='rack', + name='outer_width', + field=models.PositiveSmallIntegerField(blank=True, null=True), + ), + migrations.AlterField( + model_name='device', + name='asset_tag', + field=models.CharField(blank=True, max_length=50, null=True, unique=True), + ), + migrations.AlterField( + model_name='device', + name='name', + field=models.CharField(blank=True, max_length=64, null=True, unique=True), + ), + migrations.AlterField( + model_name='inventoryitem', + name='asset_tag', + field=models.CharField(blank=True, max_length=50, null=True, unique=True), + ), + migrations.AddField( + model_name='rack', + name='asset_tag', + field=models.CharField(blank=True, max_length=50, null=True, unique=True), + ), + migrations.AlterField( + model_name='rack', + name='facility_id', + field=models.CharField(blank=True, max_length=50, null=True), + ), + migrations.AlterField( + model_name='consoleport', + name='tags', + field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags'), + ), + migrations.AlterField( + model_name='consoleserverport', + name='tags', + field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags'), + ), + migrations.AlterField( + model_name='device', + name='tags', + field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags'), + ), + migrations.AlterField( + model_name='devicebay', + name='tags', + field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags'), + ), + migrations.AlterField( + model_name='devicetype', + name='tags', + field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags'), + ), + migrations.AlterField( + model_name='frontport', + name='tags', + field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags'), + ), + migrations.AlterField( + model_name='interface', + name='tags', + field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags'), + ), + migrations.AlterField( + model_name='inventoryitem', + name='tags', + field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags'), + ), + migrations.AlterField( + model_name='poweroutlet', + name='tags', + field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags'), + ), + migrations.AlterField( + model_name='powerport', + name='tags', + field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags'), + ), + migrations.AlterField( + model_name='rack', + name='tags', + field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags'), + ), + migrations.AlterField( + model_name='rearport', + name='tags', + field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags'), + ), + migrations.AlterField( + model_name='site', + name='tags', + field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags'), + ), + migrations.AlterField( + model_name='virtualchassis', + name='tags', + field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags'), + ), + ] diff --git a/netbox/dcim/migrations/0071_device_components_add_description_squashed_0088_powerfeed_available_power.py b/netbox/dcim/migrations/0071_device_components_add_description_squashed_0088_powerfeed_available_power.py new file mode 100644 index 000000000..f74572c6f --- /dev/null +++ b/netbox/dcim/migrations/0071_device_components_add_description_squashed_0088_powerfeed_available_power.py @@ -0,0 +1,839 @@ +import sys + +import django.core.validators +import django.db.models.deletion +import taggit.managers +from django.db import migrations, models + +SITE_STATUS_CHOICES = ( + (1, 'active'), + (2, 'planned'), + (4, 'retired'), +) + +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'), +) + +SUBDEVICE_ROLE_CHOICES = ( + ('true', 'parent'), + ('false', 'child'), +) + +DEVICE_FACE_CHOICES = ( + (0, 'front'), + (1, 'rear'), +) + +DEVICE_STATUS_CHOICES = ( + (0, 'offline'), + (1, 'active'), + (2, 'planned'), + (3, 'staged'), + (4, 'failed'), + (5, 'inventory'), + (6, 'decommissioning'), +) + +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'), +) + +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'), +) + +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'), +) + +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'), +) + +POWEROUTLET_FEED_LEG_CHOICES_CHOICES = ( + (1, 'A'), + (2, 'B'), + (3, 'C'), +) + + +def cache_cable_devices(apps, schema_editor): + Cable = apps.get_model('dcim', 'Cable') + + if 'test' not in sys.argv: + print("\nUpdating cable device terminations...") + cable_count = Cable.objects.count() + + # Cache A/B termination devices on all existing Cables. Note that the custom save() method on Cable is not + # available during a migration, so we replicate its logic here. + for i, cable in enumerate(Cable.objects.all(), start=1): + + if not i % 1000 and 'test' not in sys.argv: + print("[{}/{}]".format(i, cable_count)) + + termination_a_model = apps.get_model(cable.termination_a_type.app_label, cable.termination_a_type.model) + termination_a_device = None + if hasattr(termination_a_model, 'device'): + termination_a = termination_a_model.objects.get(pk=cable.termination_a_id) + termination_a_device = termination_a.device + + termination_b_model = apps.get_model(cable.termination_b_type.app_label, cable.termination_b_type.model) + termination_b_device = None + if hasattr(termination_b_model, 'device'): + termination_b = termination_b_model.objects.get(pk=cable.termination_b_id) + termination_b_device = termination_b.device + + Cable.objects.filter(pk=cable.pk).update( + _termination_a_device=termination_a_device, + _termination_b_device=termination_b_device + ) + + +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) + + +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) + + +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) + + +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) + + +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) + + +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) + + +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_str, slug in CABLE_STATUS_CHOICES: + Cable.objects.filter(status=bool_str).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) + + +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) + + +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): + + replaces = [('dcim', '0071_device_components_add_description'), ('dcim', '0072_powerfeeds'), ('dcim', '0073_interface_form_factor_to_type'), ('dcim', '0074_increase_field_length_platform_name_slug'), ('dcim', '0075_cable_devices'), ('dcim', '0076_console_port_types'), ('dcim', '0077_power_types'), ('dcim', '0078_3569_site_fields'), ('dcim', '0079_3569_rack_fields'), ('dcim', '0080_3569_devicetype_fields'), ('dcim', '0081_3569_device_fields'), ('dcim', '0082_3569_interface_fields'), ('dcim', '0082_3569_port_fields'), ('dcim', '0083_3569_cable_fields'), ('dcim', '0084_3569_powerfeed_fields'), ('dcim', '0085_3569_poweroutlet_fields'), ('dcim', '0086_device_name_nonunique'), ('dcim', '0087_role_descriptions'), ('dcim', '0088_powerfeed_available_power')] + + dependencies = [ + ('dcim', '0070_custom_tag_models'), + ('extras', '0021_add_color_comments_changelog_to_tag'), + ('tenancy', '0006_custom_tag_models'), + ] + + operations = [ + migrations.AddField( + model_name='consoleport', + name='description', + field=models.CharField(blank=True, max_length=100), + ), + migrations.AddField( + model_name='consoleserverport', + name='description', + field=models.CharField(blank=True, max_length=100), + ), + migrations.AddField( + model_name='devicebay', + name='description', + field=models.CharField(blank=True, max_length=100), + ), + migrations.AddField( + model_name='poweroutlet', + name='description', + field=models.CharField(blank=True, max_length=100), + ), + migrations.AddField( + model_name='powerport', + name='description', + field=models.CharField(blank=True, max_length=100), + ), + migrations.CreateModel( + name='PowerPanel', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)), + ('created', models.DateField(auto_now_add=True, null=True)), + ('last_updated', models.DateTimeField(auto_now=True, null=True)), + ('name', models.CharField(max_length=50)), + ('rack_group', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='dcim.RackGroup')), + ('site', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='dcim.Site')), + ], + options={ + 'ordering': ['site', 'name'], + 'unique_together': {('site', 'name')}, + }, + ), + migrations.CreateModel( + name='PowerFeed', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)), + ('created', models.DateField(auto_now_add=True, null=True)), + ('last_updated', models.DateTimeField(auto_now=True, null=True)), + ('name', models.CharField(max_length=50)), + ('status', models.PositiveSmallIntegerField(default=1)), + ('type', models.PositiveSmallIntegerField(default=1)), + ('supply', models.PositiveSmallIntegerField(default=1)), + ('phase', models.PositiveSmallIntegerField(default=1)), + ('voltage', models.PositiveSmallIntegerField(default=120, validators=[django.core.validators.MinValueValidator(1)])), + ('amperage', models.PositiveSmallIntegerField(default=20, validators=[django.core.validators.MinValueValidator(1)])), + ('max_utilization', models.PositiveSmallIntegerField(default=80, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)])), + ('available_power', models.PositiveSmallIntegerField(default=0, editable=False)), + ('comments', models.TextField(blank=True)), + ('cable', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.Cable')), + ('power_panel', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='powerfeeds', to='dcim.PowerPanel')), + ('rack', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='dcim.Rack')), + ('tags', taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='extras.TaggedItem', to='extras.Tag', verbose_name='Tags')), + ('connected_endpoint', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.PowerPort')), + ('connection_status', models.NullBooleanField()), + ], + options={ + 'ordering': ['power_panel', 'name'], + 'unique_together': {('power_panel', 'name')}, + }, + ), + migrations.RenameField( + model_name='powerport', + old_name='connected_endpoint', + new_name='_connected_poweroutlet', + ), + migrations.AddField( + model_name='powerport', + name='_connected_powerfeed', + field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.PowerFeed'), + ), + migrations.AddField( + model_name='powerport', + name='allocated_draw', + field=models.PositiveSmallIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(1)]), + ), + migrations.AddField( + model_name='powerport', + name='maximum_draw', + field=models.PositiveSmallIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(1)]), + ), + migrations.AddField( + model_name='powerporttemplate', + name='allocated_draw', + field=models.PositiveSmallIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(1)]), + ), + migrations.AddField( + model_name='powerporttemplate', + name='maximum_draw', + field=models.PositiveSmallIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(1)]), + ), + migrations.AddField( + model_name='poweroutlet', + name='feed_leg', + field=models.PositiveSmallIntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name='poweroutlet', + name='power_port', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='poweroutlets', to='dcim.PowerPort'), + ), + migrations.AddField( + model_name='poweroutlettemplate', + name='feed_leg', + field=models.PositiveSmallIntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name='poweroutlettemplate', + name='power_port', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='poweroutlet_templates', to='dcim.PowerPortTemplate'), + ), + migrations.RenameField( + model_name='interface', + old_name='form_factor', + new_name='type', + ), + migrations.RenameField( + model_name='interfacetemplate', + old_name='form_factor', + new_name='type', + ), + migrations.AlterField( + model_name='platform', + name='name', + field=models.CharField(max_length=100, unique=True), + ), + migrations.AlterField( + model_name='platform', + name='slug', + field=models.SlugField(max_length=100, unique=True), + ), + migrations.AddField( + model_name='cable', + name='_termination_a_device', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='+', to='dcim.Device'), + ), + migrations.AddField( + model_name='cable', + name='_termination_b_device', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='+', to='dcim.Device'), + ), + migrations.RunPython( + code=cache_cable_devices, + reverse_code=django.db.migrations.operations.special.RunPython.noop, + ), + 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), + ), + 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), + ), + migrations.AlterField( + model_name='site', + name='status', + field=models.CharField(default='active', max_length=50), + ), + migrations.RunPython( + code=site_status_to_slug, + ), + 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), + ), + migrations.AlterField( + model_name='rack', + name='status', + field=models.CharField(default='active', max_length=50), + ), + migrations.RunPython( + code=rack_status_to_slug, + ), + 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), + ), + 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), + ), + 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), + ), + migrations.AlterField( + model_name='device', + name='status', + field=models.CharField(default='active', max_length=50), + ), + migrations.RunPython( + code=device_status_to_slug, + ), + migrations.AlterField( + model_name='interfacetemplate', + name='type', + field=models.CharField(max_length=50), + ), + migrations.RunPython( + code=interfacetemplate_type_to_slug, + ), + migrations.AlterField( + model_name='interface', + name='type', + field=models.CharField(max_length=50), + ), + migrations.RunPython( + code=interface_type_to_slug, + ), + 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), + ), + migrations.AlterField( + model_name='frontporttemplate', + name='type', + field=models.CharField(max_length=50), + ), + migrations.RunPython( + code=frontporttemplate_type_to_slug, + ), + migrations.AlterField( + model_name='rearporttemplate', + name='type', + field=models.CharField(max_length=50), + ), + migrations.RunPython( + code=rearporttemplate_type_to_slug, + ), + migrations.AlterField( + model_name='frontport', + name='type', + field=models.CharField(max_length=50), + ), + migrations.RunPython( + code=frontport_type_to_slug, + ), + migrations.AlterField( + model_name='rearport', + name='type', + field=models.CharField(max_length=50), + ), + migrations.RunPython( + code=rearport_type_to_slug, + ), + 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), + ), + migrations.AlterField( + model_name='cable', + name='status', + field=models.CharField(default='connected', max_length=50), + ), + migrations.RunPython( + code=cable_status_to_slug, + ), + 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), + ), + migrations.AlterField( + model_name='powerfeed', + name='status', + field=models.CharField(default='active', max_length=50), + ), + migrations.RunPython( + code=powerfeed_status_to_slug, + ), + migrations.AlterField( + model_name='powerfeed', + name='type', + field=models.CharField(default='primary', max_length=50), + ), + migrations.RunPython( + code=powerfeed_type_to_slug, + ), + migrations.AlterField( + model_name='powerfeed', + name='supply', + field=models.CharField(default='ac', max_length=50), + ), + migrations.RunPython( + code=powerfeed_supply_to_slug, + ), + migrations.AlterField( + model_name='powerfeed', + name='phase', + field=models.CharField(default='single-phase', max_length=50), + ), + migrations.RunPython( + code=powerfeed_phase_to_slug, + ), + 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), + ), + 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), + ), + 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'), ('site', 'tenant', 'name'), ('virtual_chassis', 'vc_position')}, + ), + 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), + ), + migrations.AlterField( + model_name='powerfeed', + name='available_power', + field=models.PositiveIntegerField(default=0, editable=False), + ), + ] diff --git a/netbox/dcim/migrations/0076_console_port_types.py b/netbox/dcim/migrations/0076_console_port_types.py new file mode 100644 index 000000000..844b32283 --- /dev/null +++ b/netbox/dcim/migrations/0076_console_port_types.py @@ -0,0 +1,33 @@ +# Generated by Django 2.2.6 on 2019-10-30 17:41 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dcim', '0075_cable_devices'), + ] + + operations = [ + migrations.AddField( + model_name='consoleport', + name='type', + field=models.CharField(blank=True, max_length=50), + ), + migrations.AddField( + model_name='consoleporttemplate', + name='type', + field=models.CharField(blank=True, max_length=50), + ), + migrations.AddField( + model_name='consoleserverport', + name='type', + field=models.CharField(blank=True, max_length=50), + ), + migrations.AddField( + model_name='consoleserverporttemplate', + name='type', + field=models.CharField(blank=True, max_length=50), + ), + ] diff --git a/netbox/dcim/migrations/0077_power_types.py b/netbox/dcim/migrations/0077_power_types.py new file mode 100644 index 000000000..702bd837b --- /dev/null +++ b/netbox/dcim/migrations/0077_power_types.py @@ -0,0 +1,33 @@ +# Generated by Django 2.2.6 on 2019-11-06 19:48 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dcim', '0076_console_port_types'), + ] + + operations = [ + migrations.AddField( + model_name='poweroutlet', + name='type', + field=models.CharField(blank=True, max_length=50), + ), + migrations.AddField( + model_name='poweroutlettemplate', + name='type', + field=models.CharField(blank=True, max_length=50), + ), + migrations.AddField( + model_name='powerport', + name='type', + field=models.CharField(blank=True, max_length=50), + ), + migrations.AddField( + model_name='powerporttemplate', + name='type', + field=models.CharField(blank=True, max_length=50), + ), + ] diff --git a/netbox/dcim/migrations/0078_3569_site_fields.py b/netbox/dcim/migrations/0078_3569_site_fields.py new file mode 100644 index 000000000..8775abe5e --- /dev/null +++ b/netbox/dcim/migrations/0078_3569_site_fields.py @@ -0,0 +1,35 @@ +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 + ), + + ] diff --git a/netbox/dcim/migrations/0079_3569_rack_fields.py b/netbox/dcim/migrations/0079_3569_rack_fields.py new file mode 100644 index 000000000..4e76a270f --- /dev/null +++ b/netbox/dcim/migrations/0079_3569_rack_fields.py @@ -0,0 +1,92 @@ +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), + ), + + ] diff --git a/netbox/dcim/migrations/0080_3569_devicetype_fields.py b/netbox/dcim/migrations/0080_3569_devicetype_fields.py new file mode 100644 index 000000000..e729eaa55 --- /dev/null +++ b/netbox/dcim/migrations/0080_3569_devicetype_fields.py @@ -0,0 +1,39 @@ +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), + ), + + ] diff --git a/netbox/dcim/migrations/0081_3569_device_fields.py b/netbox/dcim/migrations/0081_3569_device_fields.py new file mode 100644 index 000000000..f1f0bdb2b --- /dev/null +++ b/netbox/dcim/migrations/0081_3569_device_fields.py @@ -0,0 +1,65 @@ +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 + ), + + ] diff --git a/netbox/dcim/migrations/0082_3569_interface_fields.py b/netbox/dcim/migrations/0082_3569_interface_fields.py new file mode 100644 index 000000000..57701ce0a --- /dev/null +++ b/netbox/dcim/migrations/0082_3569_interface_fields.py @@ -0,0 +1,147 @@ +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), + ), + + ] diff --git a/netbox/dcim/migrations/0082_3569_port_fields.py b/netbox/dcim/migrations/0082_3569_port_fields.py new file mode 100644 index 000000000..6d8f50c32 --- /dev/null +++ b/netbox/dcim/migrations/0082_3569_port_fields.py @@ -0,0 +1,93 @@ +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 + ), + ] diff --git a/netbox/dcim/migrations/0083_3569_cable_fields.py b/netbox/dcim/migrations/0083_3569_cable_fields.py new file mode 100644 index 000000000..26cf734f7 --- /dev/null +++ b/netbox/dcim/migrations/0083_3569_cable_fields.py @@ -0,0 +1,106 @@ +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_str, slug in CABLE_STATUS_CHOICES: + Cable.objects.filter(status=bool_str).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), + ), + + ] diff --git a/netbox/dcim/migrations/0084_3569_powerfeed_fields.py b/netbox/dcim/migrations/0084_3569_powerfeed_fields.py new file mode 100644 index 000000000..332443d0a --- /dev/null +++ b/netbox/dcim/migrations/0084_3569_powerfeed_fields.py @@ -0,0 +1,100 @@ +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 + ), + + ] diff --git a/netbox/dcim/migrations/0085_3569_poweroutlet_fields.py b/netbox/dcim/migrations/0085_3569_poweroutlet_fields.py new file mode 100644 index 000000000..e2c070584 --- /dev/null +++ b/netbox/dcim/migrations/0085_3569_poweroutlet_fields.py @@ -0,0 +1,62 @@ +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), + ), + + ] diff --git a/netbox/dcim/migrations/0086_device_name_nonunique.py b/netbox/dcim/migrations/0086_device_name_nonunique.py new file mode 100644 index 000000000..3666cf018 --- /dev/null +++ b/netbox/dcim/migrations/0086_device_name_nonunique.py @@ -0,0 +1,23 @@ +# 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')}, + ), + ] diff --git a/netbox/dcim/migrations/0087_role_descriptions.py b/netbox/dcim/migrations/0087_role_descriptions.py new file mode 100644 index 000000000..5f8fd9707 --- /dev/null +++ b/netbox/dcim/migrations/0087_role_descriptions.py @@ -0,0 +1,23 @@ +# 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), + ), + ] diff --git a/netbox/dcim/migrations/0088_powerfeed_available_power.py b/netbox/dcim/migrations/0088_powerfeed_available_power.py new file mode 100644 index 000000000..af13d49c6 --- /dev/null +++ b/netbox/dcim/migrations/0088_powerfeed_available_power.py @@ -0,0 +1,18 @@ +# 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), + ), + ] diff --git a/netbox/dcim/migrations/0089_deterministic_ordering.py b/netbox/dcim/migrations/0089_deterministic_ordering.py new file mode 100644 index 000000000..6944cff00 --- /dev/null +++ b/netbox/dcim/migrations/0089_deterministic_ordering.py @@ -0,0 +1,21 @@ +# Generated by Django 2.2.8 on 2020-01-15 18:10 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('dcim', '0088_powerfeed_available_power'), + ] + + operations = [ + migrations.AlterModelOptions( + name='device', + options={'ordering': ('name', 'pk'), 'permissions': (('napalm_read', 'Read-only access to devices via NAPALM'), ('napalm_write', 'Read/write access to devices via NAPALM'))}, + ), + migrations.AlterModelOptions( + name='rack', + options={'ordering': ('site', 'group', 'name', 'pk')}, + ), + ] diff --git a/netbox/dcim/migrations/0090_cable_termination_models.py b/netbox/dcim/migrations/0090_cable_termination_models.py new file mode 100644 index 000000000..b5f240f3e --- /dev/null +++ b/netbox/dcim/migrations/0090_cable_termination_models.py @@ -0,0 +1,24 @@ +# Generated by Django 2.2.8 on 2020-01-15 20:51 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('dcim', '0089_deterministic_ordering'), + ] + + operations = [ + migrations.AlterField( + model_name='cable', + name='termination_a_type', + field=models.ForeignKey(limit_choices_to=models.Q(models.Q(models.Q(('app_label', 'circuits'), ('model__in', ('circuittermination',))), models.Q(('app_label', 'dcim'), ('model__in', ('consoleport', 'consoleserverport', 'frontport', 'interface', 'powerfeed', 'poweroutlet', 'powerport', 'rearport'))), _connector='OR')), on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.ContentType'), + ), + migrations.AlterField( + model_name='cable', + name='termination_b_type', + field=models.ForeignKey(limit_choices_to=models.Q(models.Q(models.Q(('app_label', 'circuits'), ('model__in', ('circuittermination',))), models.Q(('app_label', 'dcim'), ('model__in', ('consoleport', 'consoleserverport', 'frontport', 'interface', 'powerfeed', 'poweroutlet', 'powerport', 'rearport'))), _connector='OR')), on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.ContentType'), + ), + ] diff --git a/netbox/dcim/models.py b/netbox/dcim/models/__init__.py similarity index 56% rename from netbox/dcim/models.py rename to netbox/dcim/models/__init__.py index 833fb483b..b6716e324 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models/__init__.py @@ -1,6 +1,8 @@ from collections import OrderedDict from itertools import count, groupby +import svgwrite +import yaml from django.conf import settings from django.contrib.auth.models import User from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation @@ -9,176 +11,65 @@ from django.contrib.postgres.fields import ArrayField, JSONField from django.core.exceptions import ObjectDoesNotExist, ValidationError from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models -from django.db.models import Count, F, ProtectedError, Q, Sum +from django.db.models import Count, F, ProtectedError, Sum from django.urls import reverse +from django.utils.http import urlencode from mptt.models import MPTTModel, TreeForeignKey from taggit.managers import TaggableManager from timezone_field import TimeZoneField -from extras.models import ConfigContextModel, CustomFieldModel, ObjectChange, TaggedItem +from dcim.choices import * +from dcim.constants import * +from dcim.fields import ASNField +from extras.models import ConfigContextModel, CustomFieldModel, TaggedItem from utilities.fields import ColorField from utilities.managers import NaturalOrderingManager from utilities.models import ChangeLoggedModel -from utilities.utils import serialize_object, to_meters -from .constants import * -from .exceptions import LoopDetected -from .fields import ASNField, MACAddressField -from .managers import InterfaceManager +from utilities.utils import foreground_color, to_meters +from .device_component_templates import ( + ConsolePortTemplate, ConsoleServerPortTemplate, DeviceBayTemplate, FrontPortTemplate, InterfaceTemplate, + PowerOutletTemplate, PowerPortTemplate, RearPortTemplate, +) +from .device_components import ( + CableTermination, ConsolePort, ConsoleServerPort, DeviceBay, FrontPort, Interface, InventoryItem, PowerOutlet, + PowerPort, RearPort, +) - -class ComponentTemplateModel(models.Model): - - class Meta: - abstract = True - - def instantiate(self, device): - """ - Instantiate a new component on the specified Device. - """ - raise NotImplementedError() - - def to_objectchange(self, action): - return ObjectChange( - changed_object=self, - object_repr=str(self), - action=action, - related_object=self.device_type, - object_data=serialize_object(self) - ) - - -class ComponentModel(models.Model): - description = models.CharField( - max_length=100, - blank=True - ) - - class Meta: - abstract = True - - def to_objectchange(self, action): - # Annotate the parent Device/VM - try: - parent = getattr(self, 'device', None) or getattr(self, 'virtual_machine', None) - except ObjectDoesNotExist: - # The parent device/VM has already been deleted - parent = None - - return ObjectChange( - changed_object=self, - object_repr=str(self), - action=action, - related_object=parent, - object_data=serialize_object(self) - ) - - @property - def parent(self): - return getattr(self, 'device', None) - - -class CableTermination(models.Model): - cable = models.ForeignKey( - to='dcim.Cable', - on_delete=models.SET_NULL, - related_name='+', - blank=True, - null=True - ) - - # Generic relations to Cable. These ensure that an attached Cable is deleted if the terminated object is deleted. - _cabled_as_a = GenericRelation( - to='dcim.Cable', - content_type_field='termination_a_type', - object_id_field='termination_a_id' - ) - _cabled_as_b = GenericRelation( - to='dcim.Cable', - content_type_field='termination_b_type', - object_id_field='termination_b_id' - ) - - is_path_endpoint = True - - class Meta: - abstract = True - - def trace(self, position=1, follow_circuits=False, cable_history=None): - """ - Return a list representing a complete cable path, with each individual segment represented as a three-tuple: - [ - (termination A, cable, termination B), - (termination C, cable, termination D), - (termination E, cable, termination F) - ] - """ - def get_peer_port(termination, position=1, follow_circuits=False): - from circuits.models import CircuitTermination - - # Map a front port to its corresponding rear port - if isinstance(termination, FrontPort): - return termination.rear_port, termination.rear_port_position - - # Map a rear port/position to its corresponding front port - elif isinstance(termination, RearPort): - if position not in range(1, termination.positions + 1): - raise Exception("Invalid position for {} ({} positions): {})".format( - termination, termination.positions, position - )) - try: - peer_port = FrontPort.objects.get( - rear_port=termination, - rear_port_position=position, - ) - return peer_port, 1 - except ObjectDoesNotExist: - return None, None - - # Follow a circuit to its other termination - elif isinstance(termination, CircuitTermination) and follow_circuits: - peer_termination = termination.get_peer_termination() - if peer_termination is None: - return None, None - return peer_termination, position - - # Termination is not a pass-through port - else: - return None, None - - if not self.cable: - return [(self, None, None)] - - # Record cable history to detect loops - if cable_history is None: - cable_history = [] - elif self.cable in cable_history: - raise LoopDetected() - cable_history.append(self.cable) - - far_end = self.cable.termination_b if self.cable.termination_a == self else self.cable.termination_a - path = [(self, self.cable, far_end)] - - peer_port, position = get_peer_port(far_end, position, follow_circuits) - if peer_port is None: - return path - - try: - next_segment = peer_port.trace(position, follow_circuits, cable_history) - except LoopDetected: - return path - - if next_segment is None: - return path + [(peer_port, None, None)] - - return path + next_segment - - def get_cable_peer(self): - if self.cable is None: - return None - if self._cabled_as_a.exists(): - return self.cable.termination_b - if self._cabled_as_b.exists(): - return self.cable.termination_a +__all__ = ( + 'Cable', + 'CableTermination', + 'ConsolePort', + 'ConsolePortTemplate', + 'ConsoleServerPort', + 'ConsoleServerPortTemplate', + 'Device', + 'DeviceBay', + 'DeviceBayTemplate', + 'DeviceRole', + 'DeviceType', + 'FrontPort', + 'FrontPortTemplate', + 'Interface', + 'InterfaceTemplate', + 'InventoryItem', + 'Manufacturer', + 'Platform', + 'PowerFeed', + 'PowerOutlet', + 'PowerOutletTemplate', + 'PowerPanel', + 'PowerPort', + 'PowerPortTemplate', + 'Rack', + 'RackGroup', + 'RackReservation', + 'RackRole', + 'RearPort', + 'RearPortTemplate', + 'Region', + 'Site', + 'VirtualChassis', +) # @@ -246,9 +137,10 @@ class Site(ChangeLoggedModel, CustomFieldModel): slug = models.SlugField( unique=True ) - status = models.PositiveSmallIntegerField( - choices=SITE_STATUS_CHOICES, - default=SITE_STATUS_ACTIVE + status = models.CharField( + max_length=50, + choices=SiteStatusChoices, + default=SiteStatusChoices.STATUS_ACTIVE ) region = models.ForeignKey( to='dcim.Region', @@ -331,6 +223,16 @@ class Site(ChangeLoggedModel, CustomFieldModel): 'name', 'slug', 'status', 'region', 'tenant', 'facility', 'asn', 'time_zone', 'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'contact_name', 'contact_phone', 'contact_email', 'comments', ] + clone_fields = [ + 'status', 'region', 'tenant', 'facility', 'asn', 'time_zone', 'description', 'physical_address', + 'shipping_address', 'latitude', 'longitude', 'contact_name', 'contact_phone', 'contact_email', + ] + + STATUS_CLASS_MAP = { + SiteStatusChoices.STATUS_ACTIVE: 'success', + SiteStatusChoices.STATUS_PLANNED: 'info', + SiteStatusChoices.STATUS_RETIRED: 'danger', + } class Meta: ordering = ['name'] @@ -363,7 +265,7 @@ class Site(ChangeLoggedModel, CustomFieldModel): ) def get_status_class(self): - return STATUS_CLASSES[self.status] + return self.STATUS_CLASS_MAP.get(self.status) # @@ -421,8 +323,12 @@ class RackRole(ChangeLoggedModel): unique=True ) color = ColorField() + description = models.CharField( + max_length=100, + blank=True, + ) - csv_headers = ['name', 'slug', 'color'] + csv_headers = ['name', 'slug', 'color', 'description'] class Meta: ordering = ['name'] @@ -438,10 +344,132 @@ class RackRole(ChangeLoggedModel): self.name, self.slug, self.color, + self.description, ) -class Rack(ChangeLoggedModel, CustomFieldModel): +class RackElevationHelperMixin: + """ + Utility class that renders rack elevations. Contains helper methods for rendering elevations as a list of + rack units represented as dictionaries, or an SVG of the elevation. + """ + + @staticmethod + def _add_gradient(drawing, id_, color): + gradient = drawing.linearGradient( + start=('0', '0%'), + end=('0', '5%'), + spreadMethod='repeat', + id_=id_, + gradientTransform='rotate(45, 0, 0)', + gradientUnits='userSpaceOnUse' + ) + gradient.add_stop_color(offset='0%', color='#f7f7f7') + gradient.add_stop_color(offset='50%', color='#f7f7f7') + gradient.add_stop_color(offset='50%', color=color) + gradient.add_stop_color(offset='100%', color=color) + drawing.defs.add(gradient) + + @staticmethod + def _setup_drawing(width, height): + drawing = svgwrite.Drawing(size=(width, height)) + + # add the stylesheet + with open('{}/css/rack_elevation.css'.format(settings.STATICFILES_DIRS[0])) as css_file: + drawing.defs.add(drawing.style(css_file.read())) + + # add gradients + RackElevationHelperMixin._add_gradient(drawing, 'reserved', '#c7c7ff') + RackElevationHelperMixin._add_gradient(drawing, 'occupied', '#f0f0f0') + RackElevationHelperMixin._add_gradient(drawing, 'blocked', '#ffc7c7') + + return drawing + + @staticmethod + def _draw_device_front(drawing, device, start, end, text): + color = device.device_role.color + link = drawing.add( + drawing.a( + reverse('dcim:device', kwargs={'pk': device.pk}), fill='black' + ) + ) + link.add(drawing.rect(start, end, fill='#{}'.format(color))) + hex_color = '#{}'.format(foreground_color(color)) + link.add(drawing.text(device.name, insert=text, fill=hex_color)) + + @staticmethod + def _draw_device_rear(drawing, device, start, end, text): + drawing.add(drawing.rect(start, end, class_="blocked")) + drawing.add(drawing.text(device.name, insert=text)) + + @staticmethod + def _draw_empty(drawing, rack, start, end, text, id_, face_id, class_): + link = drawing.add( + drawing.a('{}?{}'.format( + reverse('dcim:device_add'), + urlencode({'rack': rack.pk, 'site': rack.site.pk, 'face': face_id, 'position': id_}) + )) + ) + link.add(drawing.rect(start, end, class_=class_)) + link.add(drawing.text("add device", insert=text, class_='add-device')) + + def _draw_elevations(self, elevation, reserved_units, face, unit_width, unit_height): + + drawing = self._setup_drawing(unit_width, unit_height * self.u_height) + + unit_cursor = 0 + for unit in elevation: + + # Loop through all units in the elevation + device = unit['device'] + height = unit.get('height', 1) + + # Setup drawing coordinates + start_y = unit_cursor * unit_height + end_y = unit_height * height + start_cordinates = (0, start_y) + end_cordinates = (unit_width, end_y) + text_cordinates = (unit_width / 2, start_y + end_y / 2) + + # Draw the device + if device and device.face == face: + self._draw_device_front(drawing, device, start_cordinates, end_cordinates, text_cordinates) + elif device and device.device_type.is_full_depth: + self._draw_device_rear(drawing, device, start_cordinates, end_cordinates, text_cordinates) + else: + # Draw shallow devices, reservations, or empty units + class_ = 'slot' + if device: + class_ += ' occupied' + if unit["id"] in reserved_units: + class_ += ' reserved' + self._draw_empty( + drawing, self, start_cordinates, end_cordinates, text_cordinates, unit["id"], face, class_ + ) + + unit_cursor += height + + # Wrap the drawing with a border + drawing.add(drawing.rect((0, 0), (unit_width, self.u_height * unit_height), class_='rack')) + + return drawing + + def get_elevation_svg(self, face=DeviceFaceChoices.FACE_FRONT, unit_width=230, unit_height=20): + """ + Return an SVG of the rack elevation + + :param face: Enum of [front, rear] representing the desired side of the rack elevation to render + :param width: Width in pixles for the rendered drawing + :param unit_height: Height of each rack unit for the rendered drawing. Note this is not the total + height of the elevation + """ + elevation = self.get_rack_units(face=face, expand_devices=False) + reserved_units = self.get_reserved_units().keys() + + return self._draw_elevations(elevation, reserved_units, face, unit_width, unit_height) + + +class Rack(ChangeLoggedModel, CustomFieldModel, RackElevationHelperMixin): """ Devices are housed within Racks. Each rack has a defined height measured in rack units, and a front and rear face. Each Rack is assigned to a Site and (optionally) a RackGroup. @@ -474,9 +502,10 @@ class Rack(ChangeLoggedModel, CustomFieldModel): blank=True, null=True ) - status = models.PositiveSmallIntegerField( - choices=RACK_STATUS_CHOICES, - default=RACK_STATUS_ACTIVE + status = models.CharField( + max_length=50, + choices=RackStatusChoices, + default=RackStatusChoices.STATUS_ACTIVE ) role = models.ForeignKey( to='dcim.RackRole', @@ -498,15 +527,15 @@ class Rack(ChangeLoggedModel, CustomFieldModel): verbose_name='Asset tag', help_text='A unique tag used to identify this rack' ) - type = models.PositiveSmallIntegerField( - choices=RACK_TYPE_CHOICES, + type = models.CharField( + choices=RackTypeChoices, + max_length=50, blank=True, - null=True, verbose_name='Type' ) width = models.PositiveSmallIntegerField( - choices=RACK_WIDTH_CHOICES, - default=RACK_WIDTH_19IN, + choices=RackWidthChoices, + default=RackWidthChoices.WIDTH_19IN, verbose_name='Width', help_text='Rail-to-rail width' ) @@ -528,10 +557,10 @@ class Rack(ChangeLoggedModel, CustomFieldModel): blank=True, null=True ) - outer_unit = models.PositiveSmallIntegerField( - choices=RACK_DIMENSION_UNIT_CHOICES, + outer_unit = models.CharField( + max_length=50, + choices=RackDimensionUnitChoices, blank=True, - null=True ) comments = models.TextField( blank=True @@ -552,10 +581,23 @@ class Rack(ChangeLoggedModel, CustomFieldModel): 'site', 'group_name', 'name', 'facility_id', 'tenant', 'status', 'role', 'type', 'serial', 'asset_tag', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'comments', ] + clone_fields = [ + 'site', 'group', 'tenant', 'status', 'role', 'type', 'width', 'u_height', 'desc_units', 'outer_width', + 'outer_depth', 'outer_unit', + ] + + STATUS_CLASS_MAP = { + RackStatusChoices.STATUS_RESERVED: 'warning', + RackStatusChoices.STATUS_AVAILABLE: 'success', + RackStatusChoices.STATUS_PLANNED: 'info', + RackStatusChoices.STATUS_ACTIVE: 'primary', + RackStatusChoices.STATUS_DEPRECATED: 'danger', + } class Meta: - ordering = ['site', 'group', 'name'] + ordering = ('site', 'group', 'name', 'pk') # (site, group, name) may be non-unique unique_together = [ + # Name and facility_id must be unique *only* within a RackGroup ['group', 'name'], ['group', 'facility_id'], ] @@ -569,10 +611,10 @@ class Rack(ChangeLoggedModel, CustomFieldModel): def clean(self): # Validate outer dimensions and unit - if (self.outer_width is not None or self.outer_depth is not None) and self.outer_unit is None: + if (self.outer_width is not None or self.outer_depth is not None) and not self.outer_unit: raise ValidationError("Must specify a unit when setting an outer width/depth") elif self.outer_width is None and self.outer_depth is None: - self.outer_unit = None + self.outer_unit = '' if self.pk: # Validate that Rack is tall enough to house the installed Devices @@ -645,16 +687,18 @@ class Rack(ChangeLoggedModel, CustomFieldModel): return "" def get_status_class(self): - return STATUS_CLASSES[self.status] + return self.STATUS_CLASS_MAP.get(self.status) - def get_rack_units(self, face=RACK_FACE_FRONT, exclude=None, remove_redundant=False): + def get_rack_units(self, face=DeviceFaceChoices.FACE_FRONT, exclude=None, expand_devices=True): """ Return a list of rack units as dictionaries. Example: {'device': None, 'face': 0, 'id': 48, 'name': 'U48'} Each key 'device' is either a Device or None. By default, multi-U devices are repeated for each U they occupy. :param face: Rack face (front or rear) :param exclude: PK of a Device to exclude (optional); helpful when relocating a Device within a Rack - :param remove_redundant: If True, rack units occupied by a device already listed will be omitted + :param expand_devices: When True, all units that a device occupies will be listed with each containing a + reference to the device. When False, only the bottom most unit for a device is included and that unit + contains a height attribute for the device """ elevation = OrderedDict() @@ -663,27 +707,32 @@ class Rack(ChangeLoggedModel, CustomFieldModel): # Add devices to rack units list if self.pk: - for device in Device.objects.prefetch_related('device_type__manufacturer', 'device_role')\ - .annotate(devicebay_count=Count('device_bays'))\ - .exclude(pk=exclude)\ - .filter(rack=self, position__gt=0)\ - .filter(Q(face=face) | Q(device_type__is_full_depth=True)): - if remove_redundant: - elevation[device.position]['device'] = device - for u in range(device.position + 1, device.position + device.device_type.u_height): - elevation.pop(u, None) - else: + queryset = Device.objects.prefetch_related( + 'device_type', + 'device_type__manufacturer', + 'device_role' + ).annotate( + devicebay_count=Count('device_bays') + ).exclude( + pk=exclude + ).filter( + rack=self, + position__gt=0 + ).filter( + Q(face=face) | Q(device_type__is_full_depth=True) + ) + for device in queryset: + if expand_devices: for u in range(device.position, device.position + device.device_type.u_height): elevation[u]['device'] = device + else: + elevation[device.position]['device'] = device + elevation[device.position]['height'] = device.device_type.u_height + for u in range(device.position + 1, device.position + device.device_type.u_height): + elevation.pop(u, None) return [u for u in elevation.values()] - def get_front_elevation(self): - return self.get_rack_units(face=RACK_FACE_FRONT, remove_redundant=True) - - def get_rear_elevation(self): - return self.get_rack_units(face=RACK_FACE_REAR, remove_redundant=True) - def get_available_units(self, u_height=1, rack_face=None, exclude=list()): """ Return a list of units within the rack available to accommodate a device of a given U height (default 1). @@ -911,12 +960,13 @@ class DeviceType(ChangeLoggedModel, CustomFieldModel): verbose_name='Is full depth', help_text='Device consumes both front and rear rack faces' ) - subdevice_role = models.NullBooleanField( - default=None, + subdevice_role = models.CharField( + max_length=50, + choices=SubdeviceRoleChoices, + blank=True, verbose_name='Parent/child status', - choices=SUBDEVICE_ROLE_CHOICES, - help_text='Parent devices house child devices in device bays. Select ' - '"None" if this device type is neither a parent nor a child.' + help_text='Parent devices house child devices in device bays. Leave blank ' + 'if this device type is neither a parent nor a child.' ) comments = models.TextField( blank=True @@ -932,6 +982,9 @@ class DeviceType(ChangeLoggedModel, CustomFieldModel): csv_headers = [ 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role', 'comments', ] + clone_fields = [ + 'manufacturer', 'u_height', 'is_full_depth', 'subdevice_role', + ] class Meta: ordering = ['manufacturer', 'model'] @@ -952,17 +1005,92 @@ class DeviceType(ChangeLoggedModel, CustomFieldModel): def get_absolute_url(self): return reverse('dcim:devicetype', args=[self.pk]) - def to_csv(self): - return ( - self.manufacturer.name, - self.model, - self.slug, - self.part_number, - self.u_height, - self.is_full_depth, - self.get_subdevice_role_display() if self.subdevice_role else None, - self.comments, - ) + def to_yaml(self): + data = OrderedDict(( + ('manufacturer', self.manufacturer.name), + ('model', self.model), + ('slug', self.slug), + ('part_number', self.part_number), + ('u_height', self.u_height), + ('is_full_depth', self.is_full_depth), + ('subdevice_role', self.subdevice_role), + ('comments', self.comments), + )) + + # Component templates + if self.consoleport_templates.exists(): + data['console-ports'] = [ + { + 'name': c.name, + 'type': c.type, + } + for c in self.consoleport_templates.all() + ] + if self.consoleserverport_templates.exists(): + data['console-server-ports'] = [ + { + 'name': c.name, + 'type': c.type, + } + for c in self.consoleserverport_templates.all() + ] + if self.powerport_templates.exists(): + data['power-ports'] = [ + { + 'name': c.name, + 'type': c.type, + 'maximum_draw': c.maximum_draw, + 'allocated_draw': c.allocated_draw, + } + for c in self.powerport_templates.all() + ] + if self.poweroutlet_templates.exists(): + data['power-outlets'] = [ + { + 'name': c.name, + 'type': c.type, + 'power_port': c.power_port.name if c.power_port else None, + 'feed_leg': c.feed_leg, + } + for c in self.poweroutlet_templates.all() + ] + if self.interface_templates.exists(): + data['interfaces'] = [ + { + 'name': c.name, + 'type': c.type, + 'mgmt_only': c.mgmt_only, + } + for c in self.interface_templates.all() + ] + if self.frontport_templates.exists(): + data['front-ports'] = [ + { + 'name': c.name, + 'type': c.type, + 'rear_port': c.rear_port.name, + 'rear_port_position': c.rear_port_position, + } + for c in self.frontport_templates.all() + ] + if self.rearport_templates.exists(): + data['rear-ports'] = [ + { + 'name': c.name, + 'type': c.type, + 'positions': c.positions, + } + for c in self.rearport_templates.all() + ] + if self.device_bay_templates.exists(): + data['device-bays'] = [ + { + 'name': c.name, + } + for c in self.device_bay_templates.all() + ] + + return yaml.dump(dict(data), sort_keys=False) def clean(self): @@ -980,13 +1108,15 @@ class DeviceType(ChangeLoggedModel, CustomFieldModel): "{}U".format(d, d.rack, self.u_height) }) - if self.subdevice_role != SUBDEVICE_ROLE_PARENT and self.device_bay_templates.count(): + if ( + self.subdevice_role != SubdeviceRoleChoices.ROLE_PARENT + ) and self.device_bay_templates.count(): raise ValidationError({ 'subdevice_role': "Must delete all device bay templates associated with this device before " "declassifying it as a parent device." }) - if self.u_height and self.subdevice_role == SUBDEVICE_ROLE_CHILD: + if self.u_height and self.subdevice_role == SubdeviceRoleChoices.ROLE_CHILD: raise ValidationError({ 'u_height': "Child device types must be 0U." }) @@ -997,357 +1127,11 @@ class DeviceType(ChangeLoggedModel, CustomFieldModel): @property def is_parent_device(self): - return bool(self.subdevice_role) + return self.subdevice_role == SubdeviceRoleChoices.ROLE_PARENT @property def is_child_device(self): - return bool(self.subdevice_role is False) - - -class ConsolePortTemplate(ComponentTemplateModel): - """ - A template for a ConsolePort to be created for a new Device. - """ - device_type = models.ForeignKey( - to='dcim.DeviceType', - on_delete=models.CASCADE, - related_name='consoleport_templates' - ) - name = models.CharField( - max_length=50 - ) - - objects = NaturalOrderingManager() - - class Meta: - ordering = ['device_type', 'name'] - unique_together = ['device_type', 'name'] - - def __str__(self): - return self.name - - def instantiate(self, device): - return ConsolePort( - device=device, - name=self.name - ) - - -class ConsoleServerPortTemplate(ComponentTemplateModel): - """ - A template for a ConsoleServerPort to be created for a new Device. - """ - device_type = models.ForeignKey( - to='dcim.DeviceType', - on_delete=models.CASCADE, - related_name='consoleserverport_templates' - ) - name = models.CharField( - max_length=50 - ) - - objects = NaturalOrderingManager() - - class Meta: - ordering = ['device_type', 'name'] - unique_together = ['device_type', 'name'] - - def __str__(self): - return self.name - - def instantiate(self, device): - return ConsoleServerPort( - device=device, - name=self.name - ) - - -class PowerPortTemplate(ComponentTemplateModel): - """ - A template for a PowerPort to be created for a new Device. - """ - device_type = models.ForeignKey( - to='dcim.DeviceType', - on_delete=models.CASCADE, - related_name='powerport_templates' - ) - name = models.CharField( - max_length=50 - ) - maximum_draw = models.PositiveSmallIntegerField( - blank=True, - null=True, - validators=[MinValueValidator(1)], - help_text="Maximum current draw (watts)" - ) - allocated_draw = models.PositiveSmallIntegerField( - blank=True, - null=True, - validators=[MinValueValidator(1)], - help_text="Allocated current draw (watts)" - ) - - objects = NaturalOrderingManager() - - class Meta: - ordering = ['device_type', 'name'] - unique_together = ['device_type', 'name'] - - def __str__(self): - return self.name - - def instantiate(self, device): - return PowerPort( - device=device, - name=self.name, - maximum_draw=self.maximum_draw, - allocated_draw=self.allocated_draw - ) - - -class PowerOutletTemplate(ComponentTemplateModel): - """ - A template for a PowerOutlet to be created for a new Device. - """ - device_type = models.ForeignKey( - to='dcim.DeviceType', - on_delete=models.CASCADE, - related_name='poweroutlet_templates' - ) - name = models.CharField( - max_length=50 - ) - power_port = models.ForeignKey( - to='dcim.PowerPortTemplate', - on_delete=models.SET_NULL, - blank=True, - null=True, - related_name='poweroutlet_templates' - ) - feed_leg = models.PositiveSmallIntegerField( - choices=POWERFEED_LEG_CHOICES, - blank=True, - null=True, - help_text="Phase (for three-phase feeds)" - ) - - objects = NaturalOrderingManager() - - class Meta: - ordering = ['device_type', 'name'] - unique_together = ['device_type', 'name'] - - def __str__(self): - return self.name - - def clean(self): - - # Validate power port assignment - if self.power_port and self.power_port.device_type != self.device_type: - raise ValidationError( - "Parent power port ({}) must belong to the same device type".format(self.power_port) - ) - - def instantiate(self, device): - if self.power_port: - power_port = PowerPort.objects.get(device=device, name=self.power_port.name) - else: - power_port = None - return PowerOutlet( - device=device, - name=self.name, - power_port=power_port, - feed_leg=self.feed_leg - ) - - -class InterfaceTemplate(ComponentTemplateModel): - """ - A template for a physical data interface on a new Device. - """ - device_type = models.ForeignKey( - to='dcim.DeviceType', - on_delete=models.CASCADE, - related_name='interface_templates' - ) - name = models.CharField( - max_length=64 - ) - type = models.PositiveSmallIntegerField( - choices=IFACE_TYPE_CHOICES, - default=IFACE_TYPE_10GE_SFP_PLUS - ) - mgmt_only = models.BooleanField( - default=False, - verbose_name='Management only' - ) - - objects = InterfaceManager() - - class Meta: - ordering = ['device_type', 'name'] - unique_together = ['device_type', 'name'] - - def __str__(self): - return self.name - - # TODO: Remove in v2.7 - @property - def form_factor(self): - """ - Backward-compatibility for form_factor - """ - return self.type - - # TODO: Remove in v2.7 - @form_factor.setter - def form_factor(self, value): - """ - Backward-compatibility for form_factor - """ - self.type = value - - def instantiate(self, device): - return Interface( - device=device, - name=self.name, - type=self.type, - mgmt_only=self.mgmt_only - ) - - -class FrontPortTemplate(ComponentTemplateModel): - """ - Template for a pass-through port on the front of a new Device. - """ - device_type = models.ForeignKey( - to='dcim.DeviceType', - on_delete=models.CASCADE, - related_name='frontport_templates' - ) - name = models.CharField( - max_length=64 - ) - type = models.PositiveSmallIntegerField( - choices=PORT_TYPE_CHOICES - ) - rear_port = models.ForeignKey( - to='dcim.RearPortTemplate', - on_delete=models.CASCADE, - related_name='frontport_templates' - ) - rear_port_position = models.PositiveSmallIntegerField( - default=1, - validators=[MinValueValidator(1), MaxValueValidator(64)] - ) - - objects = NaturalOrderingManager() - - class Meta: - ordering = ['device_type', 'name'] - unique_together = [ - ['device_type', 'name'], - ['rear_port', 'rear_port_position'], - ] - - def __str__(self): - return self.name - - def clean(self): - - # Validate rear port assignment - if self.rear_port.device_type != self.device_type: - raise ValidationError( - "Rear port ({}) must belong to the same device type".format(self.rear_port) - ) - - # Validate rear port position assignment - if self.rear_port_position > self.rear_port.positions: - raise ValidationError( - "Invalid rear port position ({}); rear port {} has only {} positions".format( - self.rear_port_position, self.rear_port.name, self.rear_port.positions - ) - ) - - def instantiate(self, device): - if self.rear_port: - rear_port = RearPort.objects.get(device=device, name=self.rear_port.name) - else: - rear_port = None - return FrontPort( - device=device, - name=self.name, - type=self.type, - rear_port=rear_port, - rear_port_position=self.rear_port_position - ) - - -class RearPortTemplate(ComponentTemplateModel): - """ - Template for a pass-through port on the rear of a new Device. - """ - device_type = models.ForeignKey( - to='dcim.DeviceType', - on_delete=models.CASCADE, - related_name='rearport_templates' - ) - name = models.CharField( - max_length=64 - ) - type = models.PositiveSmallIntegerField( - choices=PORT_TYPE_CHOICES - ) - positions = models.PositiveSmallIntegerField( - default=1, - validators=[MinValueValidator(1), MaxValueValidator(64)] - ) - - objects = NaturalOrderingManager() - - class Meta: - ordering = ['device_type', 'name'] - unique_together = ['device_type', 'name'] - - def __str__(self): - return self.name - - def instantiate(self, device): - return RearPort( - device=device, - name=self.name, - type=self.type, - positions=self.positions - ) - - -class DeviceBayTemplate(ComponentTemplateModel): - """ - A template for a DeviceBay to be created for a new parent Device. - """ - device_type = models.ForeignKey( - to='dcim.DeviceType', - on_delete=models.CASCADE, - related_name='device_bay_templates' - ) - name = models.CharField( - max_length=50 - ) - - objects = NaturalOrderingManager() - - class Meta: - ordering = ['device_type', 'name'] - unique_together = ['device_type', 'name'] - - def __str__(self): - return self.name - - def instantiate(self, device): - return DeviceBay( - device=device, - name=self.name - ) + return self.subdevice_role == SubdeviceRoleChoices.ROLE_CHILD # @@ -1373,8 +1157,12 @@ class DeviceRole(ChangeLoggedModel): verbose_name='VM Role', help_text='Virtual machines may be assigned to this role' ) + description = models.CharField( + max_length=100, + blank=True, + ) - csv_headers = ['name', 'slug', 'color', 'vm_role'] + csv_headers = ['name', 'slug', 'color', 'vm_role', 'description'] class Meta: ordering = ['name'] @@ -1388,6 +1176,7 @@ class DeviceRole(ChangeLoggedModel): self.slug, self.color, self.vm_role, + self.description, ) @@ -1486,8 +1275,7 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel): name = models.CharField( max_length=64, blank=True, - null=True, - unique=True + null=True ) serial = models.CharField( max_length=50, @@ -1521,16 +1309,16 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel): verbose_name='Position (U)', help_text='The lowest-numbered unit occupied by the device' ) - face = models.PositiveSmallIntegerField( + face = models.CharField( + max_length=50, blank=True, - null=True, - choices=RACK_FACE_CHOICES, + choices=DeviceFaceChoices, verbose_name='Rack face' ) - status = models.PositiveSmallIntegerField( - choices=DEVICE_STATUS_CHOICES, - default=DEVICE_STATUS_ACTIVE, - verbose_name='Status' + status = models.CharField( + max_length=50, + choices=DeviceStatusChoices, + default=DeviceStatusChoices.STATUS_ACTIVE ) primary_ip4 = models.OneToOneField( to='ipam.IPAddress', @@ -1591,10 +1379,24 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel): 'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag', 'status', 'site', 'rack_group', 'rack_name', 'position', 'face', 'comments', ] + clone_fields = [ + 'device_type', 'device_role', 'tenant', 'platform', 'site', 'rack', 'status', 'cluster', + ] + + STATUS_CLASS_MAP = { + DeviceStatusChoices.STATUS_OFFLINE: 'warning', + DeviceStatusChoices.STATUS_ACTIVE: 'success', + DeviceStatusChoices.STATUS_PLANNED: 'info', + DeviceStatusChoices.STATUS_STAGED: 'primary', + DeviceStatusChoices.STATUS_FAILED: 'danger', + DeviceStatusChoices.STATUS_INVENTORY: 'default', + DeviceStatusChoices.STATUS_DECOMMISSIONING: 'warning', + } class Meta: - ordering = ['name'] + ordering = ('name', 'pk') # Name may be NULL unique_together = [ + ['site', 'tenant', 'name'], # See validate_unique below ['rack', 'position', 'face'], ['virtual_chassis', 'vc_position'], ] @@ -1609,6 +1411,18 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel): def get_absolute_url(self): return reverse('dcim:device', args=[self.pk]) + def validate_unique(self, exclude=None): + + # Check for a duplicate name on a device assigned to the same Site and no Tenant. This is necessary + # because Django does not consider two NULL fields to be equal, and thus will not trigger a violation + # of the uniqueness constraint without manual intervention. + if self.tenant is None and Device.objects.exclude(pk=self.pk).filter(name=self.name, tenant__isnull=True): + raise ValidationError({ + 'name': 'A device with this name already exists.' + }) + + super().validate_unique(exclude) + def clean(self): super().clean() @@ -1620,7 +1434,7 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel): }) if self.rack is None: - if self.face is not None: + if self.face: raise ValidationError({ 'face': "Cannot select a rack face without assigning a rack.", }) @@ -1630,7 +1444,7 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel): }) # Validate position/face combination - if self.position and self.face is None: + if self.position and not self.face: raise ValidationError({ 'face': "Must specify rack face when defining rack position.", }) @@ -1845,848 +1659,7 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel): return Device.objects.filter(parent_bay__device=self.pk) def get_status_class(self): - return STATUS_CLASSES[self.status] - - -# -# Console ports -# - -class ConsolePort(CableTermination, ComponentModel): - """ - A physical console port within a Device. ConsolePorts connect to ConsoleServerPorts. - """ - device = models.ForeignKey( - to='dcim.Device', - on_delete=models.CASCADE, - related_name='consoleports' - ) - name = models.CharField( - max_length=50 - ) - connected_endpoint = models.OneToOneField( - to='dcim.ConsoleServerPort', - on_delete=models.SET_NULL, - related_name='connected_endpoint', - blank=True, - null=True - ) - connection_status = models.NullBooleanField( - choices=CONNECTION_STATUS_CHOICES, - blank=True - ) - - objects = NaturalOrderingManager() - tags = TaggableManager(through=TaggedItem) - - csv_headers = ['device', 'name', 'description'] - - class Meta: - ordering = ['device', 'name'] - unique_together = ['device', 'name'] - - def __str__(self): - return self.name - - def get_absolute_url(self): - return self.device.get_absolute_url() - - def to_csv(self): - return ( - self.device.identifier, - self.name, - self.description, - ) - - -# -# Console server ports -# - -class ConsoleServerPort(CableTermination, ComponentModel): - """ - A physical port within a Device (typically a designated console server) which provides access to ConsolePorts. - """ - device = models.ForeignKey( - to='dcim.Device', - on_delete=models.CASCADE, - related_name='consoleserverports' - ) - name = models.CharField( - max_length=50 - ) - connection_status = models.NullBooleanField( - choices=CONNECTION_STATUS_CHOICES, - blank=True - ) - - objects = NaturalOrderingManager() - tags = TaggableManager(through=TaggedItem) - - csv_headers = ['device', 'name', 'description'] - - class Meta: - unique_together = ['device', 'name'] - - def __str__(self): - return self.name - - def get_absolute_url(self): - return self.device.get_absolute_url() - - def to_csv(self): - return ( - self.device.identifier, - self.name, - self.description, - ) - - -# -# Power ports -# - -class PowerPort(CableTermination, ComponentModel): - """ - A physical power supply (intake) port within a Device. PowerPorts connect to PowerOutlets. - """ - device = models.ForeignKey( - to='dcim.Device', - on_delete=models.CASCADE, - related_name='powerports' - ) - name = models.CharField( - max_length=50 - ) - maximum_draw = models.PositiveSmallIntegerField( - blank=True, - null=True, - validators=[MinValueValidator(1)], - help_text="Maximum current draw (watts)" - ) - allocated_draw = models.PositiveSmallIntegerField( - blank=True, - null=True, - validators=[MinValueValidator(1)], - help_text="Allocated current draw (watts)" - ) - _connected_poweroutlet = models.OneToOneField( - to='dcim.PowerOutlet', - on_delete=models.SET_NULL, - related_name='connected_endpoint', - blank=True, - null=True - ) - _connected_powerfeed = models.OneToOneField( - to='dcim.PowerFeed', - on_delete=models.SET_NULL, - related_name='+', - blank=True, - null=True - ) - connection_status = models.NullBooleanField( - choices=CONNECTION_STATUS_CHOICES, - blank=True - ) - - objects = NaturalOrderingManager() - tags = TaggableManager(through=TaggedItem) - - csv_headers = ['device', 'name', 'maximum_draw', 'allocated_draw', 'description'] - - class Meta: - ordering = ['device', 'name'] - unique_together = ['device', 'name'] - - def __str__(self): - return self.name - - def get_absolute_url(self): - return self.device.get_absolute_url() - - def to_csv(self): - return ( - self.device.identifier, - self.name, - self.maximum_draw, - self.allocated_draw, - self.description, - ) - - @property - def connected_endpoint(self): - if self._connected_poweroutlet: - return self._connected_poweroutlet - return self._connected_powerfeed - - @connected_endpoint.setter - def connected_endpoint(self, value): - if value is None: - self._connected_poweroutlet = None - self._connected_powerfeed = None - elif isinstance(value, PowerOutlet): - self._connected_poweroutlet = value - self._connected_powerfeed = None - elif isinstance(value, PowerFeed): - self._connected_poweroutlet = None - self._connected_powerfeed = value - else: - raise ValueError( - "Connected endpoint must be a PowerOutlet or PowerFeed, not {}.".format(type(value)) - ) - - def get_power_draw(self): - """ - Return the allocated and maximum power draw (in VA) and child PowerOutlet count for this PowerPort. - """ - # Calculate aggregate draw of all child power outlets if no numbers have been defined manually - if self.allocated_draw is None and self.maximum_draw is None: - outlet_ids = PowerOutlet.objects.filter(power_port=self).values_list('pk', flat=True) - utilization = PowerPort.objects.filter(_connected_poweroutlet_id__in=outlet_ids).aggregate( - maximum_draw_total=Sum('maximum_draw'), - allocated_draw_total=Sum('allocated_draw'), - ) - ret = { - 'allocated': utilization['allocated_draw_total'] or 0, - 'maximum': utilization['maximum_draw_total'] or 0, - 'outlet_count': len(outlet_ids), - 'legs': [], - } - - # Calculate per-leg aggregates for three-phase feeds - if self._connected_powerfeed and self._connected_powerfeed.phase == POWERFEED_PHASE_3PHASE: - for leg, leg_name in POWERFEED_LEG_CHOICES: - outlet_ids = PowerOutlet.objects.filter(power_port=self, feed_leg=leg).values_list('pk', flat=True) - utilization = PowerPort.objects.filter(_connected_poweroutlet_id__in=outlet_ids).aggregate( - maximum_draw_total=Sum('maximum_draw'), - allocated_draw_total=Sum('allocated_draw'), - ) - ret['legs'].append({ - 'name': leg_name, - 'allocated': utilization['allocated_draw_total'] or 0, - 'maximum': utilization['maximum_draw_total'] or 0, - 'outlet_count': len(outlet_ids), - }) - - return ret - - # Default to administratively defined values - return { - 'allocated': self.allocated_draw or 0, - 'maximum': self.maximum_draw or 0, - 'outlet_count': PowerOutlet.objects.filter(power_port=self).count(), - 'legs': [], - } - - -# -# Power outlets -# - -class PowerOutlet(CableTermination, ComponentModel): - """ - A physical power outlet (output) within a Device which provides power to a PowerPort. - """ - device = models.ForeignKey( - to='dcim.Device', - on_delete=models.CASCADE, - related_name='poweroutlets' - ) - name = models.CharField( - max_length=50 - ) - power_port = models.ForeignKey( - to='dcim.PowerPort', - on_delete=models.SET_NULL, - blank=True, - null=True, - related_name='poweroutlets' - ) - feed_leg = models.PositiveSmallIntegerField( - choices=POWERFEED_LEG_CHOICES, - blank=True, - null=True, - help_text="Phase (for three-phase feeds)" - ) - connection_status = models.NullBooleanField( - choices=CONNECTION_STATUS_CHOICES, - blank=True - ) - - objects = NaturalOrderingManager() - tags = TaggableManager(through=TaggedItem) - - csv_headers = ['device', 'name', 'power_port', 'feed_leg', 'description'] - - class Meta: - unique_together = ['device', 'name'] - - def __str__(self): - return self.name - - def get_absolute_url(self): - return self.device.get_absolute_url() - - def to_csv(self): - return ( - self.device.identifier, - self.name, - self.power_port.name if self.power_port else None, - self.get_feed_leg_display(), - self.description, - ) - - def clean(self): - - # Validate power port assignment - if self.power_port and self.power_port.device != self.device: - raise ValidationError( - "Parent power port ({}) must belong to the same device".format(self.power_port) - ) - - -# -# Interfaces -# - -class Interface(CableTermination, ComponentModel): - """ - A network interface within a Device or VirtualMachine. A physical Interface can connect to exactly one other - Interface. - """ - device = models.ForeignKey( - to='Device', - on_delete=models.CASCADE, - related_name='interfaces', - null=True, - blank=True - ) - virtual_machine = models.ForeignKey( - to='virtualization.VirtualMachine', - on_delete=models.CASCADE, - related_name='interfaces', - null=True, - blank=True - ) - name = models.CharField( - max_length=64 - ) - _connected_interface = models.OneToOneField( - to='self', - on_delete=models.SET_NULL, - related_name='+', - blank=True, - null=True - ) - _connected_circuittermination = models.OneToOneField( - to='circuits.CircuitTermination', - on_delete=models.SET_NULL, - related_name='+', - blank=True, - null=True - ) - connection_status = models.NullBooleanField( - choices=CONNECTION_STATUS_CHOICES, - blank=True - ) - lag = models.ForeignKey( - to='self', - on_delete=models.SET_NULL, - related_name='member_interfaces', - null=True, - blank=True, - verbose_name='Parent LAG' - ) - type = models.PositiveSmallIntegerField( - choices=IFACE_TYPE_CHOICES, - default=IFACE_TYPE_10GE_SFP_PLUS - ) - enabled = models.BooleanField( - default=True - ) - mac_address = MACAddressField( - null=True, - blank=True, - verbose_name='MAC Address' - ) - mtu = models.PositiveIntegerField( - blank=True, - null=True, - validators=[MinValueValidator(1), MaxValueValidator(65536)], - verbose_name='MTU' - ) - mgmt_only = models.BooleanField( - default=False, - verbose_name='OOB Management', - help_text='This interface is used only for out-of-band management' - ) - mode = models.PositiveSmallIntegerField( - choices=IFACE_MODE_CHOICES, - blank=True, - null=True - ) - untagged_vlan = models.ForeignKey( - to='ipam.VLAN', - on_delete=models.SET_NULL, - related_name='interfaces_as_untagged', - null=True, - blank=True, - verbose_name='Untagged VLAN' - ) - tagged_vlans = models.ManyToManyField( - to='ipam.VLAN', - related_name='interfaces_as_tagged', - blank=True, - verbose_name='Tagged VLANs' - ) - - objects = InterfaceManager() - tags = TaggableManager(through=TaggedItem) - - csv_headers = [ - 'device', 'virtual_machine', 'name', 'lag', 'type', 'enabled', 'mac_address', 'mtu', 'mgmt_only', - 'description', 'mode', - ] - - class Meta: - ordering = ['device', 'name'] - unique_together = ['device', 'name'] - - def __str__(self): - return self.name - - def get_absolute_url(self): - return reverse('dcim:interface', kwargs={'pk': self.pk}) - - def to_csv(self): - return ( - self.device.identifier if self.device else None, - self.virtual_machine.name if self.virtual_machine else None, - self.name, - self.lag.name if self.lag else None, - self.get_type_display(), - self.enabled, - self.mac_address, - self.mtu, - self.mgmt_only, - self.description, - self.get_mode_display(), - ) - - def clean(self): - - # An Interface must belong to a Device *or* to a VirtualMachine - if self.device and self.virtual_machine: - raise ValidationError("An interface cannot belong to both a device and a virtual machine.") - if not self.device and not self.virtual_machine: - raise ValidationError("An interface must belong to either a device or a virtual machine.") - - # VM interfaces must be virtual - if self.virtual_machine and self.type is not IFACE_TYPE_VIRTUAL: - raise ValidationError({ - 'type': "Virtual machines can only have virtual interfaces." - }) - - # Virtual interfaces cannot be connected - if self.type in NONCONNECTABLE_IFACE_TYPES and ( - self.cable or getattr(self, 'circuit_termination', False) - ): - raise ValidationError({ - 'type': "Virtual and wireless interfaces cannot be connected to another interface or circuit. " - "Disconnect the interface or choose a suitable type." - }) - - # An interface's LAG must belong to the same device (or VC master) - if self.lag and self.lag.device not in [self.device, self.device.get_vc_master()]: - raise ValidationError({ - 'lag': "The selected LAG interface ({}) belongs to a different device ({}).".format( - self.lag.name, self.lag.device.name - ) - }) - - # A virtual interface cannot have a parent LAG - if self.type in NONCONNECTABLE_IFACE_TYPES and self.lag is not None: - raise ValidationError({ - 'lag': "{} interfaces cannot have a parent LAG interface.".format(self.get_type_display()) - }) - - # Only a LAG can have LAG members - if self.type != IFACE_TYPE_LAG and self.member_interfaces.exists(): - raise ValidationError({ - 'type': "Cannot change interface type; it has LAG members ({}).".format( - ", ".join([iface.name for iface in self.member_interfaces.all()]) - ) - }) - - # Validate untagged VLAN - if self.untagged_vlan and self.untagged_vlan.site not in [self.parent.site, None]: - raise ValidationError({ - 'untagged_vlan': "The untagged VLAN ({}) must belong to the same site as the interface's parent " - "device/VM, or it must be global".format(self.untagged_vlan) - }) - - def save(self, *args, **kwargs): - - # Remove untagged VLAN assignment for non-802.1Q interfaces - if self.mode is None: - self.untagged_vlan = None - - # Only "tagged" interfaces may have tagged VLANs assigned. ("tagged all" implies all VLANs are assigned.) - if self.pk and self.mode is not IFACE_MODE_TAGGED: - self.tagged_vlans.clear() - - return super().save(*args, **kwargs) - - def to_objectchange(self, action): - # Annotate the parent Device/VM - try: - parent_obj = self.device or self.virtual_machine - except ObjectDoesNotExist: - parent_obj = None - - return ObjectChange( - changed_object=self, - object_repr=str(self), - action=action, - related_object=parent_obj, - object_data=serialize_object(self) - ) - - # TODO: Remove in v2.7 - @property - def form_factor(self): - """ - Backward-compatibility for form_factor - """ - return self.type - - # TODO: Remove in v2.7 - @form_factor.setter - def form_factor(self, value): - """ - Backward-compatibility for form_factor - """ - self.type = value - - @property - def connected_endpoint(self): - if self._connected_interface: - return self._connected_interface - return self._connected_circuittermination - - @connected_endpoint.setter - def connected_endpoint(self, value): - from circuits.models import CircuitTermination - - if value is None: - self._connected_interface = None - self._connected_circuittermination = None - elif isinstance(value, Interface): - self._connected_interface = value - self._connected_circuittermination = None - elif isinstance(value, CircuitTermination): - self._connected_interface = None - self._connected_circuittermination = value - else: - raise ValueError( - "Connected endpoint must be an Interface or CircuitTermination, not {}.".format(type(value)) - ) - - @property - def parent(self): - return self.device or self.virtual_machine - - @property - def is_connectable(self): - return self.type not in NONCONNECTABLE_IFACE_TYPES - - @property - def is_virtual(self): - return self.type in VIRTUAL_IFACE_TYPES - - @property - def is_wireless(self): - return self.type in WIRELESS_IFACE_TYPES - - @property - def is_lag(self): - return self.type == IFACE_TYPE_LAG - - @property - def count_ipaddresses(self): - return self.ip_addresses.count() - - -# -# Pass-through ports -# - -class FrontPort(CableTermination, ComponentModel): - """ - A pass-through port on the front of a Device. - """ - device = models.ForeignKey( - to='dcim.Device', - on_delete=models.CASCADE, - related_name='frontports' - ) - name = models.CharField( - max_length=64 - ) - type = models.PositiveSmallIntegerField( - choices=PORT_TYPE_CHOICES - ) - rear_port = models.ForeignKey( - to='dcim.RearPort', - on_delete=models.CASCADE, - related_name='frontports' - ) - rear_port_position = models.PositiveSmallIntegerField( - default=1, - validators=[MinValueValidator(1), MaxValueValidator(64)] - ) - - is_path_endpoint = False - - objects = NaturalOrderingManager() - tags = TaggableManager(through=TaggedItem) - - csv_headers = ['device', 'name', 'type', 'rear_port', 'rear_port_position', 'description'] - - class Meta: - ordering = ['device', 'name'] - unique_together = [ - ['device', 'name'], - ['rear_port', 'rear_port_position'], - ] - - def __str__(self): - return self.name - - def to_csv(self): - return ( - self.device.identifier, - self.name, - self.get_type_display(), - self.rear_port.name, - self.rear_port_position, - self.description, - ) - - def clean(self): - - # Validate rear port assignment - if self.rear_port.device != self.device: - raise ValidationError( - "Rear port ({}) must belong to the same device".format(self.rear_port) - ) - - # Validate rear port position assignment - if self.rear_port_position > self.rear_port.positions: - raise ValidationError( - "Invalid rear port position ({}); rear port {} has only {} positions".format( - self.rear_port_position, self.rear_port.name, self.rear_port.positions - ) - ) - - -class RearPort(CableTermination, ComponentModel): - """ - A pass-through port on the rear of a Device. - """ - device = models.ForeignKey( - to='dcim.Device', - on_delete=models.CASCADE, - related_name='rearports' - ) - name = models.CharField( - max_length=64 - ) - type = models.PositiveSmallIntegerField( - choices=PORT_TYPE_CHOICES - ) - positions = models.PositiveSmallIntegerField( - default=1, - validators=[MinValueValidator(1), MaxValueValidator(64)] - ) - - is_path_endpoint = False - - objects = NaturalOrderingManager() - tags = TaggableManager(through=TaggedItem) - - csv_headers = ['device', 'name', 'type', 'positions', 'description'] - - class Meta: - ordering = ['device', 'name'] - unique_together = ['device', 'name'] - - def __str__(self): - return self.name - - def to_csv(self): - return ( - self.device.identifier, - self.name, - self.get_type_display(), - self.positions, - self.description, - ) - - -# -# Device bays -# - -class DeviceBay(ComponentModel): - """ - An empty space within a Device which can house a child device - """ - device = models.ForeignKey( - to='dcim.Device', - on_delete=models.CASCADE, - related_name='device_bays' - ) - name = models.CharField( - max_length=50, - verbose_name='Name' - ) - installed_device = models.OneToOneField( - to='dcim.Device', - on_delete=models.SET_NULL, - related_name='parent_bay', - blank=True, - null=True - ) - - objects = NaturalOrderingManager() - tags = TaggableManager(through=TaggedItem) - - csv_headers = ['device', 'name', 'installed_device', 'description'] - - class Meta: - ordering = ['device', 'name'] - unique_together = ['device', 'name'] - - def __str__(self): - return '{} - {}'.format(self.device.name, self.name) - - def get_absolute_url(self): - return self.device.get_absolute_url() - - def to_csv(self): - return ( - self.device.identifier, - self.name, - self.installed_device.identifier if self.installed_device else None, - self.description, - ) - - def clean(self): - - # Validate that the parent Device can have DeviceBays - if not self.device.device_type.is_parent_device: - raise ValidationError("This type of device ({}) does not support device bays.".format( - self.device.device_type - )) - - # Cannot install a device into itself, obviously - if self.device == self.installed_device: - raise ValidationError("Cannot install a device into itself.") - - # Check that the installed device is not already installed elsewhere - if self.installed_device: - current_bay = DeviceBay.objects.filter(installed_device=self.installed_device).first() - if current_bay and current_bay != self: - raise ValidationError({ - 'installed_device': "Cannot install the specified device; device is already installed in {}".format( - current_bay - ) - }) - - -# -# Inventory items -# - -class InventoryItem(ComponentModel): - """ - An InventoryItem represents a serialized piece of hardware within a Device, such as a line card or power supply. - InventoryItems are used only for inventory purposes. - """ - device = models.ForeignKey( - to='dcim.Device', - on_delete=models.CASCADE, - related_name='inventory_items' - ) - parent = models.ForeignKey( - to='self', - on_delete=models.CASCADE, - related_name='child_items', - blank=True, - null=True - ) - name = models.CharField( - max_length=50, - verbose_name='Name' - ) - manufacturer = models.ForeignKey( - to='dcim.Manufacturer', - on_delete=models.PROTECT, - related_name='inventory_items', - blank=True, - null=True - ) - part_id = models.CharField( - max_length=50, - verbose_name='Part ID', - blank=True - ) - serial = models.CharField( - max_length=50, - verbose_name='Serial number', - blank=True - ) - asset_tag = models.CharField( - max_length=50, - unique=True, - blank=True, - null=True, - verbose_name='Asset tag', - help_text='A unique tag used to identify this item' - ) - discovered = models.BooleanField( - default=False, - verbose_name='Discovered' - ) - - tags = TaggableManager(through=TaggedItem) - - csv_headers = [ - 'device', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered', 'description', - ] - - class Meta: - ordering = ['device__id', 'parent__id', 'name'] - unique_together = ['device', 'parent', 'name'] - - def __str__(self): - return self.name - - def get_absolute_url(self): - return self.device.get_absolute_url() - - def to_csv(self): - return ( - self.device.name or '{{{}}}'.format(self.device.pk), - self.name, - self.manufacturer.name if self.manufacturer else None, - self.part_id, - self.serial, - self.asset_tag, - self.discovered, - self.description, - ) + return self.STATUS_CLASS_MAP.get(self.status) # @@ -2834,21 +1807,25 @@ class PowerFeed(ChangeLoggedModel, CableTermination, CustomFieldModel): name = models.CharField( max_length=50 ) - status = models.PositiveSmallIntegerField( - choices=POWERFEED_STATUS_CHOICES, - default=POWERFEED_STATUS_ACTIVE + status = models.CharField( + max_length=50, + choices=PowerFeedStatusChoices, + default=PowerFeedStatusChoices.STATUS_ACTIVE ) - type = models.PositiveSmallIntegerField( - choices=POWERFEED_TYPE_CHOICES, - default=POWERFEED_TYPE_PRIMARY + type = models.CharField( + max_length=50, + choices=PowerFeedTypeChoices, + default=PowerFeedTypeChoices.TYPE_PRIMARY ) - supply = models.PositiveSmallIntegerField( - choices=POWERFEED_SUPPLY_CHOICES, - default=POWERFEED_SUPPLY_AC + supply = models.CharField( + max_length=50, + choices=PowerFeedSupplyChoices, + default=PowerFeedSupplyChoices.SUPPLY_AC ) - phase = models.PositiveSmallIntegerField( - choices=POWERFEED_PHASE_CHOICES, - default=POWERFEED_PHASE_SINGLE + phase = models.CharField( + max_length=50, + choices=PowerFeedPhaseChoices, + default=PowerFeedPhaseChoices.PHASE_SINGLE ) voltage = models.PositiveSmallIntegerField( validators=[MinValueValidator(1)], @@ -2863,7 +1840,7 @@ class PowerFeed(ChangeLoggedModel, CableTermination, CustomFieldModel): default=80, help_text="Maximum permissible draw (percentage)" ) - available_power = models.PositiveSmallIntegerField( + available_power = models.PositiveIntegerField( default=0, editable=False ) @@ -2882,6 +1859,22 @@ class PowerFeed(ChangeLoggedModel, CableTermination, CustomFieldModel): 'site', 'panel_name', 'rack_group', 'rack_name', 'name', 'status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization', 'comments', ] + clone_fields = [ + 'power_panel', 'rack', 'status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization', + 'available_power', + ] + + STATUS_CLASS_MAP = { + PowerFeedStatusChoices.STATUS_OFFLINE: 'warning', + PowerFeedStatusChoices.STATUS_ACTIVE: 'success', + PowerFeedStatusChoices.STATUS_PLANNED: 'info', + PowerFeedStatusChoices.STATUS_FAILED: 'danger', + } + + TYPE_CLASS_MAP = { + PowerFeedTypeChoices.TYPE_PRIMARY: 'success', + PowerFeedTypeChoices.TYPE_REDUNDANT: 'info', + } class Meta: ordering = ['power_panel', 'name'] @@ -2922,7 +1915,7 @@ class PowerFeed(ChangeLoggedModel, CableTermination, CustomFieldModel): # Cache the available_power property on the instance kva = self.voltage * self.amperage * (self.max_utilization / 100) - if self.phase == POWERFEED_PHASE_3PHASE: + if self.phase == PowerFeedPhaseChoices.PHASE_3PHASE: self.available_power = round(kva * 1.732) else: self.available_power = round(kva) @@ -2930,10 +1923,10 @@ class PowerFeed(ChangeLoggedModel, CableTermination, CustomFieldModel): super().save(*args, **kwargs) def get_type_class(self): - return STATUS_CLASSES[self.type] + return self.TYPE_CLASS_MAP.get(self.type) def get_status_class(self): - return STATUS_CLASSES[self.status] + return self.STATUS_CLASS_MAP.get(self.status) # @@ -2946,7 +1939,7 @@ class Cable(ChangeLoggedModel): """ termination_a_type = models.ForeignKey( to=ContentType, - limit_choices_to={'model__in': CABLE_TERMINATION_TYPES}, + limit_choices_to=CABLE_TERMINATION_MODELS, on_delete=models.PROTECT, related_name='+' ) @@ -2957,7 +1950,7 @@ class Cable(ChangeLoggedModel): ) termination_b_type = models.ForeignKey( to=ContentType, - limit_choices_to={'model__in': CABLE_TERMINATION_TYPES}, + limit_choices_to=CABLE_TERMINATION_MODELS, on_delete=models.PROTECT, related_name='+' ) @@ -2966,14 +1959,15 @@ class Cable(ChangeLoggedModel): ct_field='termination_b_type', fk_field='termination_b_id' ) - type = models.PositiveSmallIntegerField( - choices=CABLE_TYPE_CHOICES, - blank=True, - null=True + type = models.CharField( + max_length=50, + choices=CableTypeChoices, + blank=True ) - status = models.BooleanField( - choices=CONNECTION_STATUS_CHOICES, - default=CONNECTION_STATUS_CONNECTED + status = models.CharField( + max_length=50, + choices=CableStatusChoices, + default=CableStatusChoices.STATUS_CONNECTED ) label = models.CharField( max_length=100, @@ -2986,10 +1980,10 @@ class Cable(ChangeLoggedModel): blank=True, null=True ) - length_unit = models.PositiveSmallIntegerField( - choices=CABLE_LENGTH_UNIT_CHOICES, + length_unit = models.CharField( + max_length=50, + choices=CableLengthUnitChoices, blank=True, - null=True ) # Stores the normalized length (in meters) for database ordering _abs_length = models.DecimalField( @@ -3020,6 +2014,11 @@ class Cable(ChangeLoggedModel): 'color', 'length', 'length_unit', ] + STATUS_CLASS_MAP = { + CableStatusChoices.STATUS_CONNECTED: 'success', + CableStatusChoices.STATUS_PLANNED: 'info', + } + class Meta: ordering = ['pk'] unique_together = ( @@ -3120,10 +2119,10 @@ class Cable(ChangeLoggedModel): )) # Validate length and length_unit - if self.length is not None and self.length_unit is None: + if self.length is not None and not self.length_unit: raise ValidationError("Must specify a unit when setting a cable length") elif self.length is None: - self.length_unit = None + self.length_unit = '' def save(self, *args, **kwargs): @@ -3159,7 +2158,7 @@ class Cable(ChangeLoggedModel): ) def get_status_class(self): - return 'success' if self.status else 'info' + return self.STATUS_CLASS_MAP.get(self.status) def get_compatible_types(self): """ @@ -3178,12 +2177,12 @@ class Cable(ChangeLoggedModel): b_path = self.termination_a.trace() # Determine overall path status (connected or planned) - if self.status == CONNECTION_STATUS_PLANNED: + if self.status == CableStatusChoices.STATUS_PLANNED: path_status = CONNECTION_STATUS_PLANNED else: path_status = CONNECTION_STATUS_CONNECTED for segment in a_path[1:] + b_path[1:]: - if segment[1] is None or segment[1].status == CONNECTION_STATUS_PLANNED: + if segment[1] is None or segment[1].status == CableStatusChoices.STATUS_PLANNED: path_status = CONNECTION_STATUS_PLANNED break diff --git a/netbox/dcim/models/device_component_templates.py b/netbox/dcim/models/device_component_templates.py new file mode 100644 index 000000000..2aa46d0ea --- /dev/null +++ b/netbox/dcim/models/device_component_templates.py @@ -0,0 +1,400 @@ +from django.core.exceptions import ValidationError +from django.core.validators import MaxValueValidator, MinValueValidator +from django.db import models + +from dcim.choices import * +from dcim.constants import * +from dcim.managers import InterfaceManager +from extras.models import ObjectChange +from utilities.managers import NaturalOrderingManager +from utilities.utils import serialize_object +from .device_components import ( + ConsolePort, ConsoleServerPort, DeviceBay, FrontPort, Interface, PowerOutlet, PowerPort, RearPort, +) + + +__all__ = ( + 'ConsolePortTemplate', + 'ConsoleServerPortTemplate', + 'DeviceBayTemplate', + 'FrontPortTemplate', + 'InterfaceTemplate', + 'PowerOutletTemplate', + 'PowerPortTemplate', + 'RearPortTemplate', +) + + +class ComponentTemplateModel(models.Model): + + class Meta: + abstract = True + + def instantiate(self, device): + """ + Instantiate a new component on the specified Device. + """ + raise NotImplementedError() + + def to_objectchange(self, action): + return ObjectChange( + changed_object=self, + object_repr=str(self), + action=action, + related_object=self.device_type, + object_data=serialize_object(self) + ) + + +class ConsolePortTemplate(ComponentTemplateModel): + """ + A template for a ConsolePort to be created for a new Device. + """ + device_type = models.ForeignKey( + to='dcim.DeviceType', + on_delete=models.CASCADE, + related_name='consoleport_templates' + ) + name = models.CharField( + max_length=50 + ) + type = models.CharField( + max_length=50, + choices=ConsolePortTypeChoices, + blank=True + ) + + objects = NaturalOrderingManager() + + class Meta: + ordering = ['device_type', 'name'] + unique_together = ['device_type', 'name'] + + def __str__(self): + return self.name + + def instantiate(self, device): + return ConsolePort( + device=device, + name=self.name, + type=self.type + ) + + +class ConsoleServerPortTemplate(ComponentTemplateModel): + """ + A template for a ConsoleServerPort to be created for a new Device. + """ + device_type = models.ForeignKey( + to='dcim.DeviceType', + on_delete=models.CASCADE, + related_name='consoleserverport_templates' + ) + name = models.CharField( + max_length=50 + ) + type = models.CharField( + max_length=50, + choices=ConsolePortTypeChoices, + blank=True + ) + + objects = NaturalOrderingManager() + + class Meta: + ordering = ['device_type', 'name'] + unique_together = ['device_type', 'name'] + + def __str__(self): + return self.name + + def instantiate(self, device): + return ConsoleServerPort( + device=device, + name=self.name, + type=self.type + ) + + +class PowerPortTemplate(ComponentTemplateModel): + """ + A template for a PowerPort to be created for a new Device. + """ + device_type = models.ForeignKey( + to='dcim.DeviceType', + on_delete=models.CASCADE, + related_name='powerport_templates' + ) + name = models.CharField( + max_length=50 + ) + type = models.CharField( + max_length=50, + choices=PowerPortTypeChoices, + blank=True + ) + maximum_draw = models.PositiveSmallIntegerField( + blank=True, + null=True, + validators=[MinValueValidator(1)], + help_text="Maximum power draw (watts)" + ) + allocated_draw = models.PositiveSmallIntegerField( + blank=True, + null=True, + validators=[MinValueValidator(1)], + help_text="Allocated power draw (watts)" + ) + + objects = NaturalOrderingManager() + + class Meta: + ordering = ['device_type', 'name'] + unique_together = ['device_type', 'name'] + + def __str__(self): + return self.name + + def instantiate(self, device): + return PowerPort( + device=device, + name=self.name, + maximum_draw=self.maximum_draw, + allocated_draw=self.allocated_draw + ) + + +class PowerOutletTemplate(ComponentTemplateModel): + """ + A template for a PowerOutlet to be created for a new Device. + """ + device_type = models.ForeignKey( + to='dcim.DeviceType', + on_delete=models.CASCADE, + related_name='poweroutlet_templates' + ) + name = models.CharField( + max_length=50 + ) + type = models.CharField( + max_length=50, + choices=PowerOutletTypeChoices, + blank=True + ) + power_port = models.ForeignKey( + to='dcim.PowerPortTemplate', + on_delete=models.SET_NULL, + blank=True, + null=True, + related_name='poweroutlet_templates' + ) + feed_leg = models.CharField( + max_length=50, + choices=PowerOutletFeedLegChoices, + blank=True, + help_text="Phase (for three-phase feeds)" + ) + + objects = NaturalOrderingManager() + + class Meta: + ordering = ['device_type', 'name'] + unique_together = ['device_type', 'name'] + + def __str__(self): + return self.name + + def clean(self): + + # Validate power port assignment + if self.power_port and self.power_port.device_type != self.device_type: + raise ValidationError( + "Parent power port ({}) must belong to the same device type".format(self.power_port) + ) + + def instantiate(self, device): + if self.power_port: + power_port = PowerPort.objects.get(device=device, name=self.power_port.name) + else: + power_port = None + return PowerOutlet( + device=device, + name=self.name, + power_port=power_port, + feed_leg=self.feed_leg + ) + + +class InterfaceTemplate(ComponentTemplateModel): + """ + A template for a physical data interface on a new Device. + """ + device_type = models.ForeignKey( + to='dcim.DeviceType', + on_delete=models.CASCADE, + related_name='interface_templates' + ) + name = models.CharField( + max_length=64 + ) + type = models.CharField( + max_length=50, + choices=InterfaceTypeChoices + ) + mgmt_only = models.BooleanField( + default=False, + verbose_name='Management only' + ) + + objects = InterfaceManager() + + class Meta: + ordering = ['device_type', 'name'] + unique_together = ['device_type', 'name'] + + def __str__(self): + return self.name + + def instantiate(self, device): + return Interface( + device=device, + name=self.name, + type=self.type, + mgmt_only=self.mgmt_only + ) + + +class FrontPortTemplate(ComponentTemplateModel): + """ + Template for a pass-through port on the front of a new Device. + """ + device_type = models.ForeignKey( + to='dcim.DeviceType', + on_delete=models.CASCADE, + related_name='frontport_templates' + ) + name = models.CharField( + max_length=64 + ) + type = models.CharField( + max_length=50, + choices=PortTypeChoices + ) + rear_port = models.ForeignKey( + to='dcim.RearPortTemplate', + on_delete=models.CASCADE, + related_name='frontport_templates' + ) + rear_port_position = models.PositiveSmallIntegerField( + default=1, + validators=[MinValueValidator(1), MaxValueValidator(64)] + ) + + objects = NaturalOrderingManager() + + class Meta: + ordering = ['device_type', 'name'] + unique_together = [ + ['device_type', 'name'], + ['rear_port', 'rear_port_position'], + ] + + def __str__(self): + return self.name + + def clean(self): + + # Validate rear port assignment + if self.rear_port.device_type != self.device_type: + raise ValidationError( + "Rear port ({}) must belong to the same device type".format(self.rear_port) + ) + + # Validate rear port position assignment + if self.rear_port_position > self.rear_port.positions: + raise ValidationError( + "Invalid rear port position ({}); rear port {} has only {} positions".format( + self.rear_port_position, self.rear_port.name, self.rear_port.positions + ) + ) + + def instantiate(self, device): + if self.rear_port: + rear_port = RearPort.objects.get(device=device, name=self.rear_port.name) + else: + rear_port = None + return FrontPort( + device=device, + name=self.name, + type=self.type, + rear_port=rear_port, + rear_port_position=self.rear_port_position + ) + + +class RearPortTemplate(ComponentTemplateModel): + """ + Template for a pass-through port on the rear of a new Device. + """ + device_type = models.ForeignKey( + to='dcim.DeviceType', + on_delete=models.CASCADE, + related_name='rearport_templates' + ) + name = models.CharField( + max_length=64 + ) + type = models.CharField( + max_length=50, + choices=PortTypeChoices + ) + positions = models.PositiveSmallIntegerField( + default=1, + validators=[MinValueValidator(1), MaxValueValidator(64)] + ) + + objects = NaturalOrderingManager() + + class Meta: + ordering = ['device_type', 'name'] + unique_together = ['device_type', 'name'] + + def __str__(self): + return self.name + + def instantiate(self, device): + return RearPort( + device=device, + name=self.name, + type=self.type, + positions=self.positions + ) + + +class DeviceBayTemplate(ComponentTemplateModel): + """ + A template for a DeviceBay to be created for a new parent Device. + """ + device_type = models.ForeignKey( + to='dcim.DeviceType', + on_delete=models.CASCADE, + related_name='device_bay_templates' + ) + name = models.CharField( + max_length=50 + ) + + objects = NaturalOrderingManager() + + class Meta: + ordering = ['device_type', 'name'] + unique_together = ['device_type', 'name'] + + def __str__(self): + return self.name + + def instantiate(self, device): + return DeviceBay( + device=device, + name=self.name + ) diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py new file mode 100644 index 000000000..68bab8037 --- /dev/null +++ b/netbox/dcim/models/device_components.py @@ -0,0 +1,1019 @@ +from django.contrib.contenttypes.fields import GenericRelation +from django.core.exceptions import ObjectDoesNotExist, ValidationError +from django.core.validators import MaxValueValidator, MinValueValidator +from django.db import models +from django.db.models import Sum +from django.urls import reverse +from taggit.managers import TaggableManager + +from dcim.choices import * +from dcim.constants import * +from dcim.exceptions import LoopDetected +from dcim.fields import MACAddressField +from dcim.managers import InterfaceManager +from extras.models import ObjectChange, TaggedItem +from utilities.managers import NaturalOrderingManager +from utilities.utils import serialize_object +from virtualization.choices import VMInterfaceTypeChoices + + +__all__ = ( + 'CableTermination', + 'ConsolePort', + 'ConsoleServerPort', + 'DeviceBay', + 'FrontPort', + 'Interface', + 'InventoryItem', + 'PowerOutlet', + 'PowerPort', + 'RearPort', +) + + +class ComponentModel(models.Model): + description = models.CharField( + max_length=100, + blank=True + ) + + class Meta: + abstract = True + + def to_objectchange(self, action): + # Annotate the parent Device/VM + try: + parent = getattr(self, 'device', None) or getattr(self, 'virtual_machine', None) + except ObjectDoesNotExist: + # The parent device/VM has already been deleted + parent = None + + return ObjectChange( + changed_object=self, + object_repr=str(self), + action=action, + related_object=parent, + object_data=serialize_object(self) + ) + + @property + def parent(self): + return getattr(self, 'device', None) + + +class CableTermination(models.Model): + cable = models.ForeignKey( + to='dcim.Cable', + on_delete=models.SET_NULL, + related_name='+', + blank=True, + null=True + ) + + # Generic relations to Cable. These ensure that an attached Cable is deleted if the terminated object is deleted. + _cabled_as_a = GenericRelation( + to='dcim.Cable', + content_type_field='termination_a_type', + object_id_field='termination_a_id' + ) + _cabled_as_b = GenericRelation( + to='dcim.Cable', + content_type_field='termination_b_type', + object_id_field='termination_b_id' + ) + + is_path_endpoint = True + + class Meta: + abstract = True + + def trace(self, position=1, follow_circuits=False, cable_history=None): + """ + Return a list representing a complete cable path, with each individual segment represented as a three-tuple: + [ + (termination A, cable, termination B), + (termination C, cable, termination D), + (termination E, cable, termination F) + ] + """ + def get_peer_port(termination, position=1, follow_circuits=False): + from circuits.models import CircuitTermination + + # Map a front port to its corresponding rear port + if isinstance(termination, FrontPort): + return termination.rear_port, termination.rear_port_position + + # Map a rear port/position to its corresponding front port + elif isinstance(termination, RearPort): + if position not in range(1, termination.positions + 1): + raise Exception("Invalid position for {} ({} positions): {})".format( + termination, termination.positions, position + )) + try: + peer_port = FrontPort.objects.get( + rear_port=termination, + rear_port_position=position, + ) + return peer_port, 1 + except ObjectDoesNotExist: + return None, None + + # Follow a circuit to its other termination + elif isinstance(termination, CircuitTermination) and follow_circuits: + peer_termination = termination.get_peer_termination() + if peer_termination is None: + return None, None + return peer_termination, position + + # Termination is not a pass-through port + else: + return None, None + + if not self.cable: + return [(self, None, None)] + + # Record cable history to detect loops + if cable_history is None: + cable_history = [] + elif self.cable in cable_history: + raise LoopDetected() + cable_history.append(self.cable) + + far_end = self.cable.termination_b if self.cable.termination_a == self else self.cable.termination_a + path = [(self, self.cable, far_end)] + + peer_port, position = get_peer_port(far_end, position, follow_circuits) + if peer_port is None: + return path + + try: + next_segment = peer_port.trace(position, follow_circuits, cable_history) + except LoopDetected: + return path + + if next_segment is None: + return path + [(peer_port, None, None)] + + return path + next_segment + + def get_cable_peer(self): + if self.cable is None: + return None + if self._cabled_as_a.exists(): + return self.cable.termination_b + if self._cabled_as_b.exists(): + return self.cable.termination_a + + +# +# Console ports +# + +class ConsolePort(CableTermination, ComponentModel): + """ + A physical console port within a Device. ConsolePorts connect to ConsoleServerPorts. + """ + device = models.ForeignKey( + to='dcim.Device', + on_delete=models.CASCADE, + related_name='consoleports' + ) + name = models.CharField( + max_length=50 + ) + type = models.CharField( + max_length=50, + choices=ConsolePortTypeChoices, + blank=True + ) + connected_endpoint = models.OneToOneField( + to='dcim.ConsoleServerPort', + on_delete=models.SET_NULL, + related_name='connected_endpoint', + blank=True, + null=True + ) + connection_status = models.NullBooleanField( + choices=CONNECTION_STATUS_CHOICES, + blank=True + ) + + objects = NaturalOrderingManager() + tags = TaggableManager(through=TaggedItem) + + csv_headers = ['device', 'name', 'type', 'description'] + + class Meta: + ordering = ['device', 'name'] + unique_together = ['device', 'name'] + + def __str__(self): + return self.name + + def get_absolute_url(self): + return self.device.get_absolute_url() + + def to_csv(self): + return ( + self.device.identifier, + self.name, + self.type, + self.description, + ) + + +# +# Console server ports +# + +class ConsoleServerPort(CableTermination, ComponentModel): + """ + A physical port within a Device (typically a designated console server) which provides access to ConsolePorts. + """ + device = models.ForeignKey( + to='dcim.Device', + on_delete=models.CASCADE, + related_name='consoleserverports' + ) + name = models.CharField( + max_length=50 + ) + type = models.CharField( + max_length=50, + choices=ConsolePortTypeChoices, + blank=True + ) + connection_status = models.NullBooleanField( + choices=CONNECTION_STATUS_CHOICES, + blank=True + ) + + objects = NaturalOrderingManager() + tags = TaggableManager(through=TaggedItem) + + csv_headers = ['device', 'name', 'type', 'description'] + + class Meta: + unique_together = ['device', 'name'] + + def __str__(self): + return self.name + + def get_absolute_url(self): + return self.device.get_absolute_url() + + def to_csv(self): + return ( + self.device.identifier, + self.name, + self.type, + self.description, + ) + + +# +# Power ports +# + +class PowerPort(CableTermination, ComponentModel): + """ + A physical power supply (intake) port within a Device. PowerPorts connect to PowerOutlets. + """ + device = models.ForeignKey( + to='dcim.Device', + on_delete=models.CASCADE, + related_name='powerports' + ) + name = models.CharField( + max_length=50 + ) + type = models.CharField( + max_length=50, + choices=PowerPortTypeChoices, + blank=True + ) + maximum_draw = models.PositiveSmallIntegerField( + blank=True, + null=True, + validators=[MinValueValidator(1)], + help_text="Maximum power draw (watts)" + ) + allocated_draw = models.PositiveSmallIntegerField( + blank=True, + null=True, + validators=[MinValueValidator(1)], + help_text="Allocated power draw (watts)" + ) + _connected_poweroutlet = models.OneToOneField( + to='dcim.PowerOutlet', + on_delete=models.SET_NULL, + related_name='connected_endpoint', + blank=True, + null=True + ) + _connected_powerfeed = models.OneToOneField( + to='dcim.PowerFeed', + on_delete=models.SET_NULL, + related_name='+', + blank=True, + null=True + ) + connection_status = models.NullBooleanField( + choices=CONNECTION_STATUS_CHOICES, + blank=True + ) + + objects = NaturalOrderingManager() + tags = TaggableManager(through=TaggedItem) + + csv_headers = ['device', 'name', 'type', 'maximum_draw', 'allocated_draw', 'description'] + + class Meta: + ordering = ['device', 'name'] + unique_together = ['device', 'name'] + + def __str__(self): + return self.name + + def get_absolute_url(self): + return self.device.get_absolute_url() + + def to_csv(self): + return ( + self.device.identifier, + self.name, + self.get_type_display(), + self.maximum_draw, + self.allocated_draw, + self.description, + ) + + @property + def connected_endpoint(self): + if self._connected_poweroutlet: + return self._connected_poweroutlet + return self._connected_powerfeed + + @connected_endpoint.setter + def connected_endpoint(self, value): + # TODO: Fix circular import + from . import PowerFeed + + if value is None: + self._connected_poweroutlet = None + self._connected_powerfeed = None + elif isinstance(value, PowerOutlet): + self._connected_poweroutlet = value + self._connected_powerfeed = None + elif isinstance(value, PowerFeed): + self._connected_poweroutlet = None + self._connected_powerfeed = value + else: + raise ValueError( + "Connected endpoint must be a PowerOutlet or PowerFeed, not {}.".format(type(value)) + ) + + def get_power_draw(self): + """ + Return the allocated and maximum power draw (in VA) and child PowerOutlet count for this PowerPort. + """ + # Calculate aggregate draw of all child power outlets if no numbers have been defined manually + if self.allocated_draw is None and self.maximum_draw is None: + outlet_ids = PowerOutlet.objects.filter(power_port=self).values_list('pk', flat=True) + utilization = PowerPort.objects.filter(_connected_poweroutlet_id__in=outlet_ids).aggregate( + maximum_draw_total=Sum('maximum_draw'), + allocated_draw_total=Sum('allocated_draw'), + ) + ret = { + 'allocated': utilization['allocated_draw_total'] or 0, + 'maximum': utilization['maximum_draw_total'] or 0, + 'outlet_count': len(outlet_ids), + 'legs': [], + } + + # Calculate per-leg aggregates for three-phase feeds + if self._connected_powerfeed and self._connected_powerfeed.phase == PowerFeedPhaseChoices.PHASE_3PHASE: + for leg, leg_name in PowerOutletFeedLegChoices: + outlet_ids = PowerOutlet.objects.filter(power_port=self, feed_leg=leg).values_list('pk', flat=True) + utilization = PowerPort.objects.filter(_connected_poweroutlet_id__in=outlet_ids).aggregate( + maximum_draw_total=Sum('maximum_draw'), + allocated_draw_total=Sum('allocated_draw'), + ) + ret['legs'].append({ + 'name': leg_name, + 'allocated': utilization['allocated_draw_total'] or 0, + 'maximum': utilization['maximum_draw_total'] or 0, + 'outlet_count': len(outlet_ids), + }) + + return ret + + # Default to administratively defined values + return { + 'allocated': self.allocated_draw or 0, + 'maximum': self.maximum_draw or 0, + 'outlet_count': PowerOutlet.objects.filter(power_port=self).count(), + 'legs': [], + } + + +# +# Power outlets +# + +class PowerOutlet(CableTermination, ComponentModel): + """ + A physical power outlet (output) within a Device which provides power to a PowerPort. + """ + device = models.ForeignKey( + to='dcim.Device', + on_delete=models.CASCADE, + related_name='poweroutlets' + ) + name = models.CharField( + max_length=50 + ) + type = models.CharField( + max_length=50, + choices=PowerOutletTypeChoices, + blank=True + ) + power_port = models.ForeignKey( + to='dcim.PowerPort', + on_delete=models.SET_NULL, + blank=True, + null=True, + related_name='poweroutlets' + ) + feed_leg = models.CharField( + max_length=50, + choices=PowerOutletFeedLegChoices, + blank=True, + help_text="Phase (for three-phase feeds)" + ) + connection_status = models.NullBooleanField( + choices=CONNECTION_STATUS_CHOICES, + blank=True + ) + + objects = NaturalOrderingManager() + tags = TaggableManager(through=TaggedItem) + + csv_headers = ['device', 'name', 'type', 'power_port', 'feed_leg', 'description'] + + class Meta: + unique_together = ['device', 'name'] + + def __str__(self): + return self.name + + def get_absolute_url(self): + return self.device.get_absolute_url() + + def to_csv(self): + return ( + self.device.identifier, + self.name, + self.get_type_display(), + self.power_port.name if self.power_port else None, + self.get_feed_leg_display(), + self.description, + ) + + def clean(self): + + # Validate power port assignment + if self.power_port and self.power_port.device != self.device: + raise ValidationError( + "Parent power port ({}) must belong to the same device".format(self.power_port) + ) + + +# +# Interfaces +# + +class Interface(CableTermination, ComponentModel): + """ + A network interface within a Device or VirtualMachine. A physical Interface can connect to exactly one other + Interface. + """ + device = models.ForeignKey( + to='Device', + on_delete=models.CASCADE, + related_name='interfaces', + null=True, + blank=True + ) + virtual_machine = models.ForeignKey( + to='virtualization.VirtualMachine', + on_delete=models.CASCADE, + related_name='interfaces', + null=True, + blank=True + ) + name = models.CharField( + max_length=64 + ) + _connected_interface = models.OneToOneField( + to='self', + on_delete=models.SET_NULL, + related_name='+', + blank=True, + null=True + ) + _connected_circuittermination = models.OneToOneField( + to='circuits.CircuitTermination', + on_delete=models.SET_NULL, + related_name='+', + blank=True, + null=True + ) + connection_status = models.NullBooleanField( + choices=CONNECTION_STATUS_CHOICES, + blank=True + ) + lag = models.ForeignKey( + to='self', + on_delete=models.SET_NULL, + related_name='member_interfaces', + null=True, + blank=True, + verbose_name='Parent LAG' + ) + type = models.CharField( + max_length=50, + choices=InterfaceTypeChoices + ) + enabled = models.BooleanField( + default=True + ) + mac_address = MACAddressField( + null=True, + blank=True, + verbose_name='MAC Address' + ) + mtu = models.PositiveIntegerField( + blank=True, + null=True, + validators=[MinValueValidator(1), MaxValueValidator(65536)], + verbose_name='MTU' + ) + mgmt_only = models.BooleanField( + default=False, + verbose_name='OOB Management', + help_text='This interface is used only for out-of-band management' + ) + mode = models.CharField( + max_length=50, + choices=InterfaceModeChoices, + blank=True, + ) + untagged_vlan = models.ForeignKey( + to='ipam.VLAN', + on_delete=models.SET_NULL, + related_name='interfaces_as_untagged', + null=True, + blank=True, + verbose_name='Untagged VLAN' + ) + tagged_vlans = models.ManyToManyField( + to='ipam.VLAN', + related_name='interfaces_as_tagged', + blank=True, + verbose_name='Tagged VLANs' + ) + + objects = InterfaceManager() + tags = TaggableManager(through=TaggedItem) + + csv_headers = [ + 'device', 'virtual_machine', 'name', 'lag', 'type', 'enabled', 'mac_address', 'mtu', 'mgmt_only', + 'description', 'mode', + ] + + class Meta: + ordering = ['device', 'name'] + unique_together = ['device', 'name'] + + def __str__(self): + return self.name + + def get_absolute_url(self): + return reverse('dcim:interface', kwargs={'pk': self.pk}) + + def to_csv(self): + return ( + self.device.identifier if self.device else None, + self.virtual_machine.name if self.virtual_machine else None, + self.name, + self.lag.name if self.lag else None, + self.get_type_display(), + self.enabled, + self.mac_address, + self.mtu, + self.mgmt_only, + self.description, + self.get_mode_display(), + ) + + def clean(self): + + # An Interface must belong to a Device *or* to a VirtualMachine + if self.device and self.virtual_machine: + raise ValidationError("An interface cannot belong to both a device and a virtual machine.") + if not self.device and not self.virtual_machine: + raise ValidationError("An interface must belong to either a device or a virtual machine.") + + # VM interfaces must be virtual + if self.virtual_machine and self.type not in VMInterfaceTypeChoices.values(): + raise ValidationError({ + 'type': "Invalid interface type for a virtual machine: {}".format(self.type) + }) + + # Virtual interfaces cannot be connected + if self.type in NONCONNECTABLE_IFACE_TYPES and ( + self.cable or getattr(self, 'circuit_termination', False) + ): + raise ValidationError({ + 'type': "Virtual and wireless interfaces cannot be connected to another interface or circuit. " + "Disconnect the interface or choose a suitable type." + }) + + # An interface's LAG must belong to the same device (or VC master) + if self.lag and self.lag.device not in [self.device, self.device.get_vc_master()]: + raise ValidationError({ + 'lag': "The selected LAG interface ({}) belongs to a different device ({}).".format( + self.lag.name, self.lag.device.name + ) + }) + + # A virtual interface cannot have a parent LAG + if self.type in NONCONNECTABLE_IFACE_TYPES and self.lag is not None: + raise ValidationError({ + 'lag': "{} interfaces cannot have a parent LAG interface.".format(self.get_type_display()) + }) + + # Only a LAG can have LAG members + if self.type != InterfaceTypeChoices.TYPE_LAG and self.member_interfaces.exists(): + raise ValidationError({ + 'type': "Cannot change interface type; it has LAG members ({}).".format( + ", ".join([iface.name for iface in self.member_interfaces.all()]) + ) + }) + + # Validate untagged VLAN + if self.untagged_vlan and self.untagged_vlan.site not in [self.parent.site, None]: + raise ValidationError({ + 'untagged_vlan': "The untagged VLAN ({}) must belong to the same site as the interface's parent " + "device/VM, or it must be global".format(self.untagged_vlan) + }) + + def save(self, *args, **kwargs): + + # Remove untagged VLAN assignment for non-802.1Q interfaces + if self.mode is None: + self.untagged_vlan = None + + # Only "tagged" interfaces may have tagged VLANs assigned. ("tagged all" implies all VLANs are assigned.) + if self.pk and self.mode is not InterfaceModeChoices.MODE_TAGGED: + self.tagged_vlans.clear() + + return super().save(*args, **kwargs) + + def to_objectchange(self, action): + # Annotate the parent Device/VM + try: + parent_obj = self.device or self.virtual_machine + except ObjectDoesNotExist: + parent_obj = None + + return ObjectChange( + changed_object=self, + object_repr=str(self), + action=action, + related_object=parent_obj, + object_data=serialize_object(self) + ) + + @property + def connected_endpoint(self): + if self._connected_interface: + return self._connected_interface + return self._connected_circuittermination + + @connected_endpoint.setter + def connected_endpoint(self, value): + from circuits.models import CircuitTermination + + if value is None: + self._connected_interface = None + self._connected_circuittermination = None + elif isinstance(value, Interface): + self._connected_interface = value + self._connected_circuittermination = None + elif isinstance(value, CircuitTermination): + self._connected_interface = None + self._connected_circuittermination = value + else: + raise ValueError( + "Connected endpoint must be an Interface or CircuitTermination, not {}.".format(type(value)) + ) + + @property + def parent(self): + return self.device or self.virtual_machine + + @property + def is_connectable(self): + return self.type not in NONCONNECTABLE_IFACE_TYPES + + @property + def is_virtual(self): + return self.type in VIRTUAL_IFACE_TYPES + + @property + def is_wireless(self): + return self.type in WIRELESS_IFACE_TYPES + + @property + def is_lag(self): + return self.type == InterfaceTypeChoices.TYPE_LAG + + @property + def count_ipaddresses(self): + return self.ip_addresses.count() + + +# +# Pass-through ports +# + +class FrontPort(CableTermination, ComponentModel): + """ + A pass-through port on the front of a Device. + """ + device = models.ForeignKey( + to='dcim.Device', + on_delete=models.CASCADE, + related_name='frontports' + ) + name = models.CharField( + max_length=64 + ) + type = models.CharField( + max_length=50, + choices=PortTypeChoices + ) + rear_port = models.ForeignKey( + to='dcim.RearPort', + on_delete=models.CASCADE, + related_name='frontports' + ) + rear_port_position = models.PositiveSmallIntegerField( + default=1, + validators=[MinValueValidator(1), MaxValueValidator(64)] + ) + + is_path_endpoint = False + + objects = NaturalOrderingManager() + tags = TaggableManager(through=TaggedItem) + + csv_headers = ['device', 'name', 'type', 'rear_port', 'rear_port_position', 'description'] + + class Meta: + ordering = ['device', 'name'] + unique_together = [ + ['device', 'name'], + ['rear_port', 'rear_port_position'], + ] + + def __str__(self): + return self.name + + def to_csv(self): + return ( + self.device.identifier, + self.name, + self.get_type_display(), + self.rear_port.name, + self.rear_port_position, + self.description, + ) + + def clean(self): + + # Validate rear port assignment + if self.rear_port.device != self.device: + raise ValidationError( + "Rear port ({}) must belong to the same device".format(self.rear_port) + ) + + # Validate rear port position assignment + if self.rear_port_position > self.rear_port.positions: + raise ValidationError( + "Invalid rear port position ({}); rear port {} has only {} positions".format( + self.rear_port_position, self.rear_port.name, self.rear_port.positions + ) + ) + + +class RearPort(CableTermination, ComponentModel): + """ + A pass-through port on the rear of a Device. + """ + device = models.ForeignKey( + to='dcim.Device', + on_delete=models.CASCADE, + related_name='rearports' + ) + name = models.CharField( + max_length=64 + ) + type = models.CharField( + max_length=50, + choices=PortTypeChoices + ) + positions = models.PositiveSmallIntegerField( + default=1, + validators=[MinValueValidator(1), MaxValueValidator(64)] + ) + + is_path_endpoint = False + + objects = NaturalOrderingManager() + tags = TaggableManager(through=TaggedItem) + + csv_headers = ['device', 'name', 'type', 'positions', 'description'] + + class Meta: + ordering = ['device', 'name'] + unique_together = ['device', 'name'] + + def __str__(self): + return self.name + + def to_csv(self): + return ( + self.device.identifier, + self.name, + self.get_type_display(), + self.positions, + self.description, + ) + + +# +# Device bays +# + +class DeviceBay(ComponentModel): + """ + An empty space within a Device which can house a child device + """ + device = models.ForeignKey( + to='dcim.Device', + on_delete=models.CASCADE, + related_name='device_bays' + ) + name = models.CharField( + max_length=50, + verbose_name='Name' + ) + installed_device = models.OneToOneField( + to='dcim.Device', + on_delete=models.SET_NULL, + related_name='parent_bay', + blank=True, + null=True + ) + + objects = NaturalOrderingManager() + tags = TaggableManager(through=TaggedItem) + + csv_headers = ['device', 'name', 'installed_device', 'description'] + + class Meta: + ordering = ['device', 'name'] + unique_together = ['device', 'name'] + + def __str__(self): + return '{} - {}'.format(self.device.name, self.name) + + def get_absolute_url(self): + return self.device.get_absolute_url() + + def to_csv(self): + return ( + self.device.identifier, + self.name, + self.installed_device.identifier if self.installed_device else None, + self.description, + ) + + def clean(self): + + # Validate that the parent Device can have DeviceBays + if not self.device.device_type.is_parent_device: + raise ValidationError("This type of device ({}) does not support device bays.".format( + self.device.device_type + )) + + # Cannot install a device into itself, obviously + if self.device == self.installed_device: + raise ValidationError("Cannot install a device into itself.") + + # Check that the installed device is not already installed elsewhere + if self.installed_device: + current_bay = DeviceBay.objects.filter(installed_device=self.installed_device).first() + if current_bay and current_bay != self: + raise ValidationError({ + 'installed_device': "Cannot install the specified device; device is already installed in {}".format( + current_bay + ) + }) + + +# +# Inventory items +# + +class InventoryItem(ComponentModel): + """ + An InventoryItem represents a serialized piece of hardware within a Device, such as a line card or power supply. + InventoryItems are used only for inventory purposes. + """ + device = models.ForeignKey( + to='dcim.Device', + on_delete=models.CASCADE, + related_name='inventory_items' + ) + parent = models.ForeignKey( + to='self', + on_delete=models.CASCADE, + related_name='child_items', + blank=True, + null=True + ) + name = models.CharField( + max_length=50, + verbose_name='Name' + ) + manufacturer = models.ForeignKey( + to='dcim.Manufacturer', + on_delete=models.PROTECT, + related_name='inventory_items', + blank=True, + null=True + ) + part_id = models.CharField( + max_length=50, + verbose_name='Part ID', + blank=True + ) + serial = models.CharField( + max_length=50, + verbose_name='Serial number', + blank=True + ) + asset_tag = models.CharField( + max_length=50, + unique=True, + blank=True, + null=True, + verbose_name='Asset tag', + help_text='A unique tag used to identify this item' + ) + discovered = models.BooleanField( + default=False, + verbose_name='Discovered' + ) + + tags = TaggableManager(through=TaggedItem) + + csv_headers = [ + 'device', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered', 'description', + ] + + class Meta: + ordering = ['device__id', 'parent__id', 'name'] + unique_together = ['device', 'parent', 'name'] + + def __str__(self): + return self.name + + def get_absolute_url(self): + return self.device.get_absolute_url() + + def to_csv(self): + return ( + self.device.name or '{{{}}}'.format(self.device.pk), + self.name, + self.manufacturer.name if self.manufacturer else None, + self.part_id, + self.serial, + self.asset_tag, + self.discovered, + self.description, + ) diff --git a/netbox/dcim/tables.py b/netbox/dcim/tables.py index 9b3b405aa..7e1da41d4 100644 --- a/netbox/dcim/tables.py +++ b/netbox/dcim/tables.py @@ -156,10 +156,6 @@ DEVICE_PRIMARY_IP = """ {{ record.primary_ip4.address.ip|default:"" }} """ -SUBDEVICE_ROLE_TEMPLATE = """ -{% if record.subdevice_role == True %}Parent{% elif record.subdevice_role == False %}Child{% else %}—{% endif %} -""" - DEVICETYPE_INSTANCES_TEMPLATE = """ {{ record.instance_count }} """ @@ -276,16 +272,17 @@ class RackGroupTable(BaseTable): class RackRoleTable(BaseTable): pk = ToggleColumn() - name = tables.LinkColumn(verbose_name='Name') rack_count = tables.Column(verbose_name='Racks') - color = tables.TemplateColumn(COLOR_LABEL, verbose_name='Color') - slug = tables.Column(verbose_name='Slug') - actions = tables.TemplateColumn(template_code=RACKROLE_ACTIONS, attrs={'td': {'class': 'text-right noprint'}}, - verbose_name='') + color = tables.TemplateColumn(COLOR_LABEL) + actions = tables.TemplateColumn( + template_code=RACKROLE_ACTIONS, + attrs={'td': {'class': 'text-right noprint'}}, + verbose_name='' + ) class Meta(BaseTable.Meta): model = RackRole - fields = ('pk', 'name', 'rack_count', 'color', 'slug', 'actions') + fields = ('pk', 'name', 'rack_count', 'color', 'description', 'slug', 'actions') # @@ -393,10 +390,6 @@ class DeviceTypeTable(BaseTable): verbose_name='Device Type' ) 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( template_code=DEVICETYPE_INSTANCES_TEMPLATE, verbose_name='Instances' @@ -424,10 +417,19 @@ class ConsolePortTemplateTable(BaseTable): class Meta(BaseTable.Meta): model = ConsolePortTemplate - fields = ('pk', 'name', 'actions') + fields = ('pk', 'name', 'type', 'actions') 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): pk = ToggleColumn() actions = tables.TemplateColumn( @@ -442,6 +444,15 @@ class ConsoleServerPortTemplateTable(BaseTable): 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): pk = ToggleColumn() actions = tables.TemplateColumn( @@ -452,10 +463,19 @@ class PowerPortTemplateTable(BaseTable): class Meta(BaseTable.Meta): model = PowerPortTemplate - fields = ('pk', 'name', 'maximum_draw', 'allocated_draw', 'actions') + fields = ('pk', 'name', 'type', 'maximum_draw', 'allocated_draw', 'actions') empty_text = "None" +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): pk = ToggleColumn() actions = tables.TemplateColumn( @@ -466,10 +486,19 @@ class PowerOutletTemplateTable(BaseTable): class Meta(BaseTable.Meta): model = PowerOutletTemplate - fields = ('pk', 'name', 'power_port', 'feed_leg', 'actions') + fields = ('pk', 'name', 'type', 'power_port', 'feed_leg', 'actions') empty_text = "None" +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): pk = ToggleColumn() mgmt_only = tables.TemplateColumn("{% if value %}OOB Management{% endif %}") @@ -485,6 +514,16 @@ class InterfaceTemplateTable(BaseTable): 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): pk = ToggleColumn() rear_port_position = tables.Column( @@ -502,6 +541,15 @@ class FrontPortTemplateTable(BaseTable): 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): pk = ToggleColumn() actions = tables.TemplateColumn( @@ -516,6 +564,15 @@ class RearPortTemplateTable(BaseTable): 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): pk = ToggleColumn() actions = tables.TemplateColumn( @@ -558,7 +615,7 @@ class DeviceRoleTable(BaseTable): class Meta(BaseTable.Meta): model = DeviceRole - fields = ('pk', 'name', 'device_count', 'vm_count', 'color', 'vm_role', 'slug', 'actions') + fields = ('pk', 'name', 'device_count', 'vm_count', 'color', 'vm_role', 'description', 'slug', 'actions') # @@ -645,11 +702,28 @@ class DeviceImportTable(BaseTable): # 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 Meta(BaseTable.Meta): model = ConsolePort - fields = ('name',) + fields = ('name', 'type') + + +class ConsolePortDetailTable(DeviceComponentDetailTable): + device = tables.LinkColumn() + + class Meta(DeviceComponentDetailTable.Meta, ConsolePortTable.Meta): + pass class ConsoleServerPortTable(BaseTable): @@ -659,18 +733,39 @@ class ConsoleServerPortTable(BaseTable): fields = ('name', 'description') +class ConsoleServerPortDetailTable(DeviceComponentDetailTable): + device = tables.LinkColumn() + + class Meta(DeviceComponentDetailTable.Meta, ConsoleServerPortTable.Meta): + pass + + class PowerPortTable(BaseTable): class Meta(BaseTable.Meta): model = PowerPort - fields = ('name',) + fields = ('name', 'type') + + +class PowerPortDetailTable(DeviceComponentDetailTable): + device = tables.LinkColumn() + + class Meta(DeviceComponentDetailTable.Meta, PowerPortTable.Meta): + pass class PowerOutletTable(BaseTable): class Meta(BaseTable.Meta): model = PowerOutlet - fields = ('name', 'description') + fields = ('name', 'type', 'description') + + +class PowerOutletDetailTable(DeviceComponentDetailTable): + device = tables.LinkColumn() + + class Meta(DeviceComponentDetailTable.Meta, PowerOutletTable.Meta): + pass class InterfaceTable(BaseTable): @@ -680,6 +775,15 @@ class InterfaceTable(BaseTable): 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 Meta(BaseTable.Meta): @@ -688,6 +792,13 @@ class FrontPortTable(BaseTable): empty_text = "None" +class FrontPortDetailTable(DeviceComponentDetailTable): + device = tables.LinkColumn() + + class Meta(DeviceComponentDetailTable.Meta, FrontPortTable.Meta): + pass + + class RearPortTable(BaseTable): class Meta(BaseTable.Meta): @@ -696,6 +807,13 @@ class RearPortTable(BaseTable): empty_text = "None" +class RearPortDetailTable(DeviceComponentDetailTable): + device = tables.LinkColumn() + + class Meta(DeviceComponentDetailTable.Meta, RearPortTable.Meta): + pass + + class DeviceBayTable(BaseTable): class Meta(BaseTable.Meta): @@ -703,6 +821,26 @@ class DeviceBayTable(BaseTable): 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 # diff --git a/netbox/dcim/tests/test_api.py b/netbox/dcim/tests/test_api.py index 9c873c886..a515df13c 100644 --- a/netbox/dcim/tests/test_api.py +++ b/netbox/dcim/tests/test_api.py @@ -1,8 +1,10 @@ +from django.contrib.contenttypes.models import ContentType from django.urls import reverse from netaddr import IPNetwork from rest_framework import status from circuits.models import Circuit, CircuitTermination, CircuitType, Provider +from dcim.choices import * from dcim.constants import * from dcim.models import ( Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, @@ -11,11 +13,94 @@ from dcim.models import ( Rack, RackGroup, RackReservation, RackRole, RearPort, Region, Site, VirtualChassis, ) from ipam.models import IPAddress, VLAN -from extras.models import Graph, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE -from utilities.testing import APITestCase +from extras.models import Graph +from utilities.testing import APITestCase, choices_to_dict from virtualization.models import Cluster, ClusterType +class AppTest(APITestCase): + + def test_root(self): + + url = reverse('dcim-api:api-root') + response = self.client.get('{}?format=api'.format(url), **self.header) + + self.assertEqual(response.status_code, 200) + + def test_choices(self): + + url = reverse('dcim-api:field-choice-list') + response = self.client.get(url, **self.header) + + self.assertEqual(response.status_code, 200) + + # Cable + self.assertEqual(choices_to_dict(response.data.get('cable:length_unit')), CableLengthUnitChoices.as_dict()) + self.assertEqual(choices_to_dict(response.data.get('cable:status')), CableStatusChoices.as_dict()) + content_types = ContentType.objects.filter(CABLE_TERMINATION_MODELS) + cable_termination_choices = { + "{}.{}".format(ct.app_label, ct.model): ct.name for ct in content_types + } + self.assertEqual(choices_to_dict(response.data.get('cable:termination_a_type')), cable_termination_choices) + self.assertEqual(choices_to_dict(response.data.get('cable:termination_b_type')), cable_termination_choices) + self.assertEqual(choices_to_dict(response.data.get('cable:type')), CableTypeChoices.as_dict()) + + # Console ports + self.assertEqual(choices_to_dict(response.data.get('console-port:type')), ConsolePortTypeChoices.as_dict()) + self.assertEqual(choices_to_dict(response.data.get('console-port:connection_status')), dict(CONNECTION_STATUS_CHOICES)) + self.assertEqual(choices_to_dict(response.data.get('console-port-template:type')), ConsolePortTypeChoices.as_dict()) + + # Console server ports + self.assertEqual(choices_to_dict(response.data.get('console-server-port:type')), ConsolePortTypeChoices.as_dict()) + self.assertEqual(choices_to_dict(response.data.get('console-server-port-template:type')), ConsolePortTypeChoices.as_dict()) + + # Device + self.assertEqual(choices_to_dict(response.data.get('device:face')), DeviceFaceChoices.as_dict()) + self.assertEqual(choices_to_dict(response.data.get('device:status')), DeviceStatusChoices.as_dict()) + + # Device type + self.assertEqual(choices_to_dict(response.data.get('device-type:subdevice_role')), SubdeviceRoleChoices.as_dict()) + + # Front ports + self.assertEqual(choices_to_dict(response.data.get('front-port:type')), PortTypeChoices.as_dict()) + self.assertEqual(choices_to_dict(response.data.get('front-port-template:type')), PortTypeChoices.as_dict()) + + # Interfaces + self.assertEqual(choices_to_dict(response.data.get('interface:type')), InterfaceTypeChoices.as_dict()) + self.assertEqual(choices_to_dict(response.data.get('interface:mode')), InterfaceModeChoices.as_dict()) + self.assertEqual(choices_to_dict(response.data.get('interface-template:type')), InterfaceTypeChoices.as_dict()) + + # Power feed + self.assertEqual(choices_to_dict(response.data.get('power-feed:phase')), PowerFeedPhaseChoices.as_dict()) + self.assertEqual(choices_to_dict(response.data.get('power-feed:status')), PowerFeedStatusChoices.as_dict()) + self.assertEqual(choices_to_dict(response.data.get('power-feed:supply')), PowerFeedSupplyChoices.as_dict()) + self.assertEqual(choices_to_dict(response.data.get('power-feed:type')), PowerFeedTypeChoices.as_dict()) + + # Power outlets + self.assertEqual(choices_to_dict(response.data.get('power-outlet:type')), PowerOutletTypeChoices.as_dict()) + self.assertEqual(choices_to_dict(response.data.get('power-outlet:feed_leg')), PowerOutletFeedLegChoices.as_dict()) + self.assertEqual(choices_to_dict(response.data.get('power-outlet-template:type')), PowerOutletTypeChoices.as_dict()) + self.assertEqual(choices_to_dict(response.data.get('power-outlet-template:feed_leg')), PowerOutletFeedLegChoices.as_dict()) + + # Power ports + self.assertEqual(choices_to_dict(response.data.get('power-port:type')), PowerPortTypeChoices.as_dict()) + self.assertEqual(choices_to_dict(response.data.get('power-port:connection_status')), dict(CONNECTION_STATUS_CHOICES)) + self.assertEqual(choices_to_dict(response.data.get('power-port-template:type')), PowerPortTypeChoices.as_dict()) + + # Rack + self.assertEqual(choices_to_dict(response.data.get('rack:type')), RackTypeChoices.as_dict()) + self.assertEqual(choices_to_dict(response.data.get('rack:width')), RackWidthChoices.as_dict()) + self.assertEqual(choices_to_dict(response.data.get('rack:status')), RackStatusChoices.as_dict()) + self.assertEqual(choices_to_dict(response.data.get('rack:outer_unit')), RackDimensionUnitChoices.as_dict()) + + # Rear ports + self.assertEqual(choices_to_dict(response.data.get('rear-port:type')), PortTypeChoices.as_dict()) + self.assertEqual(choices_to_dict(response.data.get('rear-port-template:type')), PortTypeChoices.as_dict()) + + # Site + self.assertEqual(choices_to_dict(response.data.get('site:status')), SiteStatusChoices.as_dict()) + + class RegionTest(APITestCase): def setUp(self): @@ -138,16 +223,20 @@ class SiteTest(APITestCase): def test_get_site_graphs(self): + site_ct = ContentType.objects.get_for_model(Site) self.graph1 = Graph.objects.create( - type=GRAPH_TYPE_SITE, name='Test Graph 1', + type=site_ct, + name='Test Graph 1', source='http://example.com/graphs.py?site={{ obj.slug }}&foo=1' ) self.graph2 = Graph.objects.create( - type=GRAPH_TYPE_SITE, name='Test Graph 2', + type=site_ct, + name='Test Graph 2', source='http://example.com/graphs.py?site={{ obj.slug }}&foo=2' ) self.graph3 = Graph.objects.create( - type=GRAPH_TYPE_SITE, name='Test Graph 3', + type=site_ct, + name='Test Graph 3', source='http://example.com/graphs.py?site={{ obj.slug }}&foo=3' ) @@ -180,7 +269,7 @@ class SiteTest(APITestCase): 'name': 'Test Site 4', 'slug': 'test-site-4', 'region': self.region1.pk, - 'status': SITE_STATUS_ACTIVE, + 'status': SiteStatusChoices.STATUS_ACTIVE, } url = reverse('dcim-api:site-list') @@ -200,19 +289,19 @@ class SiteTest(APITestCase): 'name': 'Test Site 4', 'slug': 'test-site-4', 'region': self.region1.pk, - 'status': SITE_STATUS_ACTIVE, + 'status': SiteStatusChoices.STATUS_ACTIVE, }, { 'name': 'Test Site 5', 'slug': 'test-site-5', 'region': self.region1.pk, - 'status': SITE_STATUS_ACTIVE, + 'status': SiteStatusChoices.STATUS_ACTIVE, }, { 'name': 'Test Site 6', 'slug': 'test-site-6', 'region': self.region1.pk, - 'status': SITE_STATUS_ACTIVE, + 'status': SiteStatusChoices.STATUS_ACTIVE, }, ] @@ -2416,16 +2505,20 @@ class InterfaceTest(APITestCase): def test_get_interface_graphs(self): + interface_ct = ContentType.objects.get_for_model(Interface) self.graph1 = Graph.objects.create( - type=GRAPH_TYPE_INTERFACE, name='Test Graph 1', + type=interface_ct, + name='Test Graph 1', source='http://example.com/graphs.py?interface={{ obj.name }}&foo=1' ) self.graph2 = Graph.objects.create( - type=GRAPH_TYPE_INTERFACE, name='Test Graph 2', + type=interface_ct, + name='Test Graph 2', source='http://example.com/graphs.py?interface={{ obj.name }}&foo=2' ) self.graph3 = Graph.objects.create( - type=GRAPH_TYPE_INTERFACE, name='Test Graph 3', + type=interface_ct, + name='Test Graph 3', source='http://example.com/graphs.py?interface={{ obj.name }}&foo=3' ) @@ -2473,7 +2566,7 @@ class InterfaceTest(APITestCase): data = { 'device': self.device.pk, 'name': 'Test Interface 4', - 'mode': IFACE_MODE_TAGGED, + 'mode': InterfaceModeChoices.MODE_TAGGED, 'untagged_vlan': self.vlan3.id, 'tagged_vlans': [self.vlan1.id, self.vlan2.id], } @@ -2520,21 +2613,21 @@ class InterfaceTest(APITestCase): { 'device': self.device.pk, 'name': 'Test Interface 4', - 'mode': IFACE_MODE_TAGGED, + 'mode': InterfaceModeChoices.MODE_TAGGED, 'untagged_vlan': self.vlan2.id, 'tagged_vlans': [self.vlan1.id], }, { 'device': self.device.pk, 'name': 'Test Interface 5', - 'mode': IFACE_MODE_TAGGED, + 'mode': InterfaceModeChoices.MODE_TAGGED, 'untagged_vlan': self.vlan2.id, 'tagged_vlans': [self.vlan1.id], }, { 'device': self.device.pk, 'name': 'Test Interface 6', - 'mode': IFACE_MODE_TAGGED, + 'mode': InterfaceModeChoices.MODE_TAGGED, 'untagged_vlan': self.vlan2.id, 'tagged_vlans': [self.vlan1.id], }, @@ -2553,7 +2646,7 @@ class InterfaceTest(APITestCase): def test_update_interface(self): lag_interface = Interface.objects.create( - device=self.device, name='Test LAG Interface', type=IFACE_TYPE_LAG + device=self.device, name='Test LAG Interface', type=InterfaceTypeChoices.TYPE_LAG ) data = { @@ -2590,11 +2683,11 @@ class DeviceBayTest(APITestCase): manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1') self.devicetype1 = DeviceType.objects.create( manufacturer=manufacturer, model='Parent Device Type', slug='parent-device-type', - subdevice_role=SUBDEVICE_ROLE_PARENT + subdevice_role=SubdeviceRoleChoices.ROLE_PARENT ) self.devicetype2 = DeviceType.objects.create( manufacturer=manufacturer, model='Child Device Type', slug='child-device-type', - subdevice_role=SUBDEVICE_ROLE_CHILD + subdevice_role=SubdeviceRoleChoices.ROLE_CHILD ) devicerole = DeviceRole.objects.create( name='Test Device Role 1', slug='test-device-role-1', color='ff0000' @@ -2841,7 +2934,7 @@ class CableTest(APITestCase): ) for device in [self.device1, self.device2]: for i in range(0, 10): - Interface(device=device, type=IFACE_TYPE_1GE_FIXED, name='eth{}'.format(i)).save() + Interface(device=device, type=InterfaceTypeChoices.TYPE_1GE_FIXED, name='eth{}'.format(i)).save() self.cable1 = Cable( termination_a=self.device1.interfaces.get(name='eth0'), @@ -2885,7 +2978,7 @@ class CableTest(APITestCase): 'termination_a_id': interface_a.pk, 'termination_b_type': 'dcim.interface', 'termination_b_id': interface_b.pk, - 'status': CONNECTION_STATUS_PLANNED, + 'status': CableStatusChoices.STATUS_PLANNED, 'label': 'Test Cable 4', } @@ -2939,7 +3032,7 @@ class CableTest(APITestCase): data = { 'label': 'Test Cable X', - 'status': CONNECTION_STATUS_CONNECTED, + 'status': CableStatusChoices.STATUS_CONNECTED, } url = reverse('dcim-api:cable-detail', kwargs={'pk': self.cable1.pk}) @@ -3033,16 +3126,16 @@ class ConnectionTest(APITestCase): device=self.device2, name='Test Console Server Port 1' ) rearport1 = RearPort.objects.create( - device=self.panel1, name='Test Rear Port 1', type=PORT_TYPE_8P8C + device=self.panel1, name='Test Rear Port 1', type=PortTypeChoices.TYPE_8P8C ) frontport1 = FrontPort.objects.create( - device=self.panel1, name='Test Front Port 1', type=PORT_TYPE_8P8C, rear_port=rearport1 + device=self.panel1, name='Test Front Port 1', type=PortTypeChoices.TYPE_8P8C, rear_port=rearport1 ) rearport2 = RearPort.objects.create( - device=self.panel2, name='Test Rear Port 2', type=PORT_TYPE_8P8C + device=self.panel2, name='Test Rear Port 2', type=PortTypeChoices.TYPE_8P8C ) frontport2 = FrontPort.objects.create( - device=self.panel2, name='Test Front Port 2', type=PORT_TYPE_8P8C, rear_port=rearport2 + device=self.panel2, name='Test Front Port 2', type=PortTypeChoices.TYPE_8P8C, rear_port=rearport2 ) url = reverse('dcim-api:cable-list') @@ -3161,16 +3254,16 @@ class ConnectionTest(APITestCase): device=self.device2, name='Test Interface 2' ) rearport1 = RearPort.objects.create( - device=self.panel1, name='Test Rear Port 1', type=PORT_TYPE_8P8C + device=self.panel1, name='Test Rear Port 1', type=PortTypeChoices.TYPE_8P8C ) frontport1 = FrontPort.objects.create( - device=self.panel1, name='Test Front Port 1', type=PORT_TYPE_8P8C, rear_port=rearport1 + device=self.panel1, name='Test Front Port 1', type=PortTypeChoices.TYPE_8P8C, rear_port=rearport1 ) rearport2 = RearPort.objects.create( - device=self.panel2, name='Test Rear Port 2', type=PORT_TYPE_8P8C + device=self.panel2, name='Test Rear Port 2', type=PortTypeChoices.TYPE_8P8C ) frontport2 = FrontPort.objects.create( - device=self.panel2, name='Test Front Port 2', type=PORT_TYPE_8P8C, rear_port=rearport2 + device=self.panel2, name='Test Front Port 2', type=PortTypeChoices.TYPE_8P8C, rear_port=rearport2 ) url = reverse('dcim-api:cable-list') @@ -3272,16 +3365,16 @@ class ConnectionTest(APITestCase): circuit=circuit, term_side='A', site=self.site, port_speed=10000 ) rearport1 = RearPort.objects.create( - device=self.panel1, name='Test Rear Port 1', type=PORT_TYPE_8P8C + device=self.panel1, name='Test Rear Port 1', type=PortTypeChoices.TYPE_8P8C ) frontport1 = FrontPort.objects.create( - device=self.panel1, name='Test Front Port 1', type=PORT_TYPE_8P8C, rear_port=rearport1 + device=self.panel1, name='Test Front Port 1', type=PortTypeChoices.TYPE_8P8C, rear_port=rearport1 ) rearport2 = RearPort.objects.create( - device=self.panel2, name='Test Rear Port 2', type=PORT_TYPE_8P8C + device=self.panel2, name='Test Rear Port 2', type=PortTypeChoices.TYPE_8P8C ) frontport2 = FrontPort.objects.create( - device=self.panel2, name='Test Front Port 2', type=PORT_TYPE_8P8C, rear_port=rearport2 + device=self.panel2, name='Test Front Port 2', type=PortTypeChoices.TYPE_8P8C, rear_port=rearport2 ) url = reverse('dcim-api:cable-list') @@ -3410,23 +3503,23 @@ class VirtualChassisTest(APITestCase): device_type=device_type, device_role=device_role, name='StackSwitch9', site=site ) for i in range(0, 13): - Interface.objects.create(device=self.device1, name='1/{}'.format(i), type=IFACE_TYPE_1GE_FIXED) + Interface.objects.create(device=self.device1, name='1/{}'.format(i), type=InterfaceTypeChoices.TYPE_1GE_FIXED) for i in range(0, 13): - Interface.objects.create(device=self.device2, name='2/{}'.format(i), type=IFACE_TYPE_1GE_FIXED) + Interface.objects.create(device=self.device2, name='2/{}'.format(i), type=InterfaceTypeChoices.TYPE_1GE_FIXED) for i in range(0, 13): - Interface.objects.create(device=self.device3, name='3/{}'.format(i), type=IFACE_TYPE_1GE_FIXED) + Interface.objects.create(device=self.device3, name='3/{}'.format(i), type=InterfaceTypeChoices.TYPE_1GE_FIXED) for i in range(0, 13): - Interface.objects.create(device=self.device4, name='1/{}'.format(i), type=IFACE_TYPE_1GE_FIXED) + Interface.objects.create(device=self.device4, name='1/{}'.format(i), type=InterfaceTypeChoices.TYPE_1GE_FIXED) for i in range(0, 13): - Interface.objects.create(device=self.device5, name='2/{}'.format(i), type=IFACE_TYPE_1GE_FIXED) + Interface.objects.create(device=self.device5, name='2/{}'.format(i), type=InterfaceTypeChoices.TYPE_1GE_FIXED) for i in range(0, 13): - Interface.objects.create(device=self.device6, name='3/{}'.format(i), type=IFACE_TYPE_1GE_FIXED) + Interface.objects.create(device=self.device6, name='3/{}'.format(i), type=InterfaceTypeChoices.TYPE_1GE_FIXED) for i in range(0, 13): - Interface.objects.create(device=self.device7, name='1/{}'.format(i), type=IFACE_TYPE_1GE_FIXED) + Interface.objects.create(device=self.device7, name='1/{}'.format(i), type=InterfaceTypeChoices.TYPE_1GE_FIXED) for i in range(0, 13): - Interface.objects.create(device=self.device8, name='2/{}'.format(i), type=IFACE_TYPE_1GE_FIXED) + Interface.objects.create(device=self.device8, name='2/{}'.format(i), type=InterfaceTypeChoices.TYPE_1GE_FIXED) for i in range(0, 13): - Interface.objects.create(device=self.device9, name='3/{}'.format(i), type=IFACE_TYPE_1GE_FIXED) + Interface.objects.create(device=self.device9, name='3/{}'.format(i), type=InterfaceTypeChoices.TYPE_1GE_FIXED) # Create two VirtualChassis with three members each self.vc1 = VirtualChassis.objects.create(master=self.device1, domain='test-domain-1') @@ -3678,22 +3771,22 @@ class PowerFeedTest(APITestCase): site=self.site1, rack_group=self.rackgroup1, name='Test Power Panel 2' ) self.powerfeed1 = PowerFeed.objects.create( - power_panel=self.powerpanel1, rack=self.rack1, name='Test Power Feed 1A', type=POWERFEED_TYPE_PRIMARY + power_panel=self.powerpanel1, rack=self.rack1, name='Test Power Feed 1A', type=PowerFeedTypeChoices.TYPE_PRIMARY ) self.powerfeed2 = PowerFeed.objects.create( - power_panel=self.powerpanel2, rack=self.rack1, name='Test Power Feed 1B', type=POWERFEED_TYPE_REDUNDANT + power_panel=self.powerpanel2, rack=self.rack1, name='Test Power Feed 1B', type=PowerFeedTypeChoices.TYPE_REDUNDANT ) self.powerfeed3 = PowerFeed.objects.create( - power_panel=self.powerpanel1, rack=self.rack2, name='Test Power Feed 2A', type=POWERFEED_TYPE_PRIMARY + power_panel=self.powerpanel1, rack=self.rack2, name='Test Power Feed 2A', type=PowerFeedTypeChoices.TYPE_PRIMARY ) self.powerfeed4 = PowerFeed.objects.create( - power_panel=self.powerpanel2, rack=self.rack2, name='Test Power Feed 2B', type=POWERFEED_TYPE_REDUNDANT + power_panel=self.powerpanel2, rack=self.rack2, name='Test Power Feed 2B', type=PowerFeedTypeChoices.TYPE_REDUNDANT ) self.powerfeed5 = PowerFeed.objects.create( - power_panel=self.powerpanel1, rack=self.rack3, name='Test Power Feed 3A', type=POWERFEED_TYPE_PRIMARY + power_panel=self.powerpanel1, rack=self.rack3, name='Test Power Feed 3A', type=PowerFeedTypeChoices.TYPE_PRIMARY ) self.powerfeed6 = PowerFeed.objects.create( - power_panel=self.powerpanel2, rack=self.rack3, name='Test Power Feed 3B', type=POWERFEED_TYPE_REDUNDANT + power_panel=self.powerpanel2, rack=self.rack3, name='Test Power Feed 3B', type=PowerFeedTypeChoices.TYPE_REDUNDANT ) def test_get_powerfeed(self): @@ -3726,7 +3819,7 @@ class PowerFeedTest(APITestCase): 'name': 'Test Power Feed 4A', 'power_panel': self.powerpanel1.pk, 'rack': self.rack4.pk, - 'type': POWERFEED_TYPE_PRIMARY, + 'type': PowerFeedTypeChoices.TYPE_PRIMARY, } url = reverse('dcim-api:powerfeed-list') @@ -3746,13 +3839,13 @@ class PowerFeedTest(APITestCase): 'name': 'Test Power Feed 4A', 'power_panel': self.powerpanel1.pk, 'rack': self.rack4.pk, - 'type': POWERFEED_TYPE_PRIMARY, + 'type': PowerFeedTypeChoices.TYPE_PRIMARY, }, { 'name': 'Test Power Feed 4B', 'power_panel': self.powerpanel1.pk, 'rack': self.rack4.pk, - 'type': POWERFEED_TYPE_REDUNDANT, + 'type': PowerFeedTypeChoices.TYPE_REDUNDANT, }, ] @@ -3769,7 +3862,7 @@ class PowerFeedTest(APITestCase): data = { 'name': 'Test Power Feed X', 'rack': self.rack4.pk, - 'type': POWERFEED_TYPE_REDUNDANT, + 'type': PowerFeedTypeChoices.TYPE_REDUNDANT, } url = reverse('dcim-api:powerfeed-detail', kwargs={'pk': self.powerfeed1.pk}) diff --git a/netbox/dcim/tests/test_filters.py b/netbox/dcim/tests/test_filters.py index 5ad96c363..0c3206cd7 100644 --- a/netbox/dcim/tests/test_filters.py +++ b/netbox/dcim/tests/test_filters.py @@ -1,7 +1,7 @@ from django.contrib.auth.models import User from django.test import TestCase -from dcim.constants import * +from dcim.choices import * from dcim.filters import * from dcim.models import ( Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, @@ -43,27 +43,27 @@ class RegionTestCase(TestCase): def test_id(self): id_list = self.queryset.values_list('id', flat=True)[:2] params = {'id': [str(id) for id in id_list]} - self.assertEqual(RegionFilter(params, self.queryset).qs.count(), 2) + self.assertEqual(RegionFilterSet(params, self.queryset).qs.count(), 2) def test_name(self): params = {'name': ['Region 1', 'Region 2']} - self.assertEqual(RegionFilter(params, self.queryset).qs.count(), 2) + self.assertEqual(RegionFilterSet(params, self.queryset).qs.count(), 2) def test_slug(self): params = {'slug': ['region-1', 'region-2']} - self.assertEqual(RegionFilter(params, self.queryset).qs.count(), 2) + self.assertEqual(RegionFilterSet(params, self.queryset).qs.count(), 2) def test_parent(self): parent_regions = Region.objects.filter(parent__isnull=True)[:2] params = {'parent_id': [parent_regions[0].pk, parent_regions[1].pk]} - self.assertEqual(RegionFilter(params, self.queryset).qs.count(), 4) + self.assertEqual(RegionFilterSet(params, self.queryset).qs.count(), 4) params = {'parent': [parent_regions[0].slug, parent_regions[1].slug]} - self.assertEqual(RegionFilter(params, self.queryset).qs.count(), 4) + self.assertEqual(RegionFilterSet(params, self.queryset).qs.count(), 4) class SiteTestCase(TestCase): queryset = Site.objects.all() - filterset = SiteFilter + filterset = SiteFilterSet @classmethod def setUpTestData(cls): @@ -77,9 +77,9 @@ class SiteTestCase(TestCase): region.save() sites = ( - Site(name='Site 1', slug='site-1', region=regions[0], status=SITE_STATUS_ACTIVE, facility='Facility 1', asn=65001, latitude=10, longitude=10, contact_name='Contact 1', contact_phone='123-555-0001', contact_email='contact1@example.com'), - Site(name='Site 2', slug='site-2', region=regions[1], status=SITE_STATUS_PLANNED, facility='Facility 2', asn=65002, latitude=20, longitude=20, contact_name='Contact 2', contact_phone='123-555-0002', contact_email='contact2@example.com'), - Site(name='Site 3', slug='site-3', region=regions[2], status=SITE_STATUS_RETIRED, facility='Facility 3', asn=65003, latitude=30, longitude=30, contact_name='Contact 3', contact_phone='123-555-0003', contact_email='contact3@example.com'), + Site(name='Site 1', slug='site-1', region=regions[0], status=SiteStatusChoices.STATUS_ACTIVE, facility='Facility 1', asn=65001, latitude=10, longitude=10, contact_name='Contact 1', contact_phone='123-555-0001', contact_email='contact1@example.com'), + Site(name='Site 2', slug='site-2', region=regions[1], status=SiteStatusChoices.STATUS_PLANNED, facility='Facility 2', asn=65002, latitude=20, longitude=20, contact_name='Contact 2', contact_phone='123-555-0002', contact_email='contact2@example.com'), + Site(name='Site 3', slug='site-3', region=regions[2], status=SiteStatusChoices.STATUS_RETIRED, facility='Facility 3', asn=65003, latitude=30, longitude=30, contact_name='Contact 3', contact_phone='123-555-0003', contact_email='contact3@example.com'), ) Site.objects.bulk_create(sites) @@ -130,7 +130,7 @@ class SiteTestCase(TestCase): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_status(self): - params = {'status': [SITE_STATUS_ACTIVE, SITE_STATUS_PLANNED]} + params = {'status': [SiteStatusChoices.STATUS_ACTIVE, SiteStatusChoices.STATUS_PLANNED]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_region(self): @@ -143,7 +143,7 @@ class SiteTestCase(TestCase): class RackGroupTestCase(TestCase): queryset = RackGroup.objects.all() - filterset = RackGroupFilter + filterset = RackGroupFilterSet @classmethod def setUpTestData(cls): @@ -200,7 +200,7 @@ class RackGroupTestCase(TestCase): class RackRoleTestCase(TestCase): queryset = RackRole.objects.all() - filterset = RackRoleFilter + filterset = RackRoleFilterSet @classmethod def setUpTestData(cls): @@ -232,7 +232,7 @@ class RackRoleTestCase(TestCase): class RackTestCase(TestCase): queryset = Rack.objects.all() - filterset = RackFilter + filterset = RackFilterSet @classmethod def setUpTestData(cls): @@ -267,9 +267,9 @@ class RackTestCase(TestCase): RackRole.objects.bulk_create(rack_roles) racks = ( - Rack(name='Rack 1', facility_id='rack-1', site=sites[0], group=rack_groups[0], status=RACK_STATUS_ACTIVE, role=rack_roles[0], serial='ABC', asset_tag='1001', type=RACK_TYPE_2POST, width=RACK_WIDTH_19IN, u_height=42, desc_units=False, outer_width=100, outer_depth=100, outer_unit=LENGTH_UNIT_MILLIMETER), - Rack(name='Rack 2', facility_id='rack-2', site=sites[1], group=rack_groups[1], status=RACK_STATUS_PLANNED, role=rack_roles[1], serial='DEF', asset_tag='1002', type=RACK_TYPE_4POST, width=RACK_WIDTH_19IN, u_height=43, desc_units=False, outer_width=200, outer_depth=200, outer_unit=LENGTH_UNIT_MILLIMETER), - Rack(name='Rack 3', facility_id='rack-3', site=sites[2], group=rack_groups[2], status=RACK_STATUS_RESERVED, role=rack_roles[2], serial='GHI', asset_tag='1003', type=RACK_TYPE_CABINET, width=RACK_WIDTH_23IN, u_height=44, desc_units=True, outer_width=300, outer_depth=300, outer_unit=LENGTH_UNIT_INCH), + Rack(name='Rack 1', facility_id='rack-1', site=sites[0], group=rack_groups[0], status=RackStatusChoices.STATUS_ACTIVE, role=rack_roles[0], serial='ABC', asset_tag='1001', type=RackTypeChoices.TYPE_2POST, width=RackWidthChoices.WIDTH_19IN, u_height=42, desc_units=False, outer_width=100, outer_depth=100, outer_unit=RackDimensionUnitChoices.UNIT_MILLIMETER), + Rack(name='Rack 2', facility_id='rack-2', site=sites[1], group=rack_groups[1], status=RackStatusChoices.STATUS_PLANNED, role=rack_roles[1], serial='DEF', asset_tag='1002', type=RackTypeChoices.TYPE_4POST, width=RackWidthChoices.WIDTH_19IN, u_height=43, desc_units=False, outer_width=200, outer_depth=200, outer_unit=RackDimensionUnitChoices.UNIT_MILLIMETER), + Rack(name='Rack 3', facility_id='rack-3', site=sites[2], group=rack_groups[2], status=RackStatusChoices.STATUS_RESERVED, role=rack_roles[2], serial='GHI', asset_tag='1003', type=RackTypeChoices.TYPE_CABINET, width=RackWidthChoices.WIDTH_23IN, u_height=44, desc_units=True, outer_width=300, outer_depth=300, outer_unit=RackDimensionUnitChoices.UNIT_INCH), ) Rack.objects.bulk_create(racks) @@ -292,12 +292,12 @@ class RackTestCase(TestCase): def test_type(self): # TODO: Test for multiple values - params = {'type': RACK_TYPE_2POST} + params = {'type': RackTypeChoices.TYPE_2POST} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) def test_width(self): # TODO: Test for multiple values - params = {'width': RACK_WIDTH_19IN} + params = {'width': RackWidthChoices.WIDTH_19IN} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_u_height(self): @@ -320,7 +320,7 @@ class RackTestCase(TestCase): def test_outer_unit(self): self.assertEqual(Rack.objects.filter(outer_unit__isnull=False).count(), 3) - params = {'outer_unit': LENGTH_UNIT_MILLIMETER} + params = {'outer_unit': RackDimensionUnitChoices.UNIT_MILLIMETER} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_id__in(self): @@ -350,7 +350,7 @@ class RackTestCase(TestCase): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_status(self): - params = {'status': [RACK_STATUS_ACTIVE, RACK_STATUS_PLANNED]} + params = {'status': [RackStatusChoices.STATUS_ACTIVE, RackStatusChoices.STATUS_PLANNED]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_role(self): @@ -369,7 +369,7 @@ class RackTestCase(TestCase): class RackReservationTestCase(TestCase): queryset = RackReservation.objects.all() - filterset = RackReservationFilter + filterset = RackReservationFilterSet @classmethod def setUpTestData(cls): @@ -439,7 +439,7 @@ class RackReservationTestCase(TestCase): class ManufacturerTestCase(TestCase): queryset = Manufacturer.objects.all() - filterset = ManufacturerFilter + filterset = ManufacturerFilterSet @classmethod def setUpTestData(cls): @@ -467,7 +467,7 @@ class ManufacturerTestCase(TestCase): class DeviceTypeTestCase(TestCase): queryset = DeviceType.objects.all() - filterset = DeviceTypeFilter + filterset = DeviceTypeFilterSet @classmethod def setUpTestData(cls): @@ -480,9 +480,9 @@ class DeviceTypeTestCase(TestCase): Manufacturer.objects.bulk_create(manufacturers) device_types = ( - DeviceType(manufacturer=manufacturers[0], model='Model 1', slug='model-1', part_number='Part Number 1', u_height=1, is_full_depth=True, subdevice_role=None), - DeviceType(manufacturer=manufacturers[1], model='Model 2', slug='model-2', part_number='Part Number 2', u_height=2, is_full_depth=True, subdevice_role=SUBDEVICE_ROLE_PARENT), - DeviceType(manufacturer=manufacturers[2], model='Model 3', slug='model-3', part_number='Part Number 3', u_height=3, is_full_depth=False, subdevice_role=SUBDEVICE_ROLE_CHILD), + DeviceType(manufacturer=manufacturers[0], model='Model 1', slug='model-1', part_number='Part Number 1', u_height=1, is_full_depth=True), + DeviceType(manufacturer=manufacturers[1], model='Model 2', slug='model-2', part_number='Part Number 2', u_height=2, is_full_depth=True, subdevice_role=SubdeviceRoleChoices.ROLE_PARENT), + DeviceType(manufacturer=manufacturers[2], model='Model 3', slug='model-3', part_number='Part Number 3', u_height=3, is_full_depth=False, subdevice_role=SubdeviceRoleChoices.ROLE_CHILD), ) DeviceType.objects.bulk_create(device_types) @@ -508,13 +508,13 @@ class DeviceTypeTestCase(TestCase): InterfaceTemplate(device_type=device_types[1], name='Interface 2'), )) rear_ports = ( - RearPortTemplate(device_type=device_types[0], name='Rear Port 1', type=PORT_TYPE_8P8C), - RearPortTemplate(device_type=device_types[1], name='Rear Port 2', type=PORT_TYPE_8P8C), + RearPortTemplate(device_type=device_types[0], name='Rear Port 1', type=PortTypeChoices.TYPE_8P8C), + RearPortTemplate(device_type=device_types[1], name='Rear Port 2', type=PortTypeChoices.TYPE_8P8C), ) RearPortTemplate.objects.bulk_create(rear_ports) FrontPortTemplate.objects.bulk_create(( - FrontPortTemplate(device_type=device_types[0], name='Front Port 1', type=PORT_TYPE_8P8C, rear_port=rear_ports[0]), - FrontPortTemplate(device_type=device_types[1], name='Front Port 2', type=PORT_TYPE_8P8C, rear_port=rear_ports[1]), + FrontPortTemplate(device_type=device_types[0], name='Front Port 1', type=PortTypeChoices.TYPE_8P8C, rear_port=rear_ports[0]), + FrontPortTemplate(device_type=device_types[1], name='Front Port 2', type=PortTypeChoices.TYPE_8P8C, rear_port=rear_ports[1]), )) DeviceBayTemplate.objects.bulk_create(( DeviceBayTemplate(device_type=device_types[0], name='Device Bay 1'), @@ -544,7 +544,7 @@ class DeviceTypeTestCase(TestCase): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) def test_subdevice_role(self): - params = {'subdevice_role': SUBDEVICE_ROLE_PARENT} + params = {'subdevice_role': SubdeviceRoleChoices.ROLE_PARENT} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) def test_id__in(self): @@ -605,7 +605,7 @@ class DeviceTypeTestCase(TestCase): class ConsolePortTemplateTestCase(TestCase): queryset = ConsolePortTemplate.objects.all() - filterset = ConsolePortTemplateFilter + filterset = ConsolePortTemplateFilterSet @classmethod def setUpTestData(cls): @@ -642,7 +642,7 @@ class ConsolePortTemplateTestCase(TestCase): class ConsoleServerPortTemplateTestCase(TestCase): queryset = ConsoleServerPortTemplate.objects.all() - filterset = ConsoleServerPortTemplateFilter + filterset = ConsoleServerPortTemplateFilterSet @classmethod def setUpTestData(cls): @@ -679,7 +679,7 @@ class ConsoleServerPortTemplateTestCase(TestCase): class PowerPortTemplateTestCase(TestCase): queryset = PowerPortTemplate.objects.all() - filterset = PowerPortTemplateFilter + filterset = PowerPortTemplateFilterSet @classmethod def setUpTestData(cls): @@ -724,7 +724,7 @@ class PowerPortTemplateTestCase(TestCase): class PowerOutletTemplateTestCase(TestCase): queryset = PowerOutletTemplate.objects.all() - filterset = PowerOutletTemplateFilter + filterset = PowerOutletTemplateFilterSet @classmethod def setUpTestData(cls): @@ -739,9 +739,9 @@ class PowerOutletTemplateTestCase(TestCase): DeviceType.objects.bulk_create(device_types) PowerOutletTemplate.objects.bulk_create(( - PowerOutletTemplate(device_type=device_types[0], name='Power Outlet 1', feed_leg=POWERFEED_LEG_A), - PowerOutletTemplate(device_type=device_types[1], name='Power Outlet 2', feed_leg=POWERFEED_LEG_B), - PowerOutletTemplate(device_type=device_types[2], name='Power Outlet 3', feed_leg=POWERFEED_LEG_C), + PowerOutletTemplate(device_type=device_types[0], name='Power Outlet 1', feed_leg=PowerOutletFeedLegChoices.FEED_LEG_A), + PowerOutletTemplate(device_type=device_types[1], name='Power Outlet 2', feed_leg=PowerOutletFeedLegChoices.FEED_LEG_B), + PowerOutletTemplate(device_type=device_types[2], name='Power Outlet 3', feed_leg=PowerOutletFeedLegChoices.FEED_LEG_C), )) def test_id(self): @@ -760,13 +760,13 @@ class PowerOutletTemplateTestCase(TestCase): def test_feed_leg(self): # TODO: Support filtering for multiple values - params = {'feed_leg': POWERFEED_LEG_A} + params = {'feed_leg': PowerOutletFeedLegChoices.FEED_LEG_A} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) class InterfaceTemplateTestCase(TestCase): queryset = InterfaceTemplate.objects.all() - filterset = InterfaceTemplateFilter + filterset = InterfaceTemplateFilterSet @classmethod def setUpTestData(cls): @@ -781,9 +781,9 @@ class InterfaceTemplateTestCase(TestCase): DeviceType.objects.bulk_create(device_types) InterfaceTemplate.objects.bulk_create(( - InterfaceTemplate(device_type=device_types[0], name='Interface 1', type=IFACE_TYPE_1GE_FIXED, mgmt_only=True), - InterfaceTemplate(device_type=device_types[1], name='Interface 2', type=IFACE_TYPE_1GE_GBIC, mgmt_only=False), - InterfaceTemplate(device_type=device_types[2], name='Interface 3', type=IFACE_TYPE_1GE_SFP, mgmt_only=False), + InterfaceTemplate(device_type=device_types[0], name='Interface 1', type=InterfaceTypeChoices.TYPE_1GE_FIXED, mgmt_only=True), + InterfaceTemplate(device_type=device_types[1], name='Interface 2', type=InterfaceTypeChoices.TYPE_1GE_GBIC, mgmt_only=False), + InterfaceTemplate(device_type=device_types[2], name='Interface 3', type=InterfaceTypeChoices.TYPE_1GE_SFP, mgmt_only=False), )) def test_id(self): @@ -802,7 +802,7 @@ class InterfaceTemplateTestCase(TestCase): def test_type(self): # TODO: Support filtering for multiple values - params = {'type': IFACE_TYPE_1GE_FIXED} + params = {'type': InterfaceTypeChoices.TYPE_1GE_FIXED} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) def test_mgmt_only(self): @@ -814,7 +814,7 @@ class InterfaceTemplateTestCase(TestCase): class FrontPortTemplateTestCase(TestCase): queryset = FrontPortTemplate.objects.all() - filterset = FrontPortTemplateFilter + filterset = FrontPortTemplateFilterSet @classmethod def setUpTestData(cls): @@ -829,16 +829,16 @@ class FrontPortTemplateTestCase(TestCase): DeviceType.objects.bulk_create(device_types) rear_ports = ( - RearPortTemplate(device_type=device_types[0], name='Rear Port 1', type=PORT_TYPE_8P8C), - RearPortTemplate(device_type=device_types[1], name='Rear Port 2', type=PORT_TYPE_8P8C), - RearPortTemplate(device_type=device_types[2], name='Rear Port 3', type=PORT_TYPE_8P8C), + RearPortTemplate(device_type=device_types[0], name='Rear Port 1', type=PortTypeChoices.TYPE_8P8C), + RearPortTemplate(device_type=device_types[1], name='Rear Port 2', type=PortTypeChoices.TYPE_8P8C), + RearPortTemplate(device_type=device_types[2], name='Rear Port 3', type=PortTypeChoices.TYPE_8P8C), ) RearPortTemplate.objects.bulk_create(rear_ports) FrontPortTemplate.objects.bulk_create(( - FrontPortTemplate(device_type=device_types[0], name='Front Port 1', rear_port=rear_ports[0], type=PORT_TYPE_8P8C), - FrontPortTemplate(device_type=device_types[1], name='Front Port 2', rear_port=rear_ports[1], type=PORT_TYPE_110_PUNCH), - FrontPortTemplate(device_type=device_types[2], name='Front Port 3', rear_port=rear_ports[2], type=PORT_TYPE_BNC), + FrontPortTemplate(device_type=device_types[0], name='Front Port 1', rear_port=rear_ports[0], type=PortTypeChoices.TYPE_8P8C), + FrontPortTemplate(device_type=device_types[1], name='Front Port 2', rear_port=rear_ports[1], type=PortTypeChoices.TYPE_110_PUNCH), + FrontPortTemplate(device_type=device_types[2], name='Front Port 3', rear_port=rear_ports[2], type=PortTypeChoices.TYPE_BNC), )) def test_id(self): @@ -857,13 +857,13 @@ class FrontPortTemplateTestCase(TestCase): def test_type(self): # TODO: Support filtering for multiple values - params = {'type': PORT_TYPE_8P8C} + params = {'type': PortTypeChoices.TYPE_8P8C} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) class RearPortTemplateTestCase(TestCase): queryset = RearPortTemplate.objects.all() - filterset = RearPortTemplateFilter + filterset = RearPortTemplateFilterSet @classmethod def setUpTestData(cls): @@ -878,9 +878,9 @@ class RearPortTemplateTestCase(TestCase): DeviceType.objects.bulk_create(device_types) RearPortTemplate.objects.bulk_create(( - RearPortTemplate(device_type=device_types[0], name='Rear Port 1', type=PORT_TYPE_8P8C, positions=1), - RearPortTemplate(device_type=device_types[1], name='Rear Port 2', type=PORT_TYPE_110_PUNCH, positions=2), - RearPortTemplate(device_type=device_types[2], name='Rear Port 3', type=PORT_TYPE_BNC, positions=3), + RearPortTemplate(device_type=device_types[0], name='Rear Port 1', type=PortTypeChoices.TYPE_8P8C, positions=1), + RearPortTemplate(device_type=device_types[1], name='Rear Port 2', type=PortTypeChoices.TYPE_110_PUNCH, positions=2), + RearPortTemplate(device_type=device_types[2], name='Rear Port 3', type=PortTypeChoices.TYPE_BNC, positions=3), )) def test_id(self): @@ -899,7 +899,7 @@ class RearPortTemplateTestCase(TestCase): def test_type(self): # TODO: Support filtering for multiple values - params = {'type': PORT_TYPE_8P8C} + params = {'type': PortTypeChoices.TYPE_8P8C} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) def test_positions(self): @@ -909,7 +909,7 @@ class RearPortTemplateTestCase(TestCase): class DeviceBayTemplateTestCase(TestCase): queryset = DeviceBayTemplate.objects.all() - filterset = DeviceBayTemplateFilter + filterset = DeviceBayTemplateFilterSet @classmethod def setUpTestData(cls): @@ -946,7 +946,7 @@ class DeviceBayTemplateTestCase(TestCase): class DeviceRoleTestCase(TestCase): queryset = DeviceRole.objects.all() - filterset = DeviceRoleFilter + filterset = DeviceRoleFilterSet @classmethod def setUpTestData(cls): @@ -984,7 +984,7 @@ class DeviceRoleTestCase(TestCase): class PlatformTestCase(TestCase): queryset = Platform.objects.all() - filterset = PlatformFilter + filterset = PlatformFilterSet @classmethod def setUpTestData(cls): @@ -1030,7 +1030,7 @@ class PlatformTestCase(TestCase): class DeviceTestCase(TestCase): queryset = Device.objects.all() - filterset = DeviceFilter + filterset = DeviceFilterSet @classmethod def setUpTestData(cls): @@ -1101,9 +1101,9 @@ class DeviceTestCase(TestCase): Cluster.objects.bulk_create(clusters) devices = ( - Device(name='Device 1', device_type=device_types[0], device_role=device_roles[0], platform=platforms[0], serial='ABC', asset_tag='1001', site=sites[0], rack=racks[0], position=1, face=RACK_FACE_FRONT, status=DEVICE_STATUS_ACTIVE, cluster=clusters[0], local_context_data={"foo": 123}), - Device(name='Device 2', device_type=device_types[1], device_role=device_roles[1], platform=platforms[1], serial='DEF', asset_tag='1002', site=sites[1], rack=racks[1], position=2, face=RACK_FACE_FRONT, status=DEVICE_STATUS_STAGED, cluster=clusters[1]), - Device(name='Device 3', device_type=device_types[2], device_role=device_roles[2], platform=platforms[2], serial='GHI', asset_tag='1003', site=sites[2], rack=racks[2], position=3, face=RACK_FACE_REAR, status=DEVICE_STATUS_FAILED, cluster=clusters[2]), + Device(name='Device 1', device_type=device_types[0], device_role=device_roles[0], platform=platforms[0], serial='ABC', asset_tag='1001', site=sites[0], rack=racks[0], position=1, face=DeviceFaceChoices.FACE_FRONT, status=DeviceStatusChoices.STATUS_ACTIVE, cluster=clusters[0], local_context_data={"foo": 123}), + Device(name='Device 2', device_type=device_types[1], device_role=device_roles[1], platform=platforms[1], serial='DEF', asset_tag='1002', site=sites[1], rack=racks[1], position=2, face=DeviceFaceChoices.FACE_FRONT, status=DeviceStatusChoices.STATUS_STAGED, cluster=clusters[1]), + Device(name='Device 3', device_type=device_types[2], device_role=device_roles[2], platform=platforms[2], serial='GHI', asset_tag='1003', site=sites[2], rack=racks[2], position=3, face=DeviceFaceChoices.FACE_REAR, status=DeviceStatusChoices.STATUS_FAILED, cluster=clusters[2]), ) Device.objects.bulk_create(devices) @@ -1130,13 +1130,13 @@ class DeviceTestCase(TestCase): ) Interface.objects.bulk_create(interfaces) rear_ports = ( - RearPort(device=devices[0], name='Rear Port 1', type=PORT_TYPE_8P8C), - RearPort(device=devices[1], name='Rear Port 2', type=PORT_TYPE_8P8C), + RearPort(device=devices[0], name='Rear Port 1', type=PortTypeChoices.TYPE_8P8C), + RearPort(device=devices[1], name='Rear Port 2', type=PortTypeChoices.TYPE_8P8C), ) RearPort.objects.bulk_create(rear_ports) FrontPort.objects.bulk_create(( - FrontPort(device=devices[0], name='Front Port 1', type=PORT_TYPE_8P8C, rear_port=rear_ports[0]), - FrontPort(device=devices[1], name='Front Port 2', type=PORT_TYPE_8P8C, rear_port=rear_ports[1]), + FrontPort(device=devices[0], name='Front Port 1', type=PortTypeChoices.TYPE_8P8C, rear_port=rear_ports[0]), + FrontPort(device=devices[1], name='Front Port 2', type=PortTypeChoices.TYPE_8P8C, rear_port=rear_ports[1]), )) DeviceBay.objects.bulk_create(( DeviceBay(device=devices[0], name='Device Bay 1'), @@ -1171,7 +1171,7 @@ class DeviceTestCase(TestCase): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_face(self): - params = {'face': RACK_FACE_FRONT} + params = {'face': DeviceFaceChoices.FACE_FRONT} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_position(self): @@ -1251,7 +1251,7 @@ class DeviceTestCase(TestCase): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_status(self): - params = {'status': [DEVICE_STATUS_ACTIVE, DEVICE_STATUS_STAGED]} + params = {'status': [DeviceStatusChoices.STATUS_ACTIVE, DeviceStatusChoices.STATUS_STAGED]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_is_full_depth(self): @@ -1338,7 +1338,7 @@ class DeviceTestCase(TestCase): class ConsolePortTestCase(TestCase): queryset = ConsolePort.objects.all() - filterset = ConsolePortFilter + filterset = ConsolePortFilterSet @classmethod def setUpTestData(cls): @@ -1408,7 +1408,7 @@ class ConsolePortTestCase(TestCase): class ConsoleServerPortTestCase(TestCase): queryset = ConsoleServerPort.objects.all() - filterset = ConsoleServerPortFilter + filterset = ConsoleServerPortFilterSet @classmethod def setUpTestData(cls): @@ -1478,7 +1478,7 @@ class ConsoleServerPortTestCase(TestCase): class PowerPortTestCase(TestCase): queryset = PowerPort.objects.all() - filterset = PowerPortFilter + filterset = PowerPortFilterSet @classmethod def setUpTestData(cls): @@ -1556,7 +1556,7 @@ class PowerPortTestCase(TestCase): class PowerOutletTestCase(TestCase): queryset = PowerOutlet.objects.all() - filterset = PowerOutletFilter + filterset = PowerOutletFilterSet @classmethod def setUpTestData(cls): @@ -1581,9 +1581,9 @@ class PowerOutletTestCase(TestCase): PowerPort.objects.bulk_create(power_ports) power_outlets = ( - PowerOutlet(device=devices[0], name='Power Outlet 1', feed_leg=POWERFEED_LEG_A, description='First'), - PowerOutlet(device=devices[1], name='Power Outlet 2', feed_leg=POWERFEED_LEG_B, description='Second'), - PowerOutlet(device=devices[2], name='Power Outlet 3', feed_leg=POWERFEED_LEG_C, description='Third'), + PowerOutlet(device=devices[0], name='Power Outlet 1', feed_leg=PowerOutletFeedLegChoices.FEED_LEG_A, description='First'), + PowerOutlet(device=devices[1], name='Power Outlet 2', feed_leg=PowerOutletFeedLegChoices.FEED_LEG_B, description='Second'), + PowerOutlet(device=devices[2], name='Power Outlet 3', feed_leg=PowerOutletFeedLegChoices.FEED_LEG_C, description='Third'), ) PowerOutlet.objects.bulk_create(power_outlets) @@ -1607,7 +1607,7 @@ class PowerOutletTestCase(TestCase): def test_feed_leg(self): # TODO: Support filtering for multiple values - params = {'feed_leg': POWERFEED_LEG_A} + params = {'feed_leg': PowerOutletFeedLegChoices.FEED_LEG_A} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) # TODO: Fix boolean value @@ -1631,7 +1631,7 @@ class PowerOutletTestCase(TestCase): class InterfaceTestCase(TestCase): queryset = Interface.objects.all() - filterset = InterfaceFilter + filterset = InterfaceFilterSet @classmethod def setUpTestData(cls): @@ -1650,12 +1650,12 @@ class InterfaceTestCase(TestCase): Device.objects.bulk_create(devices) interfaces = ( - Interface(device=devices[0], name='Interface 1', type=IFACE_TYPE_1GE_SFP, enabled=True, mgmt_only=True, mtu=100, mode=IFACE_MODE_ACCESS, mac_address='00-00-00-00-00-01', description='First'), - Interface(device=devices[1], name='Interface 2', type=IFACE_TYPE_1GE_GBIC, enabled=True, mgmt_only=True, mtu=200, mode=IFACE_MODE_TAGGED, mac_address='00-00-00-00-00-02', description='Second'), - Interface(device=devices[2], name='Interface 3', type=IFACE_TYPE_1GE_FIXED, enabled=False, mgmt_only=False, mtu=300, mode=IFACE_MODE_TAGGED_ALL, mac_address='00-00-00-00-00-03', description='Third'), - Interface(device=devices[3], name='Interface 4', type=IFACE_TYPE_OTHER, enabled=True, mgmt_only=True), - Interface(device=devices[3], name='Interface 5', type=IFACE_TYPE_OTHER, enabled=True, mgmt_only=True), - Interface(device=devices[3], name='Interface 6', type=IFACE_TYPE_OTHER, enabled=False, mgmt_only=False), + Interface(device=devices[0], name='Interface 1', type=InterfaceTypeChoices.TYPE_1GE_SFP, enabled=True, mgmt_only=True, mtu=100, mode=InterfaceModeChoices.MODE_ACCESS, mac_address='00-00-00-00-00-01', description='First'), + Interface(device=devices[1], name='Interface 2', type=InterfaceTypeChoices.TYPE_1GE_GBIC, enabled=True, mgmt_only=True, mtu=200, mode=InterfaceModeChoices.MODE_TAGGED, mac_address='00-00-00-00-00-02', description='Second'), + Interface(device=devices[2], name='Interface 3', type=InterfaceTypeChoices.TYPE_1GE_FIXED, enabled=False, mgmt_only=False, mtu=300, mode=InterfaceModeChoices.MODE_TAGGED_ALL, mac_address='00-00-00-00-00-03', description='Third'), + Interface(device=devices[3], name='Interface 4', type=InterfaceTypeChoices.TYPE_OTHER, enabled=True, mgmt_only=True), + Interface(device=devices[3], name='Interface 5', type=InterfaceTypeChoices.TYPE_OTHER, enabled=True, mgmt_only=True), + Interface(device=devices[3], name='Interface 6', type=InterfaceTypeChoices.TYPE_OTHER, enabled=False, mgmt_only=False), ) Interface.objects.bulk_create(interfaces) @@ -1695,7 +1695,7 @@ class InterfaceTestCase(TestCase): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_mode(self): - params = {'mode': IFACE_MODE_ACCESS} + params = {'mode': InterfaceModeChoices.MODE_ACCESS} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) def test_description(self): @@ -1726,13 +1726,13 @@ class InterfaceTestCase(TestCase): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_type(self): - params = {'type': [IFACE_TYPE_1GE_FIXED, IFACE_TYPE_1GE_GBIC]} + params = {'type': [InterfaceTypeChoices.TYPE_1GE_FIXED, InterfaceTypeChoices.TYPE_1GE_GBIC]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) class FrontPortTestCase(TestCase): queryset = FrontPort.objects.all() - filterset = FrontPortFilter + filterset = FrontPortFilterSet @classmethod def setUpTestData(cls): @@ -1751,22 +1751,22 @@ class FrontPortTestCase(TestCase): Device.objects.bulk_create(devices) rear_ports = ( - RearPort(device=devices[0], name='Rear Port 1', type=PORT_TYPE_8P8C, positions=6), - RearPort(device=devices[1], name='Rear Port 2', type=PORT_TYPE_8P8C, positions=6), - RearPort(device=devices[2], name='Rear Port 3', type=PORT_TYPE_8P8C, positions=6), - RearPort(device=devices[3], name='Rear Port 4', type=PORT_TYPE_8P8C, positions=6), - RearPort(device=devices[3], name='Rear Port 5', type=PORT_TYPE_8P8C, positions=6), - RearPort(device=devices[3], name='Rear Port 6', type=PORT_TYPE_8P8C, positions=6), + RearPort(device=devices[0], name='Rear Port 1', type=PortTypeChoices.TYPE_8P8C, positions=6), + RearPort(device=devices[1], name='Rear Port 2', type=PortTypeChoices.TYPE_8P8C, positions=6), + RearPort(device=devices[2], name='Rear Port 3', type=PortTypeChoices.TYPE_8P8C, positions=6), + RearPort(device=devices[3], name='Rear Port 4', type=PortTypeChoices.TYPE_8P8C, positions=6), + RearPort(device=devices[3], name='Rear Port 5', type=PortTypeChoices.TYPE_8P8C, positions=6), + RearPort(device=devices[3], name='Rear Port 6', type=PortTypeChoices.TYPE_8P8C, positions=6), ) RearPort.objects.bulk_create(rear_ports) front_ports = ( - FrontPort(device=devices[0], name='Front Port 1', type=PORT_TYPE_8P8C, rear_port=rear_ports[0], rear_port_position=1, description='First'), - FrontPort(device=devices[1], name='Front Port 2', type=PORT_TYPE_110_PUNCH, rear_port=rear_ports[1], rear_port_position=2, description='Second'), - FrontPort(device=devices[2], name='Front Port 3', type=PORT_TYPE_BNC, rear_port=rear_ports[2], rear_port_position=3, description='Third'), - FrontPort(device=devices[3], name='Front Port 4', type=PORT_TYPE_FC, rear_port=rear_ports[3], rear_port_position=1), - FrontPort(device=devices[3], name='Front Port 5', type=PORT_TYPE_FC, rear_port=rear_ports[4], rear_port_position=1), - FrontPort(device=devices[3], name='Front Port 6', type=PORT_TYPE_FC, rear_port=rear_ports[5], rear_port_position=1), + FrontPort(device=devices[0], name='Front Port 1', type=PortTypeChoices.TYPE_8P8C, rear_port=rear_ports[0], rear_port_position=1, description='First'), + FrontPort(device=devices[1], name='Front Port 2', type=PortTypeChoices.TYPE_110_PUNCH, rear_port=rear_ports[1], rear_port_position=2, description='Second'), + FrontPort(device=devices[2], name='Front Port 3', type=PortTypeChoices.TYPE_BNC, rear_port=rear_ports[2], rear_port_position=3, description='Third'), + FrontPort(device=devices[3], name='Front Port 4', type=PortTypeChoices.TYPE_FC, rear_port=rear_ports[3], rear_port_position=1), + FrontPort(device=devices[3], name='Front Port 5', type=PortTypeChoices.TYPE_FC, rear_port=rear_ports[4], rear_port_position=1), + FrontPort(device=devices[3], name='Front Port 6', type=PortTypeChoices.TYPE_FC, rear_port=rear_ports[5], rear_port_position=1), ) FrontPort.objects.bulk_create(front_ports) @@ -1786,7 +1786,7 @@ class FrontPortTestCase(TestCase): def test_type(self): # TODO: Test for multiple values - params = {'type': PORT_TYPE_8P8C} + params = {'type': PortTypeChoices.TYPE_8P8C} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) def test_description(self): @@ -1809,7 +1809,7 @@ class FrontPortTestCase(TestCase): class RearPortTestCase(TestCase): queryset = RearPort.objects.all() - filterset = RearPortFilter + filterset = RearPortFilterSet @classmethod def setUpTestData(cls): @@ -1828,12 +1828,12 @@ class RearPortTestCase(TestCase): Device.objects.bulk_create(devices) rear_ports = ( - RearPort(device=devices[0], name='Rear Port 1', type=PORT_TYPE_8P8C, positions=1, description='First'), - RearPort(device=devices[1], name='Rear Port 2', type=PORT_TYPE_110_PUNCH, positions=2, description='Second'), - RearPort(device=devices[2], name='Rear Port 3', type=PORT_TYPE_BNC, positions=3, description='Third'), - RearPort(device=devices[3], name='Rear Port 4', type=PORT_TYPE_FC, positions=4), - RearPort(device=devices[3], name='Rear Port 5', type=PORT_TYPE_FC, positions=5), - RearPort(device=devices[3], name='Rear Port 6', type=PORT_TYPE_FC, positions=6), + RearPort(device=devices[0], name='Rear Port 1', type=PortTypeChoices.TYPE_8P8C, positions=1, description='First'), + RearPort(device=devices[1], name='Rear Port 2', type=PortTypeChoices.TYPE_110_PUNCH, positions=2, description='Second'), + RearPort(device=devices[2], name='Rear Port 3', type=PortTypeChoices.TYPE_BNC, positions=3, description='Third'), + RearPort(device=devices[3], name='Rear Port 4', type=PortTypeChoices.TYPE_FC, positions=4), + RearPort(device=devices[3], name='Rear Port 5', type=PortTypeChoices.TYPE_FC, positions=5), + RearPort(device=devices[3], name='Rear Port 6', type=PortTypeChoices.TYPE_FC, positions=6), ) RearPort.objects.bulk_create(rear_ports) @@ -1853,7 +1853,7 @@ class RearPortTestCase(TestCase): def test_type(self): # TODO: Test for multiple values - params = {'type': PORT_TYPE_8P8C} + params = {'type': PortTypeChoices.TYPE_8P8C} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) def test_positions(self): @@ -1880,7 +1880,7 @@ class RearPortTestCase(TestCase): class DeviceBayTestCase(TestCase): queryset = DeviceBay.objects.all() - filterset = DeviceBayFilter + filterset = DeviceBayFilterSet @classmethod def setUpTestData(cls): @@ -1927,7 +1927,7 @@ class DeviceBayTestCase(TestCase): class InventoryItemTestCase(TestCase): queryset = InventoryItem.objects.all() - filterset = InventoryItemFilter + filterset = InventoryItemFilterSet @classmethod def setUpTestData(cls): @@ -2045,7 +2045,7 @@ class InventoryItemTestCase(TestCase): class VirtualChassisTestCase(TestCase): queryset = VirtualChassis.objects.all() - filterset = VirtualChassisFilter + filterset = VirtualChassisFilterSet @classmethod def setUpTestData(cls): @@ -2116,7 +2116,7 @@ class VirtualChassisTestCase(TestCase): class CableTestCase(TestCase): queryset = Cable.objects.all() - filterset = CableFilter + filterset = CableFilterSet @classmethod def setUpTestData(cls): @@ -2156,28 +2156,28 @@ class CableTestCase(TestCase): Device.objects.bulk_create(devices) interfaces = ( - Interface(device=devices[0], name='Interface 1', type=IFACE_TYPE_1GE_FIXED), - Interface(device=devices[0], name='Interface 2', type=IFACE_TYPE_1GE_FIXED), - Interface(device=devices[1], name='Interface 3', type=IFACE_TYPE_1GE_FIXED), - Interface(device=devices[1], name='Interface 4', type=IFACE_TYPE_1GE_FIXED), - Interface(device=devices[2], name='Interface 5', type=IFACE_TYPE_1GE_FIXED), - Interface(device=devices[2], name='Interface 6', type=IFACE_TYPE_1GE_FIXED), - Interface(device=devices[3], name='Interface 7', type=IFACE_TYPE_1GE_FIXED), - Interface(device=devices[3], name='Interface 8', type=IFACE_TYPE_1GE_FIXED), - Interface(device=devices[4], name='Interface 9', type=IFACE_TYPE_1GE_FIXED), - Interface(device=devices[4], name='Interface 10', type=IFACE_TYPE_1GE_FIXED), - Interface(device=devices[5], name='Interface 11', type=IFACE_TYPE_1GE_FIXED), - Interface(device=devices[5], name='Interface 12', type=IFACE_TYPE_1GE_FIXED), + Interface(device=devices[0], name='Interface 1', type=InterfaceTypeChoices.TYPE_1GE_FIXED), + Interface(device=devices[0], name='Interface 2', type=InterfaceTypeChoices.TYPE_1GE_FIXED), + Interface(device=devices[1], name='Interface 3', type=InterfaceTypeChoices.TYPE_1GE_FIXED), + Interface(device=devices[1], name='Interface 4', type=InterfaceTypeChoices.TYPE_1GE_FIXED), + Interface(device=devices[2], name='Interface 5', type=InterfaceTypeChoices.TYPE_1GE_FIXED), + Interface(device=devices[2], name='Interface 6', type=InterfaceTypeChoices.TYPE_1GE_FIXED), + Interface(device=devices[3], name='Interface 7', type=InterfaceTypeChoices.TYPE_1GE_FIXED), + Interface(device=devices[3], name='Interface 8', type=InterfaceTypeChoices.TYPE_1GE_FIXED), + Interface(device=devices[4], name='Interface 9', type=InterfaceTypeChoices.TYPE_1GE_FIXED), + Interface(device=devices[4], name='Interface 10', type=InterfaceTypeChoices.TYPE_1GE_FIXED), + Interface(device=devices[5], name='Interface 11', type=InterfaceTypeChoices.TYPE_1GE_FIXED), + Interface(device=devices[5], name='Interface 12', type=InterfaceTypeChoices.TYPE_1GE_FIXED), ) Interface.objects.bulk_create(interfaces) # Cables - Cable(termination_a=interfaces[1], termination_b=interfaces[2], label='Cable 1', type=CABLE_TYPE_CAT3, status=CONNECTION_STATUS_CONNECTED, color='aa1409', length=10, length_unit=LENGTH_UNIT_FOOT).save() - Cable(termination_a=interfaces[3], termination_b=interfaces[4], label='Cable 2', type=CABLE_TYPE_CAT3, status=CONNECTION_STATUS_CONNECTED, color='aa1409', length=20, length_unit=LENGTH_UNIT_FOOT).save() - Cable(termination_a=interfaces[5], termination_b=interfaces[6], label='Cable 3', type=CABLE_TYPE_CAT5E, status=CONNECTION_STATUS_CONNECTED, color='f44336', length=30, length_unit=LENGTH_UNIT_FOOT).save() - Cable(termination_a=interfaces[7], termination_b=interfaces[8], label='Cable 4', type=CABLE_TYPE_CAT5E, status=CONNECTION_STATUS_PLANNED, color='f44336', length=40, length_unit=LENGTH_UNIT_FOOT).save() - Cable(termination_a=interfaces[9], termination_b=interfaces[10], label='Cable 5', type=CABLE_TYPE_CAT6, status=CONNECTION_STATUS_PLANNED, color='e91e63', length=10, length_unit=LENGTH_UNIT_METER).save() - Cable(termination_a=interfaces[11], termination_b=interfaces[0], label='Cable 6', type=CABLE_TYPE_CAT6, status=CONNECTION_STATUS_PLANNED, color='e91e63', length=20, length_unit=LENGTH_UNIT_METER).save() + Cable(termination_a=interfaces[1], termination_b=interfaces[2], label='Cable 1', type=CableTypeChoices.TYPE_CAT3, status=CableStatusChoices.STATUS_CONNECTED, color='aa1409', length=10, length_unit=CableLengthUnitChoices.UNIT_FOOT).save() + Cable(termination_a=interfaces[3], termination_b=interfaces[4], label='Cable 2', type=CableTypeChoices.TYPE_CAT3, status=CableStatusChoices.STATUS_CONNECTED, color='aa1409', length=20, length_unit=CableLengthUnitChoices.UNIT_FOOT).save() + Cable(termination_a=interfaces[5], termination_b=interfaces[6], label='Cable 3', type=CableTypeChoices.TYPE_CAT5E, status=CableStatusChoices.STATUS_CONNECTED, color='f44336', length=30, length_unit=CableLengthUnitChoices.UNIT_FOOT).save() + Cable(termination_a=interfaces[7], termination_b=interfaces[8], label='Cable 4', type=CableTypeChoices.TYPE_CAT5E, status=CableStatusChoices.STATUS_PLANNED, color='f44336', length=40, length_unit=CableLengthUnitChoices.UNIT_FOOT).save() + Cable(termination_a=interfaces[9], termination_b=interfaces[10], label='Cable 5', type=CableTypeChoices.TYPE_CAT6, status=CableStatusChoices.STATUS_PLANNED, color='e91e63', length=10, length_unit=CableLengthUnitChoices.UNIT_METER).save() + Cable(termination_a=interfaces[11], termination_b=interfaces[0], label='Cable 6', type=CableTypeChoices.TYPE_CAT6, status=CableStatusChoices.STATUS_PLANNED, color='e91e63', length=20, length_unit=CableLengthUnitChoices.UNIT_METER).save() def test_id(self): id_list = self.queryset.values_list('id', flat=True)[:2] @@ -2193,15 +2193,17 @@ class CableTestCase(TestCase): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) def test_length_unit(self): - params = {'length_unit': LENGTH_UNIT_FOOT} + params = {'length_unit': CableLengthUnitChoices.UNIT_FOOT} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) def test_type(self): - params = {'type': [CABLE_TYPE_CAT3, CABLE_TYPE_CAT5E]} + params = {'type': [CableTypeChoices.TYPE_CAT3, CableTypeChoices.TYPE_CAT5E]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) def test_status(self): - params = {'status': [CONNECTION_STATUS_CONNECTED]} + params = {'status': [CableStatusChoices.STATUS_CONNECTED]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3) + params = {'status': [CableStatusChoices.STATUS_PLANNED]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3) def test_color(self): @@ -2239,7 +2241,7 @@ class CableTestCase(TestCase): class PowerPanelTestCase(TestCase): queryset = PowerPanel.objects.all() - filterset = PowerPanelFilter + filterset = PowerPanelFilterSet @classmethod def setUpTestData(cls): @@ -2299,7 +2301,7 @@ class PowerPanelTestCase(TestCase): class PowerFeedTestCase(TestCase): queryset = PowerFeed.objects.all() - filterset = PowerFeedFilter + filterset = PowerFeedFilterSet @classmethod def setUpTestData(cls): @@ -2334,9 +2336,9 @@ class PowerFeedTestCase(TestCase): PowerPanel.objects.bulk_create(power_panels) power_feeds = ( - PowerFeed(power_panel=power_panels[0], rack=racks[0], name='Power Feed 1', status=POWERFEED_STATUS_ACTIVE, type=POWERFEED_TYPE_PRIMARY, supply=POWERFEED_SUPPLY_AC, phase=POWERFEED_PHASE_3PHASE, voltage=100, amperage=100, max_utilization=10), - PowerFeed(power_panel=power_panels[1], rack=racks[1], name='Power Feed 2', status=POWERFEED_STATUS_FAILED, type=POWERFEED_TYPE_PRIMARY, supply=POWERFEED_SUPPLY_AC, phase=POWERFEED_PHASE_3PHASE, voltage=200, amperage=200, max_utilization=20), - PowerFeed(power_panel=power_panels[2], rack=racks[2], name='Power Feed 3', status=POWERFEED_STATUS_OFFLINE, type=POWERFEED_TYPE_REDUNDANT, supply=POWERFEED_SUPPLY_DC, phase=POWERFEED_PHASE_SINGLE, voltage=300, amperage=300, max_utilization=30), + PowerFeed(power_panel=power_panels[0], rack=racks[0], name='Power Feed 1', status=PowerFeedStatusChoices.STATUS_ACTIVE, type=PowerFeedTypeChoices.TYPE_PRIMARY, supply=PowerFeedSupplyChoices.SUPPLY_AC, phase=PowerFeedPhaseChoices.PHASE_3PHASE, voltage=100, amperage=100, max_utilization=10), + PowerFeed(power_panel=power_panels[1], rack=racks[1], name='Power Feed 2', status=PowerFeedStatusChoices.STATUS_FAILED, type=PowerFeedTypeChoices.TYPE_PRIMARY, supply=PowerFeedSupplyChoices.SUPPLY_AC, phase=PowerFeedPhaseChoices.PHASE_3PHASE, voltage=200, amperage=200, max_utilization=20), + PowerFeed(power_panel=power_panels[2], rack=racks[2], name='Power Feed 3', status=PowerFeedStatusChoices.STATUS_OFFLINE, type=PowerFeedTypeChoices.TYPE_REDUNDANT, supply=PowerFeedSupplyChoices.SUPPLY_DC, phase=PowerFeedPhaseChoices.PHASE_SINGLE, voltage=300, amperage=300, max_utilization=30), ) PowerFeed.objects.bulk_create(power_feeds) @@ -2346,19 +2348,19 @@ class PowerFeedTestCase(TestCase): def test_status(self): # TODO: Test for multiple values - params = {'status': POWERFEED_STATUS_ACTIVE} + params = {'status': PowerFeedStatusChoices.STATUS_ACTIVE} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) def test_type(self): - params = {'type': POWERFEED_TYPE_PRIMARY} + params = {'type': PowerFeedTypeChoices.TYPE_PRIMARY} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_supply(self): - params = {'supply': POWERFEED_SUPPLY_AC} + params = {'supply': PowerFeedSupplyChoices.SUPPLY_AC} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_phase(self): - params = {'phase': POWERFEED_PHASE_3PHASE} + params = {'phase': PowerFeedPhaseChoices.PHASE_3PHASE} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_voltage(self): diff --git a/netbox/dcim/tests/test_forms.py b/netbox/dcim/tests/test_forms.py index 2f333ea69..d7a946568 100644 --- a/netbox/dcim/tests/test_forms.py +++ b/netbox/dcim/tests/test_forms.py @@ -21,10 +21,10 @@ class DeviceTestCase(TestCase): 'device_type': get_id(DeviceType, 'qfx5100-48s'), 'site': get_id(Site, 'test1'), 'rack': '1', - 'face': RACK_FACE_FRONT, + 'face': DeviceFaceChoices.FACE_FRONT, 'position': 41, 'platform': get_id(Platform, 'juniper-junos'), - 'status': DEVICE_STATUS_ACTIVE, + 'status': DeviceStatusChoices.STATUS_ACTIVE, }) self.assertTrue(test.is_valid(), test.fields['position'].choices) self.assertTrue(test.save()) @@ -38,10 +38,10 @@ class DeviceTestCase(TestCase): 'device_type': get_id(DeviceType, 'qfx5100-48s'), 'site': get_id(Site, 'test1'), 'rack': '1', - 'face': RACK_FACE_FRONT, + 'face': DeviceFaceChoices.FACE_FRONT, 'position': 1, 'platform': get_id(Platform, 'juniper-junos'), - 'status': DEVICE_STATUS_ACTIVE, + 'status': DeviceStatusChoices.STATUS_ACTIVE, }) self.assertFalse(test.is_valid()) @@ -54,10 +54,10 @@ class DeviceTestCase(TestCase): 'device_type': get_id(DeviceType, 'cwg-24vym415c9'), 'site': get_id(Site, 'test1'), 'rack': '1', - 'face': None, + 'face': '', 'position': None, 'platform': None, - 'status': DEVICE_STATUS_ACTIVE, + 'status': DeviceStatusChoices.STATUS_ACTIVE, }) self.assertTrue(test.is_valid()) self.assertTrue(test.save()) @@ -71,10 +71,10 @@ class DeviceTestCase(TestCase): 'device_type': get_id(DeviceType, 'cwg-24vym415c9'), 'site': get_id(Site, 'test1'), 'rack': '1', - 'face': RACK_FACE_REAR, + 'face': DeviceFaceChoices.FACE_REAR, 'position': None, 'platform': None, - 'status': DEVICE_STATUS_ACTIVE, + 'status': DeviceStatusChoices.STATUS_ACTIVE, }) self.assertTrue(test.is_valid()) self.assertTrue(test.save()) diff --git a/netbox/dcim/tests/test_models.py b/netbox/dcim/tests/test_models.py index eba81b136..7573d2cc4 100644 --- a/netbox/dcim/tests/test_models.py +++ b/netbox/dcim/tests/test_models.py @@ -1,6 +1,10 @@ +from django.core.exceptions import ValidationError from django.test import TestCase +from dcim.choices import * +from dcim.constants import CONNECTION_STATUS_CONNECTED, CONNECTION_STATUS_PLANNED from dcim.models import * +from tenancy.models import Tenant class RackTestCase(TestCase): @@ -87,7 +91,7 @@ class RackTestCase(TestCase): site=self.site1, rack=rack1, position=43, - face=RACK_FACE_FRONT, + face=DeviceFaceChoices.FACE_FRONT, ) device1.save() @@ -117,7 +121,7 @@ class RackTestCase(TestCase): site=self.site1, rack=self.rack, position=10, - face=RACK_FACE_REAR, + face=DeviceFaceChoices.FACE_REAR, ) device1.save() @@ -125,14 +129,14 @@ class RackTestCase(TestCase): self.assertEqual(list(self.rack.units), list(reversed(range(1, 43)))) # Validate inventory (front face) - rack1_inventory_front = self.rack.get_front_elevation() + rack1_inventory_front = self.rack.get_rack_units(face=DeviceFaceChoices.FACE_FRONT) self.assertEqual(rack1_inventory_front[-10]['device'], device1) del(rack1_inventory_front[-10]) for u in rack1_inventory_front: self.assertIsNone(u['device']) # Validate inventory (rear face) - rack1_inventory_rear = self.rack.get_rear_elevation() + rack1_inventory_rear = self.rack.get_rack_units(face=DeviceFaceChoices.FACE_REAR) self.assertEqual(rack1_inventory_rear[-10]['device'], device1) del(rack1_inventory_rear[-10]) for u in rack1_inventory_rear: @@ -146,7 +150,7 @@ class RackTestCase(TestCase): site=self.site1, rack=self.rack, position=None, - face=None, + face='', ) self.assertTrue(pdu) @@ -187,20 +191,20 @@ class DeviceTestCase(TestCase): device_type=self.device_type, name='Power Outlet 1', power_port=ppt, - feed_leg=POWERFEED_LEG_A + feed_leg=PowerOutletFeedLegChoices.FEED_LEG_A ).save() InterfaceTemplate( device_type=self.device_type, name='Interface 1', - type=IFACE_TYPE_1GE_FIXED, + type=InterfaceTypeChoices.TYPE_1GE_FIXED, mgmt_only=True ).save() rpt = RearPortTemplate( device_type=self.device_type, name='Rear Port 1', - type=PORT_TYPE_8P8C, + type=PortTypeChoices.TYPE_8P8C, positions=8 ) rpt.save() @@ -208,7 +212,7 @@ class DeviceTestCase(TestCase): FrontPortTemplate( device_type=self.device_type, name='Front Port 1', - type=PORT_TYPE_8P8C, + type=PortTypeChoices.TYPE_8P8C, rear_port=rpt, rear_port_position=2 ).save() @@ -251,27 +255,27 @@ class DeviceTestCase(TestCase): device=d, name='Power Outlet 1', power_port=pp, - feed_leg=POWERFEED_LEG_A + feed_leg=PowerOutletFeedLegChoices.FEED_LEG_A ) Interface.objects.get( device=d, name='Interface 1', - type=IFACE_TYPE_1GE_FIXED, + type=InterfaceTypeChoices.TYPE_1GE_FIXED, mgmt_only=True ) rp = RearPort.objects.get( device=d, name='Rear Port 1', - type=PORT_TYPE_8P8C, + type=PortTypeChoices.TYPE_8P8C, positions=8 ) FrontPort.objects.get( device=d, name='Front Port 1', - type=PORT_TYPE_8P8C, + type=PortTypeChoices.TYPE_8P8C, rear_port=rp, rear_port_position=2 ) @@ -281,6 +285,42 @@ class DeviceTestCase(TestCase): 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): @@ -382,7 +422,7 @@ class CableTestCase(TestCase): """ A cable cannot terminate to a virtual interface """ - virtual_interface = Interface(device=self.device1, name="V1", type=IFACE_TYPE_VIRTUAL) + virtual_interface = Interface(device=self.device1, name="V1", type=InterfaceTypeChoices.TYPE_VIRTUAL) cable = Cable(termination_a=self.interface2, termination_b=virtual_interface) with self.assertRaises(ValidationError): cable.clean() @@ -391,7 +431,7 @@ class CableTestCase(TestCase): """ A cable cannot terminate to a wireless interface """ - wireless_interface = Interface(device=self.device1, name="W1", type=IFACE_TYPE_80211A) + wireless_interface = Interface(device=self.device1, name="W1", type=InterfaceTypeChoices.TYPE_80211A) cable = Cable(termination_a=self.interface2, termination_b=wireless_interface) with self.assertRaises(ValidationError): cable.clean() @@ -424,16 +464,16 @@ class CablePathTestCase(TestCase): device_type=devicetype, device_role=devicerole, name='Test Panel 2', site=site ) self.rear_port1 = RearPort.objects.create( - device=self.panel1, name='Rear Port 1', type=PORT_TYPE_8P8C + device=self.panel1, name='Rear Port 1', type=PortTypeChoices.TYPE_8P8C ) self.front_port1 = FrontPort.objects.create( - device=self.panel1, name='Front Port 1', type=PORT_TYPE_8P8C, rear_port=self.rear_port1 + device=self.panel1, name='Front Port 1', type=PortTypeChoices.TYPE_8P8C, rear_port=self.rear_port1 ) self.rear_port2 = RearPort.objects.create( - device=self.panel2, name='Rear Port 2', type=PORT_TYPE_8P8C + device=self.panel2, name='Rear Port 2', type=PortTypeChoices.TYPE_8P8C ) self.front_port2 = FrontPort.objects.create( - device=self.panel2, name='Front Port 2', type=PORT_TYPE_8P8C, rear_port=self.rear_port2 + device=self.panel2, name='Front Port 2', type=PortTypeChoices.TYPE_8P8C, rear_port=self.rear_port2 ) def test_path_completion(self): @@ -453,14 +493,18 @@ class CablePathTestCase(TestCase): self.assertIsNone(interface1.connection_status) # Third segment - cable3 = Cable(termination_a=self.front_port2, termination_b=self.interface2, status=CONNECTION_STATUS_PLANNED) + cable3 = Cable( + termination_a=self.front_port2, + termination_b=self.interface2, + status=CableStatusChoices.STATUS_PLANNED + ) cable3.save() interface1 = Interface.objects.get(pk=self.interface1.pk) self.assertEqual(interface1.connected_endpoint, self.interface2) self.assertEqual(interface1.connection_status, CONNECTION_STATUS_PLANNED) # Switch third segment from planned to connected - cable3.status = CONNECTION_STATUS_CONNECTED + cable3.status = CableStatusChoices.STATUS_CONNECTED cable3.save() interface1 = Interface.objects.get(pk=self.interface1.pk) self.assertEqual(interface1.connected_endpoint, self.interface2) diff --git a/netbox/dcim/tests/test_views.py b/netbox/dcim/tests/test_views.py index 6e34b8ae9..856862a3e 100644 --- a/netbox/dcim/tests/test_views.py +++ b/netbox/dcim/tests/test_views.py @@ -1,20 +1,24 @@ import urllib.parse +import yaml from django.test import Client, TestCase from django.urls import reverse -from dcim.constants import CABLE_TYPE_CAT6, IFACE_TYPE_1GE_FIXED -from dcim.models import ( - Cable, Device, DeviceRole, DeviceType, Interface, InventoryItem, Manufacturer, Platform, Rack, RackGroup, - RackReservation, RackRole, Site, Region, VirtualChassis, -) +from dcim.choices import * +from dcim.constants import * +from dcim.models import * from utilities.testing import create_test_user class RegionTestCase(TestCase): def setUp(self): - user = create_test_user(permissions=['dcim.view_region']) + user = create_test_user( + permissions=[ + 'dcim.view_region', + 'dcim.add_region', + ] + ) self.client = Client() self.client.force_login(user) @@ -29,11 +33,30 @@ class RegionTestCase(TestCase): response = self.client.get(url) self.assertEqual(response.status_code, 200) + def test_region_import(self): + + csv_data = ( + "name,slug", + "Region 4,region-4", + "Region 5,region-5", + "Region 6,region-6", + ) + + response = self.client.post(reverse('dcim:region_import'), {'csv': '\n'.join(csv_data)}) + + self.assertEqual(response.status_code, 200) + self.assertEqual(Region.objects.count(), 6) + class SiteTestCase(TestCase): def setUp(self): - user = create_test_user(permissions=['dcim.view_site']) + user = create_test_user( + permissions=[ + 'dcim.view_site', + 'dcim.add_site', + ] + ) self.client = Client() self.client.force_login(user) @@ -62,11 +85,30 @@ class SiteTestCase(TestCase): response = self.client.get(site.get_absolute_url()) self.assertEqual(response.status_code, 200) + def test_site_import(self): + + csv_data = ( + "name,slug", + "Site 4,site-4", + "Site 5,site-5", + "Site 6,site-6", + ) + + response = self.client.post(reverse('dcim:site_import'), {'csv': '\n'.join(csv_data)}) + + self.assertEqual(response.status_code, 200) + self.assertEqual(Site.objects.count(), 6) + class RackGroupTestCase(TestCase): def setUp(self): - user = create_test_user(permissions=['dcim.view_rackgroup']) + user = create_test_user( + permissions=[ + 'dcim.view_rackgroup', + 'dcim.add_rackgroup', + ] + ) self.client = Client() self.client.force_login(user) @@ -86,11 +128,30 @@ class RackGroupTestCase(TestCase): response = self.client.get(url) self.assertEqual(response.status_code, 200) + def test_rackgroup_import(self): + + csv_data = ( + "site,name,slug", + "Site 1,Rack Group 4,rack-group-4", + "Site 1,Rack Group 5,rack-group-5", + "Site 1,Rack Group 6,rack-group-6", + ) + + response = self.client.post(reverse('dcim:rackgroup_import'), {'csv': '\n'.join(csv_data)}) + + self.assertEqual(response.status_code, 200) + self.assertEqual(RackGroup.objects.count(), 6) + class RackRoleTestCase(TestCase): def setUp(self): - user = create_test_user(permissions=['dcim.view_rackrole']) + user = create_test_user( + permissions=[ + 'dcim.view_rackrole', + 'dcim.add_rackrole', + ] + ) self.client = Client() self.client.force_login(user) @@ -107,6 +168,20 @@ class RackRoleTestCase(TestCase): response = self.client.get(url) self.assertEqual(response.status_code, 200) + def test_rackrole_import(self): + + csv_data = ( + "name,slug,color", + "Rack Role 4,rack-role-4,ff0000", + "Rack Role 5,rack-role-5,00ff00", + "Rack Role 6,rack-role-6,0000ff", + ) + + response = self.client.post(reverse('dcim:rackrole_import'), {'csv': '\n'.join(csv_data)}) + + self.assertEqual(response.status_code, 200) + self.assertEqual(RackRole.objects.count(), 6) + class RackReservationTestCase(TestCase): @@ -138,7 +213,12 @@ class RackReservationTestCase(TestCase): class RackTestCase(TestCase): def setUp(self): - user = create_test_user(permissions=['dcim.view_rack']) + user = create_test_user( + permissions=[ + 'dcim.view_rack', + 'dcim.add_rack', + ] + ) self.client = Client() self.client.force_login(user) @@ -167,11 +247,30 @@ class RackTestCase(TestCase): response = self.client.get(rack.get_absolute_url()) self.assertEqual(response.status_code, 200) + def test_rack_import(self): + + csv_data = ( + "site,name,width,u_height", + "Site 1,Rack 4,19,42", + "Site 1,Rack 5,19,42", + "Site 1,Rack 6,19,42", + ) + + response = self.client.post(reverse('dcim:rack_import'), {'csv': '\n'.join(csv_data)}) + + self.assertEqual(response.status_code, 200) + self.assertEqual(Rack.objects.count(), 6) + class ManufacturerTypeTestCase(TestCase): def setUp(self): - user = create_test_user(permissions=['dcim.view_manufacturer']) + user = create_test_user( + permissions=[ + 'dcim.view_manufacturer', + 'dcim.add_manufacturer', + ] + ) self.client = Client() self.client.force_login(user) @@ -188,6 +287,20 @@ class ManufacturerTypeTestCase(TestCase): response = self.client.get(url) self.assertEqual(response.status_code, 200) + def test_manufacturer_import(self): + + csv_data = ( + "name,slug", + "Manufacturer 4,manufacturer-4", + "Manufacturer 5,manufacturer-5", + "Manufacturer 6,manufacturer-6", + ) + + response = self.client.post(reverse('dcim:manufacturer_import'), {'csv': '\n'.join(csv_data)}) + + self.assertEqual(response.status_code, 200) + self.assertEqual(Manufacturer.objects.count(), 6) + class DeviceTypeTestCase(TestCase): @@ -215,17 +328,175 @@ class DeviceTypeTestCase(TestCase): response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params))) self.assertEqual(response.status_code, 200) + def test_devicetype_export(self): + + url = reverse('dcim:devicetype_list') + + response = self.client.get('{}?export'.format(url)) + self.assertEqual(response.status_code, 200) + data = list(yaml.load_all(response.content, Loader=yaml.SafeLoader)) + self.assertEqual(len(data), 3) + self.assertEqual(data[0]['manufacturer'], 'Manufacturer 1') + self.assertEqual(data[0]['model'], 'Device Type 1') + def test_devicetype(self): devicetype = DeviceType.objects.first() response = self.client.get(devicetype.get_absolute_url()) self.assertEqual(response.status_code, 200) + def test_devicetype_import(self): + + IMPORT_DATA = """ +manufacturer: Generic +model: TEST-1000 +slug: test-1000 +u_height: 2 +console-ports: + - name: Console Port 1 + type: de-9 + - name: Console Port 2 + type: de-9 + - name: Console Port 3 + type: de-9 +console-server-ports: + - name: Console Server Port 1 + type: rj-45 + - name: Console Server Port 2 + type: rj-45 + - name: Console Server Port 3 + type: rj-45 +power-ports: + - name: Power Port 1 + type: iec-60320-c14 + - name: Power Port 2 + type: iec-60320-c14 + - name: Power Port 3 + type: iec-60320-c14 +power-outlets: + - name: Power Outlet 1 + type: iec-60320-c13 + power_port: Power Port 1 + feed_leg: A + - name: Power Outlet 2 + type: iec-60320-c13 + power_port: Power Port 1 + feed_leg: A + - name: Power Outlet 3 + type: iec-60320-c13 + power_port: Power Port 1 + feed_leg: A +interfaces: + - name: Interface 1 + type: 1000base-t + mgmt_only: true + - name: Interface 2 + type: 1000base-t + - name: Interface 3 + type: 1000base-t +rear-ports: + - name: Rear Port 1 + type: 8p8c + - name: Rear Port 2 + type: 8p8c + - name: Rear Port 3 + type: 8p8c +front-ports: + - name: Front Port 1 + type: 8p8c + rear_port: Rear Port 1 + - name: Front Port 2 + type: 8p8c + rear_port: Rear Port 2 + - name: Front Port 3 + type: 8p8c + rear_port: Rear Port 3 +device-bays: + - name: Device Bay 1 + - name: Device Bay 2 + - name: Device Bay 3 +""" + + # Create the manufacturer + Manufacturer(name='Generic', slug='generic').save() + + # Authenticate as user with necessary permissions + user = create_test_user(username='testuser2', permissions=[ + 'dcim.view_devicetype', + 'dcim.add_devicetype', + 'dcim.add_consoleporttemplate', + 'dcim.add_consoleserverporttemplate', + 'dcim.add_powerporttemplate', + 'dcim.add_poweroutlettemplate', + 'dcim.add_interfacetemplate', + 'dcim.add_frontporttemplate', + 'dcim.add_rearporttemplate', + 'dcim.add_devicebaytemplate', + ]) + self.client.force_login(user) + + form_data = { + 'data': IMPORT_DATA, + 'format': 'yaml' + } + response = self.client.post(reverse('dcim:devicetype_import'), data=form_data, follow=True) + self.assertEqual(response.status_code, 200) + + dt = DeviceType.objects.get(model='TEST-1000') + + # Verify all of the components were created + self.assertEqual(dt.consoleport_templates.count(), 3) + cp1 = ConsolePortTemplate.objects.first() + self.assertEqual(cp1.name, 'Console Port 1') + self.assertEqual(cp1.type, ConsolePortTypeChoices.TYPE_DE9) + + self.assertEqual(dt.consoleserverport_templates.count(), 3) + csp1 = ConsoleServerPortTemplate.objects.first() + self.assertEqual(csp1.name, 'Console Server Port 1') + self.assertEqual(csp1.type, ConsolePortTypeChoices.TYPE_RJ45) + + self.assertEqual(dt.powerport_templates.count(), 3) + pp1 = PowerPortTemplate.objects.first() + self.assertEqual(pp1.name, 'Power Port 1') + self.assertEqual(pp1.type, PowerPortTypeChoices.TYPE_IEC_C14) + + self.assertEqual(dt.poweroutlet_templates.count(), 3) + po1 = PowerOutletTemplate.objects.first() + self.assertEqual(po1.name, 'Power Outlet 1') + self.assertEqual(po1.type, PowerOutletTypeChoices.TYPE_IEC_C13) + self.assertEqual(po1.power_port, pp1) + self.assertEqual(po1.feed_leg, PowerOutletFeedLegChoices.FEED_LEG_A) + + self.assertEqual(dt.interface_templates.count(), 3) + iface1 = InterfaceTemplate.objects.first() + self.assertEqual(iface1.name, 'Interface 1') + self.assertEqual(iface1.type, InterfaceTypeChoices.TYPE_1GE_FIXED) + self.assertTrue(iface1.mgmt_only) + + self.assertEqual(dt.rearport_templates.count(), 3) + rp1 = RearPortTemplate.objects.first() + self.assertEqual(rp1.name, 'Rear Port 1') + + self.assertEqual(dt.frontport_templates.count(), 3) + fp1 = FrontPortTemplate.objects.first() + self.assertEqual(fp1.name, 'Front Port 1') + self.assertEqual(fp1.rear_port, rp1) + self.assertEqual(fp1.rear_port_position, 1) + + self.assertEqual(dt.device_bay_templates.count(), 3) + db1 = DeviceBayTemplate.objects.first() + self.assertEqual(db1.name, 'Device Bay 1') + class DeviceRoleTestCase(TestCase): def setUp(self): - user = create_test_user(permissions=['dcim.view_devicerole']) + user = create_test_user( + permissions=[ + 'dcim.view_devicerole', + 'dcim.add_devicerole', + ] + ) self.client = Client() self.client.force_login(user) @@ -242,11 +513,30 @@ class DeviceRoleTestCase(TestCase): response = self.client.get(url) self.assertEqual(response.status_code, 200) + def test_devicerole_import(self): + + csv_data = ( + "name,slug,color", + "Device Role 4,device-role-4,ff0000", + "Device Role 5,device-role-5,00ff00", + "Device Role 6,device-role-6,0000ff", + ) + + response = self.client.post(reverse('dcim:devicerole_import'), {'csv': '\n'.join(csv_data)}) + + self.assertEqual(response.status_code, 200) + self.assertEqual(DeviceRole.objects.count(), 6) + class PlatformTestCase(TestCase): def setUp(self): - user = create_test_user(permissions=['dcim.view_platform']) + user = create_test_user( + permissions=[ + 'dcim.view_platform', + 'dcim.add_platform', + ] + ) self.client = Client() self.client.force_login(user) @@ -263,11 +553,30 @@ class PlatformTestCase(TestCase): response = self.client.get(url) self.assertEqual(response.status_code, 200) + def test_platform_import(self): + + csv_data = ( + "name,slug", + "Platform 4,platform-4", + "Platform 5,platform-5", + "Platform 6,platform-6", + ) + + response = self.client.post(reverse('dcim:platform_import'), {'csv': '\n'.join(csv_data)}) + + self.assertEqual(response.status_code, 200) + self.assertEqual(Platform.objects.count(), 6) + class DeviceTestCase(TestCase): def setUp(self): - user = create_test_user(permissions=['dcim.view_device']) + user = create_test_user( + permissions=[ + 'dcim.view_device', + 'dcim.add_device', + ] + ) self.client = Client() self.client.force_login(user) @@ -306,11 +615,486 @@ class DeviceTestCase(TestCase): response = self.client.get(device.get_absolute_url()) self.assertEqual(response.status_code, 200) + def test_device_import(self): + + csv_data = ( + "device_role,manufacturer,model_name,status,site,name", + "Device Role 1,Manufacturer 1,Device Type 1,Active,Site 1,Device 4", + "Device Role 1,Manufacturer 1,Device Type 1,Active,Site 1,Device 5", + "Device Role 1,Manufacturer 1,Device Type 1,Active,Site 1,Device 6", + ) + + response = self.client.post(reverse('dcim:device_import'), {'csv': '\n'.join(csv_data)}) + + self.assertEqual(response.status_code, 200) + self.assertEqual(Device.objects.count(), 6) + + +class ConsolePortTestCase(TestCase): + + def setUp(self): + user = create_test_user( + permissions=[ + 'dcim.view_consoleport', + 'dcim.add_consoleport', + ] + ) + self.client = Client() + self.client.force_login(user) + + site = Site(name='Site 1', slug='site-1') + site.save() + + manufacturer = Manufacturer(name='Manufacturer 1', slug='manufacturer-1') + manufacturer.save() + + devicetype = DeviceType(model='Device Type 1', manufacturer=manufacturer) + devicetype.save() + + devicerole = DeviceRole(name='Device Role 1', slug='device-role-1') + devicerole.save() + + device = Device(name='Device 1', site=site, device_type=devicetype, device_role=devicerole) + device.save() + + ConsolePort.objects.bulk_create([ + ConsolePort(device=device, name='Console Port 1'), + ConsolePort(device=device, name='Console Port 2'), + ConsolePort(device=device, name='Console Port 3'), + ]) + + def test_consoleport_list(self): + + url = reverse('dcim:consoleport_list') + + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + + def test_consoleport_import(self): + + csv_data = ( + "device,name", + "Device 1,Console Port 4", + "Device 1,Console Port 5", + "Device 1,Console Port 6", + ) + + response = self.client.post(reverse('dcim:consoleport_import'), {'csv': '\n'.join(csv_data)}) + + self.assertEqual(response.status_code, 200) + self.assertEqual(ConsolePort.objects.count(), 6) + + +class ConsoleServerPortTestCase(TestCase): + + def setUp(self): + user = create_test_user( + permissions=[ + 'dcim.view_consoleserverport', + 'dcim.add_consoleserverport', + ] + ) + self.client = Client() + self.client.force_login(user) + + site = Site(name='Site 1', slug='site-1') + site.save() + + manufacturer = Manufacturer(name='Manufacturer 1', slug='manufacturer-1') + manufacturer.save() + + devicetype = DeviceType(model='Device Type 1', manufacturer=manufacturer) + devicetype.save() + + devicerole = DeviceRole(name='Device Role 1', slug='device-role-1') + devicerole.save() + + device = Device(name='Device 1', site=site, device_type=devicetype, device_role=devicerole) + device.save() + + ConsoleServerPort.objects.bulk_create([ + ConsoleServerPort(device=device, name='Console Server Port 1'), + ConsoleServerPort(device=device, name='Console Server Port 2'), + ConsoleServerPort(device=device, name='Console Server Port 3'), + ]) + + def test_consoleserverport_list(self): + + url = reverse('dcim:consoleserverport_list') + + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + + def test_consoleserverport_import(self): + + csv_data = ( + "device,name", + "Device 1,Console Server Port 4", + "Device 1,Console Server Port 5", + "Device 1,Console Server Port 6", + ) + + response = self.client.post(reverse('dcim:consoleserverport_import'), {'csv': '\n'.join(csv_data)}) + + self.assertEqual(response.status_code, 200) + self.assertEqual(ConsoleServerPort.objects.count(), 6) + + +class PowerPortTestCase(TestCase): + + def setUp(self): + user = create_test_user( + permissions=[ + 'dcim.view_powerport', + 'dcim.add_powerport', + ] + ) + self.client = Client() + self.client.force_login(user) + + site = Site(name='Site 1', slug='site-1') + site.save() + + manufacturer = Manufacturer(name='Manufacturer 1', slug='manufacturer-1') + manufacturer.save() + + devicetype = DeviceType(model='Device Type 1', manufacturer=manufacturer) + devicetype.save() + + devicerole = DeviceRole(name='Device Role 1', slug='device-role-1') + devicerole.save() + + device = Device(name='Device 1', site=site, device_type=devicetype, device_role=devicerole) + device.save() + + PowerPort.objects.bulk_create([ + PowerPort(device=device, name='Power Port 1'), + PowerPort(device=device, name='Power Port 2'), + PowerPort(device=device, name='Power Port 3'), + ]) + + def test_powerport_list(self): + + url = reverse('dcim:powerport_list') + + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + + def test_powerport_import(self): + + csv_data = ( + "device,name", + "Device 1,Power Port 4", + "Device 1,Power Port 5", + "Device 1,Power Port 6", + ) + + response = self.client.post(reverse('dcim:powerport_import'), {'csv': '\n'.join(csv_data)}) + + self.assertEqual(response.status_code, 200) + self.assertEqual(PowerPort.objects.count(), 6) + + +class PowerOutletTestCase(TestCase): + + def setUp(self): + user = create_test_user( + permissions=[ + 'dcim.view_poweroutlet', + 'dcim.add_poweroutlet', + ] + ) + self.client = Client() + self.client.force_login(user) + + site = Site(name='Site 1', slug='site-1') + site.save() + + manufacturer = Manufacturer(name='Manufacturer 1', slug='manufacturer-1') + manufacturer.save() + + devicetype = DeviceType(model='Device Type 1', manufacturer=manufacturer) + devicetype.save() + + devicerole = DeviceRole(name='Device Role 1', slug='device-role-1') + devicerole.save() + + device = Device(name='Device 1', site=site, device_type=devicetype, device_role=devicerole) + device.save() + + PowerOutlet.objects.bulk_create([ + PowerOutlet(device=device, name='Power Outlet 1'), + PowerOutlet(device=device, name='Power Outlet 2'), + PowerOutlet(device=device, name='Power Outlet 3'), + ]) + + def test_poweroutlet_list(self): + + url = reverse('dcim:poweroutlet_list') + + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + + def test_poweroutlet_import(self): + + csv_data = ( + "device,name", + "Device 1,Power Outlet 4", + "Device 1,Power Outlet 5", + "Device 1,Power Outlet 6", + ) + + response = self.client.post(reverse('dcim:poweroutlet_import'), {'csv': '\n'.join(csv_data)}) + + self.assertEqual(response.status_code, 200) + self.assertEqual(PowerOutlet.objects.count(), 6) + + +class InterfaceTestCase(TestCase): + + def setUp(self): + user = create_test_user( + permissions=[ + 'dcim.view_interface', + 'dcim.add_interface', + ] + ) + self.client = Client() + self.client.force_login(user) + + site = Site(name='Site 1', slug='site-1') + site.save() + + manufacturer = Manufacturer(name='Manufacturer 1', slug='manufacturer-1') + manufacturer.save() + + devicetype = DeviceType(model='Device Type 1', manufacturer=manufacturer) + devicetype.save() + + devicerole = DeviceRole(name='Device Role 1', slug='device-role-1') + devicerole.save() + + device = Device(name='Device 1', site=site, device_type=devicetype, device_role=devicerole) + device.save() + + Interface.objects.bulk_create([ + Interface(device=device, name='Interface 1'), + Interface(device=device, name='Interface 2'), + Interface(device=device, name='Interface 3'), + ]) + + def test_interface_list(self): + + url = reverse('dcim:interface_list') + + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + + def test_interface_import(self): + + csv_data = ( + "device,name,type", + "Device 1,Interface 4,1000BASE-T (1GE)", + "Device 1,Interface 5,1000BASE-T (1GE)", + "Device 1,Interface 6,1000BASE-T (1GE)", + ) + + response = self.client.post(reverse('dcim:interface_import'), {'csv': '\n'.join(csv_data)}) + + self.assertEqual(response.status_code, 200) + self.assertEqual(Interface.objects.count(), 6) + + +class FrontPortTestCase(TestCase): + + def setUp(self): + user = create_test_user( + permissions=[ + 'dcim.view_frontport', + 'dcim.add_frontport', + ] + ) + self.client = Client() + self.client.force_login(user) + + site = Site(name='Site 1', slug='site-1') + site.save() + + manufacturer = Manufacturer(name='Manufacturer 1', slug='manufacturer-1') + manufacturer.save() + + devicetype = DeviceType(model='Device Type 1', manufacturer=manufacturer) + devicetype.save() + + devicerole = DeviceRole(name='Device Role 1', slug='device-role-1') + devicerole.save() + + device = Device(name='Device 1', site=site, device_type=devicetype, device_role=devicerole) + device.save() + + rearport1 = RearPort(device=device, name='Rear Port 1') + rearport1.save() + rearport2 = RearPort(device=device, name='Rear Port 2') + rearport2.save() + rearport3 = RearPort(device=device, name='Rear Port 3') + rearport3.save() + + # RearPorts for CSV import test + RearPort(device=device, name='Rear Port 4').save() + RearPort(device=device, name='Rear Port 5').save() + RearPort(device=device, name='Rear Port 6').save() + + FrontPort.objects.bulk_create([ + FrontPort(device=device, name='Front Port 1', rear_port=rearport1), + FrontPort(device=device, name='Front Port 2', rear_port=rearport2), + FrontPort(device=device, name='Front Port 3', rear_port=rearport3), + ]) + + def test_frontport_list(self): + + url = reverse('dcim:frontport_list') + + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + + def test_frontport_import(self): + + csv_data = ( + "device,name,type,rear_port,rear_port_position", + "Device 1,Front Port 4,8P8C,Rear Port 4,1", + "Device 1,Front Port 5,8P8C,Rear Port 5,1", + "Device 1,Front Port 6,8P8C,Rear Port 6,1", + ) + + response = self.client.post(reverse('dcim:frontport_import'), {'csv': '\n'.join(csv_data)}) + + self.assertEqual(response.status_code, 200) + self.assertEqual(FrontPort.objects.count(), 6) + + +class RearPortTestCase(TestCase): + + def setUp(self): + user = create_test_user( + permissions=[ + 'dcim.view_rearport', + 'dcim.add_rearport', + ] + ) + self.client = Client() + self.client.force_login(user) + + site = Site(name='Site 1', slug='site-1') + site.save() + + manufacturer = Manufacturer(name='Manufacturer 1', slug='manufacturer-1') + manufacturer.save() + + devicetype = DeviceType(model='Device Type 1', manufacturer=manufacturer) + devicetype.save() + + devicerole = DeviceRole(name='Device Role 1', slug='device-role-1') + devicerole.save() + + device = Device(name='Device 1', site=site, device_type=devicetype, device_role=devicerole) + device.save() + + RearPort.objects.bulk_create([ + RearPort(device=device, name='Rear Port 1'), + RearPort(device=device, name='Rear Port 2'), + RearPort(device=device, name='Rear Port 3'), + ]) + + def test_rearport_list(self): + + url = reverse('dcim:rearport_list') + + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + + def test_rearport_import(self): + + csv_data = ( + "device,name,type,positions", + "Device 1,Rear Port 4,8P8C,1", + "Device 1,Rear Port 5,8P8C,1", + "Device 1,Rear Port 6,8P8C,1", + ) + + response = self.client.post(reverse('dcim:rearport_import'), {'csv': '\n'.join(csv_data)}) + + self.assertEqual(response.status_code, 200) + self.assertEqual(RearPort.objects.count(), 6) + + +class DeviceBayTestCase(TestCase): + + def setUp(self): + user = create_test_user( + permissions=[ + 'dcim.view_devicebay', + 'dcim.add_devicebay', + ] + ) + self.client = Client() + self.client.force_login(user) + + site = Site(name='Site 1', slug='site-1') + site.save() + + manufacturer = Manufacturer(name='Manufacturer 1', slug='manufacturer-1') + manufacturer.save() + + devicetype = DeviceType( + model='Device Type 1', + manufacturer=manufacturer, + subdevice_role=SubdeviceRoleChoices.ROLE_PARENT + ) + devicetype.save() + + devicerole = DeviceRole(name='Device Role 1', slug='device-role-1') + devicerole.save() + + device = Device(name='Device 1', site=site, device_type=devicetype, device_role=devicerole) + device.save() + + DeviceBay.objects.bulk_create([ + DeviceBay(device=device, name='Device Bay 1'), + DeviceBay(device=device, name='Device Bay 2'), + DeviceBay(device=device, name='Device Bay 3'), + ]) + + def test_devicebay_list(self): + + url = reverse('dcim:devicebay_list') + + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + + def test_devicebay_import(self): + + csv_data = ( + "device,name", + "Device 1,Device Bay 4", + "Device 1,Device Bay 5", + "Device 1,Device Bay 6", + ) + + response = self.client.post(reverse('dcim:devicebay_import'), {'csv': '\n'.join(csv_data)}) + + self.assertEqual(response.status_code, 200) + self.assertEqual(DeviceBay.objects.count(), 6) + class InventoryItemTestCase(TestCase): def setUp(self): - user = create_test_user(permissions=['dcim.view_inventoryitem']) + user = create_test_user( + permissions=[ + 'dcim.view_inventoryitem', + 'dcim.add_inventoryitem', + ] + ) self.client = Client() self.client.force_login(user) @@ -345,11 +1129,30 @@ class InventoryItemTestCase(TestCase): response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params))) self.assertEqual(response.status_code, 200) + def test_inventoryitem_import(self): + + csv_data = ( + "device,name", + "Device 1,Inventory Item 4", + "Device 1,Inventory Item 5", + "Device 1,Inventory Item 6", + ) + + response = self.client.post(reverse('dcim:inventoryitem_import'), {'csv': '\n'.join(csv_data)}) + + self.assertEqual(response.status_code, 200) + self.assertEqual(InventoryItem.objects.count(), 6) + class CableTestCase(TestCase): def setUp(self): - user = create_test_user(permissions=['dcim.view_cable']) + user = create_test_user( + permissions=[ + 'dcim.view_cable', + 'dcim.add_cable', + ] + ) self.client = Client() self.client.force_login(user) @@ -369,29 +1172,41 @@ class CableTestCase(TestCase): device1.save() device2 = Device(name='Device 2', site=site, device_type=devicetype, device_role=devicerole) device2.save() + device3 = Device(name='Device 3', site=site, device_type=devicetype, device_role=devicerole) + device3.save() + device4 = Device(name='Device 4', site=site, device_type=devicetype, device_role=devicerole) + device4.save() - iface1 = Interface(device=device1, name='Interface 1', type=IFACE_TYPE_1GE_FIXED) + iface1 = Interface(device=device1, name='Interface 1', type=InterfaceTypeChoices.TYPE_1GE_FIXED) iface1.save() - iface2 = Interface(device=device1, name='Interface 2', type=IFACE_TYPE_1GE_FIXED) + iface2 = Interface(device=device1, name='Interface 2', type=InterfaceTypeChoices.TYPE_1GE_FIXED) iface2.save() - iface3 = Interface(device=device1, name='Interface 3', type=IFACE_TYPE_1GE_FIXED) + iface3 = Interface(device=device1, name='Interface 3', type=InterfaceTypeChoices.TYPE_1GE_FIXED) iface3.save() - iface4 = Interface(device=device2, name='Interface 1', type=IFACE_TYPE_1GE_FIXED) + iface4 = Interface(device=device2, name='Interface 1', type=InterfaceTypeChoices.TYPE_1GE_FIXED) iface4.save() - iface5 = Interface(device=device2, name='Interface 2', type=IFACE_TYPE_1GE_FIXED) + iface5 = Interface(device=device2, name='Interface 2', type=InterfaceTypeChoices.TYPE_1GE_FIXED) iface5.save() - iface6 = Interface(device=device2, name='Interface 3', type=IFACE_TYPE_1GE_FIXED) + iface6 = Interface(device=device2, name='Interface 3', type=InterfaceTypeChoices.TYPE_1GE_FIXED) iface6.save() - Cable(termination_a=iface1, termination_b=iface4, type=CABLE_TYPE_CAT6).save() - Cable(termination_a=iface2, termination_b=iface5, type=CABLE_TYPE_CAT6).save() - Cable(termination_a=iface3, termination_b=iface6, type=CABLE_TYPE_CAT6).save() + # Interfaces for CSV import testing + Interface(device=device3, name='Interface 1', type=InterfaceTypeChoices.TYPE_1GE_FIXED).save() + Interface(device=device3, name='Interface 2', type=InterfaceTypeChoices.TYPE_1GE_FIXED).save() + Interface(device=device3, name='Interface 3', type=InterfaceTypeChoices.TYPE_1GE_FIXED).save() + Interface(device=device4, name='Interface 1', type=InterfaceTypeChoices.TYPE_1GE_FIXED).save() + Interface(device=device4, name='Interface 2', type=InterfaceTypeChoices.TYPE_1GE_FIXED).save() + Interface(device=device4, name='Interface 3', type=InterfaceTypeChoices.TYPE_1GE_FIXED).save() + + Cable(termination_a=iface1, termination_b=iface4, type=CableTypeChoices.TYPE_CAT6).save() + Cable(termination_a=iface2, termination_b=iface5, type=CableTypeChoices.TYPE_CAT6).save() + Cable(termination_a=iface3, termination_b=iface6, type=CableTypeChoices.TYPE_CAT6).save() def test_cable_list(self): url = reverse('dcim:cable_list') params = { - "type": CABLE_TYPE_CAT6, + "type": CableTypeChoices.TYPE_CAT6, } response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params))) @@ -403,6 +1218,20 @@ class CableTestCase(TestCase): response = self.client.get(cable.get_absolute_url()) self.assertEqual(response.status_code, 200) + def test_cable_import(self): + + csv_data = ( + "side_a_device,side_a_type,side_a_name,side_b_device,side_b_type,side_b_name", + "Device 3,interface,Interface 1,Device 4,interface,Interface 1", + "Device 3,interface,Interface 2,Device 4,interface,Interface 2", + "Device 3,interface,Interface 3,Device 4,interface,Interface 3", + ) + + response = self.client.post(reverse('dcim:cable_import'), {'csv': '\n'.join(csv_data)}) + + self.assertEqual(response.status_code, 200) + self.assertEqual(Cable.objects.count(), 6) + class VirtualChassisTestCase(TestCase): diff --git a/netbox/dcim/urls.py b/netbox/dcim/urls.py index c3e852d1e..956b49bc4 100644 --- a/netbox/dcim/urls.py +++ b/netbox/dcim/urls.py @@ -82,7 +82,7 @@ urlpatterns = [ # Device types path(r'device-types/', views.DeviceTypeListView.as_view(), name='devicetype_list'), path(r'device-types/add/', views.DeviceTypeCreateView.as_view(), name='devicetype_add'), - path(r'device-types/import/', views.DeviceTypeBulkImportView.as_view(), name='devicetype_import'), + path(r'device-types/import/', views.DeviceTypeImportView.as_view(), name='devicetype_import'), path(r'device-types/edit/', views.DeviceTypeBulkEditView.as_view(), name='devicetype_bulk_edit'), path(r'device-types/delete/', views.DeviceTypeBulkDeleteView.as_view(), name='devicetype_bulk_delete'), path(r'device-types//', views.DeviceTypeView.as_view(), name='devicetype'), @@ -171,49 +171,58 @@ urlpatterns = [ path(r'devices/console-ports/add/', views.DeviceBulkAddConsolePortView.as_view(), name='device_bulk_add_consoleport'), path(r'devices//console-ports/add/', views.ConsolePortCreateView.as_view(), name='consoleport_add'), path(r'devices//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//connect//', views.CableCreateView.as_view(), name='consoleport_connect', kwargs={'termination_a_type': ConsolePort}), path(r'console-ports//edit/', views.ConsolePortEditView.as_view(), name='consoleport_edit'), path(r'console-ports//delete/', views.ConsolePortDeleteView.as_view(), name='consoleport_delete'), path(r'console-ports//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 path(r'devices/console-server-ports/add/', views.DeviceBulkAddConsoleServerPortView.as_view(), name='device_bulk_add_consoleserverport'), path(r'devices//console-server-ports/add/', views.ConsoleServerPortCreateView.as_view(), name='consoleserverport_add'), path(r'devices//console-server-ports/edit/', views.ConsoleServerPortBulkEditView.as_view(), name='consoleserverport_bulk_edit'), path(r'devices//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//connect//', views.CableCreateView.as_view(), name='consoleserverport_connect', kwargs={'termination_a_type': ConsoleServerPort}), path(r'console-server-ports//edit/', views.ConsoleServerPortEditView.as_view(), name='consoleserverport_edit'), path(r'console-server-ports//delete/', views.ConsoleServerPortDeleteView.as_view(), name='consoleserverport_delete'), path(r'console-server-ports//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/disconnect/', views.ConsoleServerPortBulkDisconnectView.as_view(), name='consoleserverport_bulk_disconnect'), + path(r'console-server-ports/import/', views.ConsoleServerPortBulkImportView.as_view(), name='consoleserverport_import'), # Power ports path(r'devices/power-ports/add/', views.DeviceBulkAddPowerPortView.as_view(), name='device_bulk_add_powerport'), path(r'devices//power-ports/add/', views.PowerPortCreateView.as_view(), name='powerport_add'), path(r'devices//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//connect//', views.CableCreateView.as_view(), name='powerport_connect', kwargs={'termination_a_type': PowerPort}), path(r'power-ports//edit/', views.PowerPortEditView.as_view(), name='powerport_edit'), path(r'power-ports//delete/', views.PowerPortDeleteView.as_view(), name='powerport_delete'), path(r'power-ports//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 path(r'devices/power-outlets/add/', views.DeviceBulkAddPowerOutletView.as_view(), name='device_bulk_add_poweroutlet'), path(r'devices//power-outlets/add/', views.PowerOutletCreateView.as_view(), name='poweroutlet_add'), path(r'devices//power-outlets/edit/', views.PowerOutletBulkEditView.as_view(), name='poweroutlet_bulk_edit'), path(r'devices//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//connect//', views.CableCreateView.as_view(), name='poweroutlet_connect', kwargs={'termination_a_type': PowerOutlet}), path(r'power-outlets//edit/', views.PowerOutletEditView.as_view(), name='poweroutlet_edit'), path(r'power-outlets//delete/', views.PowerOutletDeleteView.as_view(), name='poweroutlet_delete'), path(r'power-outlets//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/disconnect/', views.PowerOutletBulkDisconnectView.as_view(), name='poweroutlet_bulk_disconnect'), + path(r'power-outlets/import/', views.PowerOutletBulkImportView.as_view(), name='poweroutlet_import'), # Interfaces path(r'devices/interfaces/add/', views.DeviceBulkAddInterfaceView.as_view(), name='device_bulk_add_interface'), path(r'devices//interfaces/add/', views.InterfaceCreateView.as_view(), name='interface_add'), path(r'devices//interfaces/edit/', views.InterfaceBulkEditView.as_view(), name='interface_bulk_edit'), path(r'devices//interfaces/delete/', views.InterfaceBulkDeleteView.as_view(), name='interface_bulk_delete'), + path(r'interfaces/', views.InterfaceListView.as_view(), name='interface_list'), path(r'interfaces//connect//', views.CableCreateView.as_view(), name='interface_connect', kwargs={'termination_a_type': Interface}), path(r'interfaces//', views.InterfaceView.as_view(), name='interface'), path(r'interfaces//edit/', views.InterfaceEditView.as_view(), name='interface_edit'), @@ -222,40 +231,47 @@ urlpatterns = [ path(r'interfaces//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/disconnect/', views.InterfaceBulkDisconnectView.as_view(), name='interface_bulk_disconnect'), + path(r'interfaces/import/', views.InterfaceBulkImportView.as_view(), name='interface_import'), # Front ports # path(r'devices/front-ports/add/', views.DeviceBulkAddFrontPortView.as_view(), name='device_bulk_add_frontport'), path(r'devices//front-ports/add/', views.FrontPortCreateView.as_view(), name='frontport_add'), path(r'devices//front-ports/edit/', views.FrontPortBulkEditView.as_view(), name='frontport_bulk_edit'), path(r'devices//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//connect//', views.CableCreateView.as_view(), name='frontport_connect', kwargs={'termination_a_type': FrontPort}), path(r'front-ports//edit/', views.FrontPortEditView.as_view(), name='frontport_edit'), path(r'front-ports//delete/', views.FrontPortDeleteView.as_view(), name='frontport_delete'), path(r'front-ports//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/disconnect/', views.FrontPortBulkDisconnectView.as_view(), name='frontport_bulk_disconnect'), + path(r'front-ports/import/', views.FrontPortBulkImportView.as_view(), name='frontport_import'), # Rear ports # path(r'devices/rear-ports/add/', views.DeviceBulkAddRearPortView.as_view(), name='device_bulk_add_rearport'), path(r'devices//rear-ports/add/', views.RearPortCreateView.as_view(), name='rearport_add'), path(r'devices//rear-ports/edit/', views.RearPortBulkEditView.as_view(), name='rearport_bulk_edit'), path(r'devices//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//connect//', views.CableCreateView.as_view(), name='rearport_connect', kwargs={'termination_a_type': RearPort}), path(r'rear-ports//edit/', views.RearPortEditView.as_view(), name='rearport_edit'), path(r'rear-ports//delete/', views.RearPortDeleteView.as_view(), name='rearport_delete'), path(r'rear-ports//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/disconnect/', views.RearPortBulkDisconnectView.as_view(), name='rearport_bulk_disconnect'), + path(r'rear-ports/import/', views.RearPortBulkImportView.as_view(), name='rearport_import'), # Device bays path(r'devices/device-bays/add/', views.DeviceBulkAddDeviceBayView.as_view(), name='device_bulk_add_devicebay'), path(r'devices//bays/add/', views.DeviceBayCreateView.as_view(), name='devicebay_add'), path(r'devices//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//edit/', views.DeviceBayEditView.as_view(), name='devicebay_edit'), path(r'device-bays//delete/', views.DeviceBayDeleteView.as_view(), name='devicebay_delete'), path(r'device-bays//populate/', views.DeviceBayPopulateView.as_view(), name='devicebay_populate'), path(r'device-bays//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/import/', views.DeviceBayBulkImportView.as_view(), name='devicebay_import'), # Inventory items path(r'inventory-items/', views.InventoryItemListView.as_view(), name='inventoryitem_list'), diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 55a08fdb8..e41d44d95 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -1,3 +1,4 @@ +from collections import OrderedDict import re from django.conf import settings @@ -16,8 +17,7 @@ from django.utils.safestring import mark_safe from django.views.generic import View from circuits.models import Circuit -from extras.constants import GRAPH_TYPE_DEVICE, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE -from extras.models import Graph, TopologyMap +from extras.models import Graph from extras.views import ObjectConfigContextView from ipam.models import Prefix, VLAN from ipam.tables import InterfaceIPAddressTable, InterfaceVLANTable @@ -26,7 +26,7 @@ from utilities.paginator import EnhancedPaginator from utilities.utils import csv_format from utilities.views import ( BulkComponentCreateView, BulkDeleteView, BulkEditView, BulkImportView, ComponentCreateView, GetReturnURLMixin, - ObjectDeleteView, ObjectEditView, ObjectListView, + ObjectImportView, ObjectDeleteView, ObjectEditView, ObjectListView, ) from virtualization.models import VirtualMachine from . import filters, forms, tables @@ -148,8 +148,8 @@ class RegionListView(PermissionRequiredMixin, ObjectListView): 'site_count', cumulative=True ) - filter = filters.RegionFilter - filter_form = forms.RegionFilterForm + filterset = filters.RegionFilterSet + filterset_form = forms.RegionFilterForm table = tables.RegionTable template_name = 'dcim/region_list.html' @@ -175,7 +175,7 @@ class RegionBulkImportView(PermissionRequiredMixin, BulkImportView): class RegionBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_region' queryset = Region.objects.all() - filter = filters.RegionFilter + filterset = filters.RegionFilterSet table = tables.RegionTable default_return_url = 'dcim:region_list' @@ -187,8 +187,8 @@ class RegionBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class SiteListView(PermissionRequiredMixin, ObjectListView): permission_required = 'dcim.view_site' queryset = Site.objects.prefetch_related('region', 'tenant') - filter = filters.SiteFilter - filter_form = forms.SiteFilterForm + filterset = filters.SiteFilterSet + filterset_form = forms.SiteFilterForm table = tables.SiteTable template_name = 'dcim/site_list.html' @@ -208,14 +208,12 @@ class SiteView(PermissionRequiredMixin, View): 'vm_count': VirtualMachine.objects.filter(cluster__site=site).count(), } rack_groups = RackGroup.objects.filter(site=site).annotate(rack_count=Count('racks')) - topology_maps = TopologyMap.objects.filter(site=site) - show_graphs = Graph.objects.filter(type=GRAPH_TYPE_SITE).exists() + show_graphs = Graph.objects.filter(type__model='site').exists() return render(request, 'dcim/site.html', { 'site': site, 'stats': stats, 'rack_groups': rack_groups, - 'topology_maps': topology_maps, 'show_graphs': show_graphs, }) @@ -248,7 +246,7 @@ class SiteBulkImportView(PermissionRequiredMixin, BulkImportView): class SiteBulkEditView(PermissionRequiredMixin, BulkEditView): permission_required = 'dcim.change_site' queryset = Site.objects.prefetch_related('region', 'tenant') - filter = filters.SiteFilter + filterset = filters.SiteFilterSet table = tables.SiteTable form = forms.SiteBulkEditForm default_return_url = 'dcim:site_list' @@ -257,7 +255,7 @@ class SiteBulkEditView(PermissionRequiredMixin, BulkEditView): class SiteBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_site' queryset = Site.objects.prefetch_related('region', 'tenant') - filter = filters.SiteFilter + filterset = filters.SiteFilterSet table = tables.SiteTable default_return_url = 'dcim:site_list' @@ -269,8 +267,8 @@ class SiteBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class RackGroupListView(PermissionRequiredMixin, ObjectListView): permission_required = 'dcim.view_rackgroup' queryset = RackGroup.objects.prefetch_related('site').annotate(rack_count=Count('racks')) - filter = filters.RackGroupFilter - filter_form = forms.RackGroupFilterForm + filterset = filters.RackGroupFilterSet + filterset_form = forms.RackGroupFilterForm table = tables.RackGroupTable template_name = 'dcim/rackgroup_list.html' @@ -296,7 +294,7 @@ class RackGroupBulkImportView(PermissionRequiredMixin, BulkImportView): class RackGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_rackgroup' queryset = RackGroup.objects.prefetch_related('site').annotate(rack_count=Count('racks')) - filter = filters.RackGroupFilter + filterset = filters.RackGroupFilterSet table = tables.RackGroupTable default_return_url = 'dcim:rackgroup_list' @@ -348,8 +346,8 @@ class RackListView(PermissionRequiredMixin, ObjectListView): ).annotate( device_count=Count('devices') ) - filter = filters.RackFilter - filter_form = forms.RackFilterForm + filterset = filters.RackFilterSet + filterset_form = forms.RackFilterForm table = tables.RackDetailTable template_name = 'dcim/rack_list.html' @@ -363,7 +361,7 @@ class RackElevationListView(PermissionRequiredMixin, View): def get(self, request): racks = Rack.objects.prefetch_related('site', 'group', 'tenant', 'role', 'devices__device_type') - racks = filters.RackFilter(request.GET, racks).qs + racks = filters.RackFilterSet(request.GET, racks).qs total_count = racks.count() # Pagination @@ -421,8 +419,6 @@ class RackView(PermissionRequiredMixin, View): 'nonracked_devices': nonracked_devices, 'next_rack': next_rack, 'prev_rack': prev_rack, - 'front_elevation': rack.get_front_elevation(), - 'rear_elevation': rack.get_rear_elevation(), }) @@ -454,7 +450,7 @@ class RackBulkImportView(PermissionRequiredMixin, BulkImportView): class RackBulkEditView(PermissionRequiredMixin, BulkEditView): permission_required = 'dcim.change_rack' queryset = Rack.objects.prefetch_related('site', 'group', 'tenant', 'role') - filter = filters.RackFilter + filterset = filters.RackFilterSet table = tables.RackTable form = forms.RackBulkEditForm default_return_url = 'dcim:rack_list' @@ -463,7 +459,7 @@ class RackBulkEditView(PermissionRequiredMixin, BulkEditView): class RackBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_rack' queryset = Rack.objects.prefetch_related('site', 'group', 'tenant', 'role') - filter = filters.RackFilter + filterset = filters.RackFilterSet table = tables.RackTable default_return_url = 'dcim:rack_list' @@ -475,8 +471,8 @@ class RackBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class RackReservationListView(PermissionRequiredMixin, ObjectListView): permission_required = 'dcim.view_rackreservation' queryset = RackReservation.objects.prefetch_related('rack__site') - filter = filters.RackReservationFilter - filter_form = forms.RackReservationFilterForm + filterset = filters.RackReservationFilterSet + filterset_form = forms.RackReservationFilterForm table = tables.RackReservationTable template_name = 'dcim/rackreservation_list.html' @@ -511,7 +507,7 @@ class RackReservationDeleteView(PermissionRequiredMixin, ObjectDeleteView): class RackReservationBulkEditView(PermissionRequiredMixin, BulkEditView): permission_required = 'dcim.change_rackreservation' queryset = RackReservation.objects.prefetch_related('rack', 'user') - filter = filters.RackReservationFilter + filterset = filters.RackReservationFilterSet table = tables.RackReservationTable form = forms.RackReservationBulkEditForm default_return_url = 'dcim:rackreservation_list' @@ -520,7 +516,7 @@ class RackReservationBulkEditView(PermissionRequiredMixin, BulkEditView): class RackReservationBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_rackreservation' queryset = RackReservation.objects.prefetch_related('rack', 'user') - filter = filters.RackReservationFilter + filterset = filters.RackReservationFilterSet table = tables.RackReservationTable default_return_url = 'dcim:rackreservation_list' @@ -572,8 +568,8 @@ class ManufacturerBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class DeviceTypeListView(PermissionRequiredMixin, ObjectListView): permission_required = 'dcim.view_devicetype' queryset = DeviceType.objects.prefetch_related('manufacturer').annotate(instance_count=Count('instances')) - filter = filters.DeviceTypeFilter - filter_form = forms.DeviceTypeFilterForm + filterset = filters.DeviceTypeFilterSet + filterset_form = forms.DeviceTypeFilterForm table = tables.DeviceTypeTable template_name = 'dcim/devicetype_list.html' @@ -659,17 +655,37 @@ class DeviceTypeDeleteView(PermissionRequiredMixin, ObjectDeleteView): default_return_url = 'dcim:devicetype_list' -class DeviceTypeBulkImportView(PermissionRequiredMixin, BulkImportView): - permission_required = 'dcim.add_devicetype' - model_form = forms.DeviceTypeCSVForm - table = tables.DeviceTypeTable - default_return_url = 'dcim:devicetype_list' +class DeviceTypeImportView(PermissionRequiredMixin, ObjectImportView): + permission_required = [ + 'dcim.add_devicetype', + 'dcim.add_consoleporttemplate', + 'dcim.add_consoleserverporttemplate', + 'dcim.add_powerporttemplate', + 'dcim.add_poweroutlettemplate', + 'dcim.add_interfacetemplate', + 'dcim.add_frontporttemplate', + 'dcim.add_rearporttemplate', + 'dcim.add_devicebaytemplate', + ] + model = DeviceType + model_form = forms.DeviceTypeImportForm + related_object_forms = OrderedDict(( + ('console-ports', forms.ConsolePortTemplateImportForm), + ('console-server-ports', forms.ConsoleServerPortTemplateImportForm), + ('power-ports', forms.PowerPortTemplateImportForm), + ('power-outlets', forms.PowerOutletTemplateImportForm), + ('interfaces', forms.InterfaceTemplateImportForm), + ('rear-ports', forms.RearPortTemplateImportForm), + ('front-ports', forms.FrontPortTemplateImportForm), + ('device-bays', forms.DeviceBayTemplateImportForm), + )) + default_return_url = 'dcim:devicetype_import' class DeviceTypeBulkEditView(PermissionRequiredMixin, BulkEditView): permission_required = 'dcim.change_devicetype' queryset = DeviceType.objects.prefetch_related('manufacturer').annotate(instance_count=Count('instances')) - filter = filters.DeviceTypeFilter + filterset = filters.DeviceTypeFilterSet table = tables.DeviceTypeTable form = forms.DeviceTypeBulkEditForm default_return_url = 'dcim:devicetype_list' @@ -678,7 +694,7 @@ class DeviceTypeBulkEditView(PermissionRequiredMixin, BulkEditView): class DeviceTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_devicetype' queryset = DeviceType.objects.prefetch_related('manufacturer').annotate(instance_count=Count('instances')) - filter = filters.DeviceTypeFilter + filterset = filters.DeviceTypeFilterSet table = tables.DeviceTypeTable default_return_url = 'dcim:devicetype_list' @@ -960,8 +976,8 @@ class DeviceListView(PermissionRequiredMixin, ObjectListView): queryset = Device.objects.prefetch_related( 'device_type__manufacturer', 'device_role', 'tenant', 'site', 'rack', 'primary_ip4', 'primary_ip6' ) - filter = filters.DeviceFilter - filter_form = forms.DeviceFilterForm + filterset = filters.DeviceFilterSet + filterset_form = forms.DeviceFilterForm table = tables.DeviceDetailTable template_name = 'dcim/device_list.html' @@ -1039,8 +1055,8 @@ class DeviceView(PermissionRequiredMixin, View): 'secrets': secrets, 'vc_members': vc_members, 'related_devices': related_devices, - 'show_graphs': Graph.objects.filter(type=GRAPH_TYPE_DEVICE).exists(), - 'show_interface_graphs': Graph.objects.filter(type=GRAPH_TYPE_INTERFACE).exists(), + 'show_graphs': Graph.objects.filter(type__model='device').exists(), + 'show_interface_graphs': Graph.objects.filter(type__model='interface').exists(), }) @@ -1160,7 +1176,7 @@ class ChildDeviceBulkImportView(PermissionRequiredMixin, BulkImportView): class DeviceBulkEditView(PermissionRequiredMixin, BulkEditView): permission_required = 'dcim.change_device' queryset = Device.objects.prefetch_related('tenant', 'site', 'rack', 'device_role', 'device_type__manufacturer') - filter = filters.DeviceFilter + filterset = filters.DeviceFilterSet table = tables.DeviceTable form = forms.DeviceBulkEditForm default_return_url = 'dcim:device_list' @@ -1169,7 +1185,7 @@ class DeviceBulkEditView(PermissionRequiredMixin, BulkEditView): class DeviceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_device' queryset = Device.objects.prefetch_related('tenant', 'site', 'rack', 'device_role', 'device_type__manufacturer') - filter = filters.DeviceFilter + filterset = filters.DeviceFilterSet table = tables.DeviceTable default_return_url = 'dcim:device_list' @@ -1178,6 +1194,15 @@ class DeviceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): # Console ports # +class ConsolePortListView(PermissionRequiredMixin, ObjectListView): + permission_required = 'dcim.view_consoleport' + queryset = ConsolePort.objects.prefetch_related('device', 'device__tenant', 'device__site', 'cable') + filterset = filters.ConsolePortFilterSet + filterset_form = forms.ConsolePortFilterForm + table = tables.ConsolePortDetailTable + template_name = 'dcim/device_component_list.html' + + class ConsolePortCreateView(PermissionRequiredMixin, ComponentCreateView): permission_required = 'dcim.add_consoleport' parent_model = Device @@ -1199,6 +1224,13 @@ class ConsolePortDeleteView(PermissionRequiredMixin, ObjectDeleteView): 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): permission_required = 'dcim.delete_consoleport' queryset = ConsolePort.objects.all() @@ -1210,6 +1242,15 @@ class ConsolePortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): # Console server ports # +class ConsoleServerPortListView(PermissionRequiredMixin, ObjectListView): + permission_required = 'dcim.view_consoleserverport' + queryset = ConsoleServerPort.objects.prefetch_related('device', 'device__tenant', 'device__site', 'cable') + filterset = filters.ConsoleServerPortFilterSet + filterset_form = forms.ConsoleServerPortFilterForm + table = tables.ConsoleServerPortDetailTable + template_name = 'dcim/device_component_list.html' + + class ConsoleServerPortCreateView(PermissionRequiredMixin, ComponentCreateView): permission_required = 'dcim.add_consoleserverport' parent_model = Device @@ -1231,6 +1272,13 @@ class ConsoleServerPortDeleteView(PermissionRequiredMixin, ObjectDeleteView): 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): permission_required = 'dcim.change_consoleserverport' queryset = ConsoleServerPort.objects.all() @@ -1262,6 +1310,15 @@ class ConsoleServerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): # Power ports # +class PowerPortListView(PermissionRequiredMixin, ObjectListView): + permission_required = 'dcim.view_powerport' + queryset = PowerPort.objects.prefetch_related('device', 'device__tenant', 'device__site', 'cable') + filterset = filters.PowerPortFilterSet + filterset_form = forms.PowerPortFilterForm + table = tables.PowerPortDetailTable + template_name = 'dcim/device_component_list.html' + + class PowerPortCreateView(PermissionRequiredMixin, ComponentCreateView): permission_required = 'dcim.add_powerport' parent_model = Device @@ -1283,6 +1340,13 @@ class PowerPortDeleteView(PermissionRequiredMixin, ObjectDeleteView): 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): permission_required = 'dcim.delete_powerport' queryset = PowerPort.objects.all() @@ -1294,6 +1358,15 @@ class PowerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): # Power outlets # +class PowerOutletListView(PermissionRequiredMixin, ObjectListView): + permission_required = 'dcim.view_poweroutlet' + queryset = PowerOutlet.objects.prefetch_related('device', 'device__tenant', 'device__site', 'cable') + filterset = filters.PowerOutletFilterSet + filterset_form = forms.PowerOutletFilterForm + table = tables.PowerOutletDetailTable + template_name = 'dcim/device_component_list.html' + + class PowerOutletCreateView(PermissionRequiredMixin, ComponentCreateView): permission_required = 'dcim.add_poweroutlet' parent_model = Device @@ -1315,6 +1388,13 @@ class PowerOutletDeleteView(PermissionRequiredMixin, ObjectDeleteView): 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): permission_required = 'dcim.change_poweroutlet' queryset = PowerOutlet.objects.all() @@ -1346,6 +1426,15 @@ class PowerOutletBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): # Interfaces # +class InterfaceListView(PermissionRequiredMixin, ObjectListView): + permission_required = 'dcim.view_interface' + queryset = Interface.objects.prefetch_related('device', 'device__tenant', 'device__site', 'cable') + filterset = filters.InterfaceFilterSet + filterset_form = forms.InterfaceFilterForm + table = tables.InterfaceDetailTable + template_name = 'dcim/device_component_list.html' + + class InterfaceView(PermissionRequiredMixin, View): permission_required = 'dcim.view_interface' @@ -1404,6 +1493,13 @@ class InterfaceDeleteView(PermissionRequiredMixin, ObjectDeleteView): 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): permission_required = 'dcim.change_interface' queryset = Interface.objects.all() @@ -1435,6 +1531,15 @@ class InterfaceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): # Front ports # +class FrontPortListView(PermissionRequiredMixin, ObjectListView): + permission_required = 'dcim.view_frontport' + queryset = FrontPort.objects.prefetch_related('device', 'device__tenant', 'device__site', 'cable') + filterset = filters.FrontPortFilterSet + filterset_form = forms.FrontPortFilterForm + table = tables.FrontPortDetailTable + template_name = 'dcim/device_component_list.html' + + class FrontPortCreateView(PermissionRequiredMixin, ComponentCreateView): permission_required = 'dcim.add_frontport' parent_model = Device @@ -1456,6 +1561,13 @@ class FrontPortDeleteView(PermissionRequiredMixin, ObjectDeleteView): 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): permission_required = 'dcim.change_frontport' queryset = FrontPort.objects.all() @@ -1487,6 +1599,15 @@ class FrontPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): # Rear ports # +class RearPortListView(PermissionRequiredMixin, ObjectListView): + permission_required = 'dcim.view_rearport' + queryset = RearPort.objects.prefetch_related('device', 'device__tenant', 'device__site', 'cable') + filterset = filters.RearPortFilterSet + filterset_form = forms.RearPortFilterForm + table = tables.RearPortDetailTable + template_name = 'dcim/device_component_list.html' + + class RearPortCreateView(PermissionRequiredMixin, ComponentCreateView): permission_required = 'dcim.add_rearport' parent_model = Device @@ -1508,6 +1629,13 @@ class RearPortDeleteView(PermissionRequiredMixin, ObjectDeleteView): 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): permission_required = 'dcim.change_rearport' queryset = RearPort.objects.all() @@ -1539,6 +1667,17 @@ class RearPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): # Device bays # +class DeviceBayListView(PermissionRequiredMixin, ObjectListView): + permission_required = 'dcim.view_devicebay' + queryset = DeviceBay.objects.prefetch_related( + 'device', 'device__site', 'installed_device', 'installed_device__site' + ) + filterset = filters.DeviceBayFilterSet + filterset_form = forms.DeviceBayFilterForm + table = tables.DeviceBayDetailTable + template_name = 'dcim/device_component_list.html' + + class DeviceBayCreateView(PermissionRequiredMixin, ComponentCreateView): permission_required = 'dcim.add_devicebay' parent_model = Device @@ -1629,6 +1768,13 @@ 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): permission_required = 'dcim.change_devicebay' queryset = DeviceBay.objects.all() @@ -1653,7 +1799,7 @@ class DeviceBulkAddConsolePortView(PermissionRequiredMixin, BulkComponentCreateV form = forms.DeviceBulkAddComponentForm model = ConsolePort model_form = forms.ConsolePortForm - filter = filters.DeviceFilter + filterset = filters.DeviceFilterSet table = tables.DeviceTable default_return_url = 'dcim:device_list' @@ -1665,7 +1811,7 @@ class DeviceBulkAddConsoleServerPortView(PermissionRequiredMixin, BulkComponentC form = forms.DeviceBulkAddComponentForm model = ConsoleServerPort model_form = forms.ConsoleServerPortForm - filter = filters.DeviceFilter + filterset = filters.DeviceFilterSet table = tables.DeviceTable default_return_url = 'dcim:device_list' @@ -1677,7 +1823,7 @@ class DeviceBulkAddPowerPortView(PermissionRequiredMixin, BulkComponentCreateVie form = forms.DeviceBulkAddComponentForm model = PowerPort model_form = forms.PowerPortForm - filter = filters.DeviceFilter + filterset = filters.DeviceFilterSet table = tables.DeviceTable default_return_url = 'dcim:device_list' @@ -1689,7 +1835,7 @@ class DeviceBulkAddPowerOutletView(PermissionRequiredMixin, BulkComponentCreateV form = forms.DeviceBulkAddComponentForm model = PowerOutlet model_form = forms.PowerOutletForm - filter = filters.DeviceFilter + filterset = filters.DeviceFilterSet table = tables.DeviceTable default_return_url = 'dcim:device_list' @@ -1701,7 +1847,7 @@ class DeviceBulkAddInterfaceView(PermissionRequiredMixin, BulkComponentCreateVie form = forms.DeviceBulkAddInterfaceForm model = Interface model_form = forms.InterfaceForm - filter = filters.DeviceFilter + filterset = filters.DeviceFilterSet table = tables.DeviceTable default_return_url = 'dcim:device_list' @@ -1713,7 +1859,7 @@ class DeviceBulkAddDeviceBayView(PermissionRequiredMixin, BulkComponentCreateVie form = forms.DeviceBulkAddComponentForm model = DeviceBay model_form = forms.DeviceBayForm - filter = filters.DeviceFilter + filterset = filters.DeviceFilterSet table = tables.DeviceTable default_return_url = 'dcim:device_list' @@ -1727,8 +1873,8 @@ class CableListView(PermissionRequiredMixin, ObjectListView): queryset = Cable.objects.prefetch_related( 'termination_a', 'termination_b' ) - filter = filters.CableFilter - filter_form = forms.CableFilterForm + filterset = filters.CableFilterSet + filterset_form = forms.CableFilterForm table = tables.CableTable template_name = 'dcim/cable_list.html' @@ -1864,7 +2010,7 @@ class CableBulkImportView(PermissionRequiredMixin, BulkImportView): class CableBulkEditView(PermissionRequiredMixin, BulkEditView): permission_required = 'dcim.change_cable' queryset = Cable.objects.prefetch_related('termination_a', 'termination_b') - filter = filters.CableFilter + filterset = filters.CableFilterSet table = tables.CableTable form = forms.CableBulkEditForm default_return_url = 'dcim:cable_list' @@ -1873,7 +2019,7 @@ class CableBulkEditView(PermissionRequiredMixin, BulkEditView): class CableBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_cable' queryset = Cable.objects.prefetch_related('termination_a', 'termination_b') - filter = filters.CableFilter + filterset = filters.CableFilterSet table = tables.CableTable default_return_url = 'dcim:cable_list' @@ -1891,8 +2037,8 @@ class ConsoleConnectionsListView(PermissionRequiredMixin, ObjectListView): ).order_by( 'cable', 'connected_endpoint__device__name', 'connected_endpoint__name' ) - filter = filters.ConsoleConnectionFilter - filter_form = forms.ConsoleConnectionFilterForm + filterset = filters.ConsoleConnectionFilterSet + filterset_form = forms.ConsoleConnectionFilterForm table = tables.ConsoleConnectionTable template_name = 'dcim/console_connections_list.html' @@ -1910,7 +2056,8 @@ class ConsoleConnectionsListView(PermissionRequiredMixin, ObjectListView): obj.get_connection_status_display(), ]) csv_data.append(csv) - return csv_data + + return '\n'.join(csv_data) class PowerConnectionsListView(PermissionRequiredMixin, ObjectListView): @@ -1922,8 +2069,8 @@ class PowerConnectionsListView(PermissionRequiredMixin, ObjectListView): ).order_by( 'cable', '_connected_poweroutlet__device__name', '_connected_poweroutlet__name' ) - filter = filters.PowerConnectionFilter - filter_form = forms.PowerConnectionFilterForm + filterset = filters.PowerConnectionFilterSet + filterset_form = forms.PowerConnectionFilterForm table = tables.PowerConnectionTable template_name = 'dcim/power_connections_list.html' @@ -1941,7 +2088,8 @@ class PowerConnectionsListView(PermissionRequiredMixin, ObjectListView): obj.get_connection_status_display(), ]) csv_data.append(csv) - return csv_data + + return '\n'.join(csv_data) class InterfaceConnectionsListView(PermissionRequiredMixin, ObjectListView): @@ -1955,8 +2103,8 @@ class InterfaceConnectionsListView(PermissionRequiredMixin, ObjectListView): ).order_by( 'device' ) - filter = filters.InterfaceConnectionFilter - filter_form = forms.InterfaceConnectionFilterForm + filterset = filters.InterfaceConnectionFilterSet + filterset_form = forms.InterfaceConnectionFilterForm table = tables.InterfaceConnectionTable template_name = 'dcim/interface_connections_list.html' @@ -1980,7 +2128,8 @@ class InterfaceConnectionsListView(PermissionRequiredMixin, ObjectListView): obj.get_connection_status_display(), ]) csv_data.append(csv) - return csv_data + + return '\n'.join(csv_data) # @@ -1990,8 +2139,8 @@ class InterfaceConnectionsListView(PermissionRequiredMixin, ObjectListView): class InventoryItemListView(PermissionRequiredMixin, ObjectListView): permission_required = 'dcim.view_inventoryitem' queryset = InventoryItem.objects.prefetch_related('device', 'manufacturer') - filter = filters.InventoryItemFilter - filter_form = forms.InventoryItemFilterForm + filterset = filters.InventoryItemFilterSet + filterset_form = forms.InventoryItemFilterForm table = tables.InventoryItemTable template_name = 'dcim/inventoryitem_list.html' @@ -2025,7 +2174,7 @@ class InventoryItemBulkImportView(PermissionRequiredMixin, BulkImportView): class InventoryItemBulkEditView(PermissionRequiredMixin, BulkEditView): permission_required = 'dcim.change_inventoryitem' queryset = InventoryItem.objects.prefetch_related('device', 'manufacturer') - filter = filters.InventoryItemFilter + filterset = filters.InventoryItemFilterSet table = tables.InventoryItemTable form = forms.InventoryItemBulkEditForm default_return_url = 'dcim:inventoryitem_list' @@ -2047,8 +2196,8 @@ class VirtualChassisListView(PermissionRequiredMixin, ObjectListView): permission_required = 'dcim.view_virtualchassis' queryset = VirtualChassis.objects.prefetch_related('master').annotate(member_count=Count('members')) table = tables.VirtualChassisTable - filter = filters.VirtualChassisFilter - filter_form = forms.VirtualChassisFilterForm + filterset = filters.VirtualChassisFilterSet + filterset_form = forms.VirtualChassisFilterForm template_name = 'dcim/virtualchassis_list.html' @@ -2290,8 +2439,8 @@ class PowerPanelListView(PermissionRequiredMixin, ObjectListView): ).annotate( powerfeed_count=Count('powerfeeds') ) - filter = filters.PowerPanelFilter - filter_form = forms.PowerPanelFilterForm + filterset = filters.PowerPanelFilterSet + filterset_form = forms.PowerPanelFilterForm table = tables.PowerPanelTable template_name = 'dcim/powerpanel_list.html' @@ -2345,7 +2494,7 @@ class PowerPanelBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): ).annotate( rack_count=Count('powerfeeds') ) - filter = filters.PowerPanelFilter + filterset = filters.PowerPanelFilterSet table = tables.PowerPanelTable default_return_url = 'dcim:powerpanel_list' @@ -2359,8 +2508,8 @@ class PowerFeedListView(PermissionRequiredMixin, ObjectListView): queryset = PowerFeed.objects.prefetch_related( 'power_panel', 'rack' ) - filter = filters.PowerFeedFilter - filter_form = forms.PowerFeedFilterForm + filterset = filters.PowerFeedFilterSet + filterset_form = forms.PowerFeedFilterForm table = tables.PowerFeedTable template_name = 'dcim/powerfeed_list.html' @@ -2405,7 +2554,7 @@ class PowerFeedBulkImportView(PermissionRequiredMixin, BulkImportView): class PowerFeedBulkEditView(PermissionRequiredMixin, BulkEditView): permission_required = 'dcim.change_powerfeed' queryset = PowerFeed.objects.prefetch_related('power_panel', 'rack') - filter = filters.PowerFeedFilter + filterset = filters.PowerFeedFilterSet table = tables.PowerFeedTable form = forms.PowerFeedBulkEditForm default_return_url = 'dcim:powerfeed_list' @@ -2414,6 +2563,6 @@ class PowerFeedBulkEditView(PermissionRequiredMixin, BulkEditView): class PowerFeedBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_powerfeed' queryset = PowerFeed.objects.prefetch_related('power_panel', 'rack') - filter = filters.PowerFeedFilter + filterset = filters.PowerFeedFilterSet table = tables.PowerFeedTable default_return_url = 'dcim:powerfeed_list' diff --git a/netbox/extras/__init__.py b/netbox/extras/__init__.py index c7e9c66ad..3db5f9c25 100644 --- a/netbox/extras/__init__.py +++ b/netbox/extras/__init__.py @@ -1,15 +1 @@ -from django.conf import settings -from django.core.exceptions import ImproperlyConfigured - - 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." - ) diff --git a/netbox/extras/admin.py b/netbox/extras/admin.py index ee21b4f5d..2a39c207e 100644 --- a/netbox/extras/admin.py +++ b/netbox/extras/admin.py @@ -3,9 +3,7 @@ from django.contrib import admin from netbox.admin import admin_site from utilities.forms import LaxURLField -from .models import ( - CustomField, CustomFieldChoice, CustomLink, Graph, ExportTemplate, ReportResult, TopologyMap, Webhook, -) +from .models import CustomField, CustomFieldChoice, CustomLink, Graph, ExportTemplate, ReportResult, Webhook from .reports import get_report @@ -133,10 +131,10 @@ class CustomLinkAdmin(admin.ModelAdmin): @admin.register(Graph, site=admin_site) class GraphAdmin(admin.ModelAdmin): list_display = [ - 'name', 'type', 'weight', 'source', + 'name', 'type', 'weight', 'template_language', 'source', ] list_filter = [ - 'type', + 'type', 'template_language', ] @@ -197,15 +195,3 @@ class ReportResultAdmin(admin.ModelAdmin): def passing(self, obj): return not obj.failed passing.boolean = True - - -# -# Topology maps -# - -@admin.register(TopologyMap, site=admin_site) -class TopologyMapAdmin(admin.ModelAdmin): - list_display = ['name', 'slug', 'site'] - prepopulated_fields = { - 'slug': ['name'], - } diff --git a/netbox/extras/api/customfields.py b/netbox/extras/api/customfields.py index e0c70efa3..9a3041238 100644 --- a/netbox/extras/api/customfields.py +++ b/netbox/extras/api/customfields.py @@ -5,7 +5,7 @@ from django.db import transaction from rest_framework import serializers from rest_framework.exceptions import ValidationError -from extras.constants import * +from extras.choices import * from extras.models import CustomField, CustomFieldChoice, CustomFieldValue from utilities.api import ValidatedModelSerializer @@ -39,7 +39,7 @@ class CustomFieldsSerializer(serializers.BaseSerializer): if value not in [None, '']: # Validate integer - if cf.type == CF_TYPE_INTEGER: + if cf.type == CustomFieldTypeChoices.TYPE_INTEGER: try: int(value) except ValueError: @@ -48,13 +48,13 @@ class CustomFieldsSerializer(serializers.BaseSerializer): ) # Validate boolean - if cf.type == CF_TYPE_BOOLEAN and value not in [True, False, 1, 0]: + if cf.type == CustomFieldTypeChoices.TYPE_BOOLEAN and value not in [True, False, 1, 0]: raise ValidationError( "Invalid value for boolean field {}: {}".format(field_name, value) ) # Validate date - if cf.type == CF_TYPE_DATE: + if cf.type == CustomFieldTypeChoices.TYPE_DATE: try: datetime.strptime(value, '%Y-%m-%d') except ValueError: @@ -63,7 +63,7 @@ class CustomFieldsSerializer(serializers.BaseSerializer): ) # Validate selected choice - if cf.type == CF_TYPE_SELECT: + if cf.type == CustomFieldTypeChoices.TYPE_SELECT: try: value = int(value) except ValueError: @@ -102,7 +102,7 @@ class CustomFieldModelSerializer(ValidatedModelSerializer): instance.custom_fields = {} for field in fields: value = instance.cf.get(field.name) - if field.type == CF_TYPE_SELECT and value is not None: + if field.type == CustomFieldTypeChoices.TYPE_SELECT and value is not None: instance.custom_fields[field.name] = CustomFieldChoiceSerializer(value).data else: instance.custom_fields[field.name] = value @@ -134,9 +134,9 @@ class CustomFieldModelSerializer(ValidatedModelSerializer): # Populate initial data using custom field default values for field in fields: if field.name not in self.initial_data['custom_fields'] and field.default: - if field.type == CF_TYPE_SELECT: + if field.type == CustomFieldTypeChoices.TYPE_SELECT: field_value = field.choices.get(value=field.default).pk - elif field.type == CF_TYPE_BOOLEAN: + elif field.type == CustomFieldTypeChoices.TYPE_BOOLEAN: field_value = bool(field.default) else: field_value = field.default diff --git a/netbox/extras/api/serializers.py b/netbox/extras/api/serializers.py index 8cbddc860..0e27a8ee5 100644 --- a/netbox/extras/api/serializers.py +++ b/netbox/extras/api/serializers.py @@ -8,10 +8,10 @@ from dcim.api.nested_serializers import ( NestedRegionSerializer, NestedSiteSerializer, ) from dcim.models import Device, DeviceRole, Platform, Rack, Region, Site +from extras.choices import * from extras.constants import * from extras.models import ( - ConfigContext, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, TopologyMap, - Tag + ConfigContext, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, Tag, ) from tenancy.api.nested_serializers import NestedTenantSerializer, NestedTenantGroupSerializer from tenancy.models import Tenant, TenantGroup @@ -28,17 +28,21 @@ from .nested_serializers import * # class GraphSerializer(ValidatedModelSerializer): - type = ChoiceField(choices=GRAPH_TYPE_CHOICES) + type = ContentTypeField( + queryset=ContentType.objects.filter(GRAPH_MODELS), + ) class Meta: model = Graph - fields = ['id', 'type', 'weight', 'name', 'source', 'link'] + fields = ['id', 'type', 'weight', 'name', 'template_language', 'source', 'link'] class RenderedGraphSerializer(serializers.ModelSerializer): embed_url = serializers.SerializerMethodField() embed_link = serializers.SerializerMethodField() - type = ChoiceField(choices=GRAPH_TYPE_CHOICES) + type = ContentTypeField( + queryset=ContentType.objects.all() + ) class Meta: model = Graph @@ -57,8 +61,8 @@ class RenderedGraphSerializer(serializers.ModelSerializer): class ExportTemplateSerializer(ValidatedModelSerializer): template_language = ChoiceField( - choices=TEMPLATE_LANGUAGE_CHOICES, - default=TEMPLATE_LANGUAGE_JINJA2 + choices=TemplateLanguageChoices, + default=TemplateLanguageChoices.LANGUAGE_JINJA2 ) class Meta: @@ -69,18 +73,6 @@ class ExportTemplateSerializer(ValidatedModelSerializer): ] -# -# Topology maps -# - -class TopologyMapSerializer(ValidatedModelSerializer): - site = NestedSiteSerializer() - - class Meta: - model = TopologyMap - fields = ['id', 'name', 'slug', 'site', 'device_patterns', 'description'] - - # # Tags # @@ -181,12 +173,18 @@ class ConfigContextSerializer(ValidatedModelSerializer): required=False, many=True ) + tags = serializers.SlugRelatedField( + queryset=Tag.objects.all(), + slug_field='slug', + required=False, + many=True + ) class Meta: model = ConfigContext fields = [ 'id', 'name', 'weight', 'description', 'is_active', 'regions', 'sites', 'roles', 'platforms', - 'tenant_groups', 'tenants', 'data', + 'tenant_groups', 'tenants', 'tags', 'data', ] @@ -213,6 +211,52 @@ class ReportDetailSerializer(ReportSerializer): result = ReportResultSerializer() +# +# Scripts +# + +class ScriptSerializer(serializers.Serializer): + id = serializers.SerializerMethodField(read_only=True) + name = serializers.SerializerMethodField(read_only=True) + description = serializers.SerializerMethodField(read_only=True) + vars = serializers.SerializerMethodField(read_only=True) + + def get_id(self, instance): + return '{}.{}'.format(instance.__module__, instance.__name__) + + def get_name(self, instance): + return getattr(instance.Meta, 'name', instance.__name__) + + def get_description(self, instance): + return getattr(instance.Meta, 'description', '') + + def get_vars(self, instance): + return { + k: v.__class__.__name__ for k, v in instance._get_vars().items() + } + + +class ScriptInputSerializer(serializers.Serializer): + data = serializers.JSONField() + commit = serializers.BooleanField() + + +class ScriptLogMessageSerializer(serializers.Serializer): + status = serializers.SerializerMethodField(read_only=True) + message = serializers.SerializerMethodField(read_only=True) + + def get_status(self, instance): + return LOG_LEVEL_CODES.get(instance[0]) + + def get_message(self, instance): + return instance[1] + + +class ScriptOutputSerializer(serializers.Serializer): + log = ScriptLogMessageSerializer(many=True, read_only=True) + output = serializers.CharField(read_only=True) + + # # Change logging # @@ -222,7 +266,7 @@ class ObjectChangeSerializer(serializers.ModelSerializer): read_only=True ) action = ChoiceField( - choices=OBJECTCHANGE_ACTION_CHOICES, + choices=ObjectChangeActionChoices, read_only=True ) changed_object_type = ContentTypeField( diff --git a/netbox/extras/api/urls.py b/netbox/extras/api/urls.py index f4968d004..50a54d3fe 100644 --- a/netbox/extras/api/urls.py +++ b/netbox/extras/api/urls.py @@ -26,9 +26,6 @@ router.register(r'graphs', views.GraphViewSet) # Export templates router.register(r'export-templates', views.ExportTemplateViewSet) -# Topology maps -router.register(r'topology-maps', views.TopologyMapViewSet) - # Tags router.register(r'tags', views.TagViewSet) @@ -41,6 +38,9 @@ router.register(r'config-contexts', views.ConfigContextViewSet) # Reports router.register(r'reports', views.ReportViewSet, basename='report') +# Scripts +router.register(r'scripts', views.ScriptViewSet, basename='script') + # Change logging router.register(r'object-changes', views.ObjectChangeViewSet) diff --git a/netbox/extras/api/views.py b/netbox/extras/api/views.py index 526db20a2..167768861 100644 --- a/netbox/extras/api/views.py +++ b/netbox/extras/api/views.py @@ -2,8 +2,8 @@ from collections import OrderedDict from django.contrib.contenttypes.models import ContentType from django.db.models import Count -from django.http import Http404, HttpResponse -from django.shortcuts import get_object_or_404 +from django.http import Http404 +from rest_framework import status from rest_framework.decorators import action from rest_framework.exceptions import PermissionDenied from rest_framework.response import Response @@ -11,10 +11,10 @@ from rest_framework.viewsets import ReadOnlyModelViewSet, ViewSet from extras import filters from extras.models import ( - ConfigContext, CustomFieldChoice, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, TopologyMap, - Tag, + ConfigContext, CustomFieldChoice, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, Tag, ) from extras.reports import get_report, get_reports +from extras.scripts import get_script, get_scripts from utilities.api import FieldChoicesViewSet, IsAuthenticatedOrLoginNotRequired, ModelViewSet from . import serializers @@ -25,9 +25,9 @@ from . import serializers class ExtrasFieldChoicesViewSet(FieldChoicesViewSet): fields = ( - (ExportTemplate, ['template_language']), - (Graph, ['type']), - (ObjectChange, ['action']), + (serializers.ExportTemplateSerializer, ['template_language']), + (serializers.GraphSerializer, ['type', 'template_language']), + (serializers.ObjectChangeSerializer, ['action']), ) @@ -102,7 +102,7 @@ class CustomFieldModelViewSet(ModelViewSet): class GraphViewSet(ModelViewSet): queryset = Graph.objects.all() serializer_class = serializers.GraphSerializer - filterset_class = filters.GraphFilter + filterset_class = filters.GraphFilterSet # @@ -112,35 +112,7 @@ class GraphViewSet(ModelViewSet): class ExportTemplateViewSet(ModelViewSet): queryset = ExportTemplate.objects.all() serializer_class = serializers.ExportTemplateSerializer - 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 + filterset_class = filters.ExportTemplateFilterSet # @@ -152,7 +124,7 @@ class TagViewSet(ModelViewSet): tagged_items=Count('extras_taggeditem_items', distinct=True) ) serializer_class = serializers.TagSerializer - filterset_class = filters.TagFilter + filterset_class = filters.TagFilterSet # @@ -173,7 +145,7 @@ class ConfigContextViewSet(ModelViewSet): 'regions', 'sites', 'roles', 'platforms', 'tenant_groups', 'tenants', ) serializer_class = serializers.ConfigContextSerializer - filterset_class = filters.ConfigContextFilter + filterset_class = filters.ConfigContextFilterSet # @@ -252,6 +224,56 @@ class ReportViewSet(ViewSet): return Response(serializer.data) +# +# Scripts +# + +class ScriptViewSet(ViewSet): + permission_classes = [IsAuthenticatedOrLoginNotRequired] + _ignore_model_permissions = True + exclude_from_schema = True + lookup_value_regex = '[^/]+' # Allow dots + + def _get_script(self, pk): + module_name, script_name = pk.split('.') + script = get_script(module_name, script_name) + if script is None: + raise Http404 + return script + + def list(self, request): + + flat_list = [] + for script_list in get_scripts().values(): + flat_list.extend(script_list.values()) + + serializer = serializers.ScriptSerializer(flat_list, many=True, context={'request': request}) + + return Response(serializer.data) + + def retrieve(self, request, pk): + script = self._get_script(pk) + serializer = serializers.ScriptSerializer(script, context={'request': request}) + + return Response(serializer.data) + + def post(self, request, pk): + """ + Run a Script identified as ". +``` + +>>> Immediately following a new release, it takes some time for CDNs to catch up and get the new versions live on the CDN. + +## Installing with Bower + +Select2 is available on Bower. Add the following to your `bower.json` file and then run `bower install`: + +``` +"dependencies": { + "select2": "~4.0" +} +``` + +Or, run `bower install select2` from your project directory. + +The precompiled distribution files will be available in `vendor/select2/dist/css/` and `vendor/select2/dist/js/`, relative to your project directory. Include them in your page: + +``` + + +``` + +## Manual installation + +We strongly recommend that you use either a CDN or a package manager like Bower or npm. This will make it easier for you to deploy your project in different environments, and easily update Select2 when new versions are released. Nonetheless if you prefer to integrate Select2 into your project manually, you can [download the release of your choice](https://github.com/select2/select2/tags) from GitHub and copy the files from the `dist` directory into your project. + +Include the compiled files in your page: + +``` + + +``` diff --git a/netbox/project-static/select2-4.0.12/docs/pages/01.getting-started/02.basic-usage/docs.md b/netbox/project-static/select2-4.0.12/docs/pages/01.getting-started/02.basic-usage/docs.md new file mode 100644 index 000000000..e19c98f5e --- /dev/null +++ b/netbox/project-static/select2-4.0.12/docs/pages/01.getting-started/02.basic-usage/docs.md @@ -0,0 +1,106 @@ +--- +title: Basic usage +taxonomy: + category: docs +process: + twig: true +never_cache_twig: true +--- + +## Single select boxes + +Select2 was designed to be a replacement for the standard ` + +and turn it into this... + +
+ +
+ +``` + +``` + + + +Select2 will register itself as a jQuery function if you use any of the distribution builds, so you can call `.select2()` on any jQuery selector where you would like to initialize Select2. + +``` +// In your Javascript (external .js resource or diff --git a/netbox/project-static/select2-4.0.12/docs/pages/01.getting-started/03.builds-and-modules/docs.md b/netbox/project-static/select2-4.0.12/docs/pages/01.getting-started/03.builds-and-modules/docs.md new file mode 100644 index 000000000..4bcf65290 --- /dev/null +++ b/netbox/project-static/select2-4.0.12/docs/pages/01.getting-started/03.builds-and-modules/docs.md @@ -0,0 +1,69 @@ +--- +title: Builds and modules +taxonomy: + category: docs +process: + twig: true +--- + +## The different Select2 builds + +Select2 provides multiple builds that are tailored to different +environments where it is going to be used. If you think you need to use +Select2 in a nonstandard environment, like when you are using AMD, you +should read over the list below. + + + + + + + + + + + + + + + + + + +
Build nameWhen you should use it
+ Standard (select2.js / select2.min.js) + + This is the build that most people should be using for Select2. It + includes the most commonly used features. +
+ Full (select2.full.js / select2.full.min.js) + + You should only use this build if you need the additional features from Select2, like the compatibility modules or recommended includes like jquery.mousewheel +
+ +## Using Select2 with AMD or CommonJS loaders + +Select2 should work with most AMD- or CommonJS-compliant module loaders, including [RequireJS](http://requirejs.org/) and [almond](https://github.com/jrburke/almond). Select2 ships with a modified version of the [UMD jQuery template](https://github.com/umdjs/umd/blob/f208d385768ed30cd0025d5415997075345cd1c0/templates/jqueryPlugin.js) that supports both CommonJS and AMD environments. + +### Configuration + +For most AMD and CommonJS setups, the location of the data files used by Select2 will be automatically determined and handled without you needing to do anything. + +Select2 internally uses AMD and the r.js build tool to build the files located in the `dist` folder. These are built using the files in the `src` folder, so _you can_ just point your modules to the Select2 source and load in `jquery.select2` or `select2/core` when you want to use Select2. The files located in the `dist` folder are also AMD-compatible, so you can point to that file if you want to load in all of the default Select2 modules. + +If you are using Select2 in a build environment where preexisting module names are changed during a build step, Select2 may not be able to find optional dependencies or language files. You can manually set the prefixes to use for these files using the `amdBase` and `amdLanguageBase` options. + +``` +$.fn.select2.defaults.set('amdBase', 'select2/'); +$.fn.select2.defaults.set('amdLanguageBase', 'select2/i18n/'); +``` + +#### `amdBase` + +Specifies the base AMD loader path to be used for select2 dependency resolution. This option typically doesn't need to be changed, but is available for situations where module names may change as a result of certain build environments. + +#### `amdLanguageBase` + +Specifies the base AMD loader language path to be used for select2 language file resolution. This option typically doesn't need to be changed, but is available for situations where module names may change as a result of certain build environments. + +>>> Due to [a bug in older versions](https://github.com/jrburke/requirejs/issues/1342) of the r.js build tool, Select2 was sometimes placed before jQuery in then compiled build file. Because of this, Select2 will trigger an error because it won't be able to find or load jQuery. By upgrading to version 2.1.18 or higher of the r.js build tool, you will be able to fix the issue. diff --git a/netbox/project-static/select2-4.0.12/docs/pages/01.getting-started/chapter.md b/netbox/project-static/select2-4.0.12/docs/pages/01.getting-started/chapter.md new file mode 100644 index 000000000..a09a28e3b --- /dev/null +++ b/netbox/project-static/select2-4.0.12/docs/pages/01.getting-started/chapter.md @@ -0,0 +1,84 @@ +--- +title: Getting Started +taxonomy: + category: docs +process: + twig: true +twig_first: true +--- + +![Select2 logo](/images/logo.png) + +# Select2 + +The jQuery replacement for select boxes + + + +Select2 gives you a customizable select box with support for searching, tagging, remote data sets, infinite scrolling, and many other highly used options. + +
+
+
+ +

In your language

+

+ Select2 comes with support for + RTL environments, + searching with diacritics and + over 40 languages built-in. +

+
+ +
+ +

Remote data support

+

+ Using AJAX you can efficiently + search large lists of items. +

+
+ +
+ +

Theming

+

+ Fully skinnable, CSS built with Sass and an + optional theme for Bootstrap 3. +

+
+
+ +
+
+ +

Fully extensible

+

+ The plugin system + allows you to easily customize Select2 to work exactly how you want it + to. +

+
+ +
+ +

Dynamic item creation

+

+ Allow users to type in a new option and + add it on the fly. +

+
+ +
+ +

Full browser support

+

Support for both modern and legacy browsers is built-in, even including Internet Explorer 8.

+
+
+
+ +>>>>>

Looking for the documentation for Select2 3.5.3? That can still be found here.

diff --git a/netbox/project-static/select2-4.0.12/docs/pages/02.troubleshooting/01.getting-help/docs.md b/netbox/project-static/select2-4.0.12/docs/pages/02.troubleshooting/01.getting-help/docs.md new file mode 100644 index 000000000..32c6b6773 --- /dev/null +++ b/netbox/project-static/select2-4.0.12/docs/pages/02.troubleshooting/01.getting-help/docs.md @@ -0,0 +1,39 @@ +--- +title: Getting Help +metadata: + description: How to get support, report a bug, or suggest a feature for Select2. +taxonomy: + category: docs +--- + +## General support + +Having trouble getting Select2 working on your website? Is it not working together with another plugin, even though you think it should? Select2 has a few communities that you can go to for help getting it all working together. + +1. Join our [forums](https://forums.select2.org), graciously hosted by [NextGI](https://nextgi.com) and start a new topic. +2. Search [Stack Overflow](http://stackoverflow.com/questions/tagged/jquery-select2?sort=votes) **carefully** for existing questions that might address your issue. If you need to open a new question, make sure to include the `jquery-select2` tag. +3. Ask in the `#select2` channel on `chat.freenode.net` or use the [web irc client.](https://webchat.freenode.net/?channels=select2) + +>>>> Do **NOT** use the GitHub issue tracker for general support and troubleshooting questions. The issue tracker is **only** for bug reports with a [minimal, complete, and verifiable example](https://stackoverflow.com/help/mcve) and feature requests. Use the forums instead. + +## Reporting bugs + +Found a problem with Select2? Feel free to open a ticket on the Select2 repository on GitHub, but you should keep a few things in mind: + +1. Use the [GitHub issue search](https://github.com/select2/select2/search?q=&type=Issues) to check if your issue has already been reported. +2. Try to isolate your problem as much as possible. Use [JS Bin](http://jsbin.com/goqagokoye/edit?html,js,output) to create a [minimal, verifiable, and complete](https://stackoverflow.com/help/mcve) example of the problem. +3. Once you are sure the issue is with Select2, and not a third party library, [open an issue](https://github.com/select2/select2/issues/new) with a description of the bug, and link to your jsbin example. + +You can find more information on reporting bugs in the [contributing guide,](https://github.com/select2/select2/blob/master/CONTRIBUTING.md#reporting-bugs-with-select2) including tips on what information to include. + +>>>>> If you are not conversationally proficient in English, do **NOT** post a machine translation (e.g. Google Translate) to GitHub. Get help in crafting your question, either via the [forums](https://forums.select2.org) or in [chat](https://webchat.freenode.net/?channels=select2). If all else fails, you may post your bug report or feature request in your native language and we will tag it with `translation-needed` so that it can be properly translated. + +## Requesting new features + +New feature requests are usually requested by the [Select2 community on GitHub,](https://github.com/select2/select2/issues) and are often fulfilled by [fellow contributors.](https://github.com/select2/select2/blob/master/CONTRIBUTING.md) + +1. Use the [GitHub issue search](https://github.com/select2/select2/search?q=&type=Issues) to check if your feature has already been requested. +2. Check if it hasn't already been implemented as a [third party plugin.](https://github.com/search?q=topic%3Aselect2&type=Repositories) +3. Please make sure you are only requesting a single feature, and not a collection of smaller features. + +You can find more information on requesting new features in the [contributing guide.](https://github.com/select2/select2/blob/master/CONTRIBUTING.md#requesting-features-in-select2) diff --git a/netbox/project-static/select2-4.0.12/docs/pages/02.troubleshooting/02.common-problems/docs.md b/netbox/project-static/select2-4.0.12/docs/pages/02.troubleshooting/02.common-problems/docs.md new file mode 100644 index 000000000..85329454b --- /dev/null +++ b/netbox/project-static/select2-4.0.12/docs/pages/02.troubleshooting/02.common-problems/docs.md @@ -0,0 +1,48 @@ +--- +title: Common problems +metadata: + description: Commonly encountered issues when using Select2. +taxonomy: + category: docs +--- + +### Select2 does not function properly when I use it inside a Bootstrap modal. + +This issue occurs because Bootstrap modals tend to steal focus from other elements outside of the modal. Since by default, Select2 [attaches the dropdown menu to the `` element](/dropdown#dropdown-placement), it is considered "outside of the modal". + +To avoid this problem, you may attach the dropdown to the modal itself with the [dropdownParent](/dropdown#dropdown-placement) setting: + +``` + + +... + + +``` + +This will cause the dropdown to be attached to the modal, rather than the `` element. + +**Alternatively**, you may simply globally override Bootstrap's behavior: + +``` +// Do this before you initialize any of your modals +$.fn.modal.Constructor.prototype.enforceFocus = function() {}; +``` + +See [this answer](https://stackoverflow.com/questions/18487056/select2-doesnt-work-when-embedded-in-a-bootstrap-modal/19574076#19574076) for more information. + +### The dropdown becomes misaligned/displaced when using pinch-zoom. + +See [#5048](https://github.com/select2/select2/issues/5048). The problem is that some browsers' implementations of pinch-zoom affect the `body` element, which [Select2 attaches to by default](https://select2.org/dropdown#dropdown-placement), causing it to render incorrectly. + +The solution is to use `dropdownParent` to attach the dropdown to a more specific element. diff --git a/netbox/project-static/select2-4.0.12/docs/pages/02.troubleshooting/chapter.md b/netbox/project-static/select2-4.0.12/docs/pages/02.troubleshooting/chapter.md new file mode 100644 index 000000000..e4c9f5185 --- /dev/null +++ b/netbox/project-static/select2-4.0.12/docs/pages/02.troubleshooting/chapter.md @@ -0,0 +1,11 @@ +--- +title: Troubleshooting +metadata: + description: The chapter covers some common issues you may encounter with Select2, as well as where you can go to get help. +taxonomy: + category: docs +--- + +# Troubleshooting + +The chapter covers some common issues you may encounter with Select2, as well as where you can go to get help. \ No newline at end of file diff --git a/netbox/project-static/select2-4.0.12/docs/pages/03.configuration/01.options-api/docs.md b/netbox/project-static/select2-4.0.12/docs/pages/03.configuration/01.options-api/docs.md new file mode 100644 index 000000000..06af83c7d --- /dev/null +++ b/netbox/project-static/select2-4.0.12/docs/pages/03.configuration/01.options-api/docs.md @@ -0,0 +1,52 @@ +--- +title: Options +taxonomy: + category: docs +--- + +This is a list of all the Select 2 configuration options. + +| Option | Type | Default | Description | +| ------ | ---- | ------- | ----------- | +| `adaptContainerCssClass` | | | | +| `adaptDropdownCssClass` | | | | +| `ajax` | object | `null` | Provides support for [ajax data sources](/data-sources/ajax). | +| `allowClear` | boolean | `false` | Provides support for [clearable selections](/selections#clearable-selections). | +| `amdBase` | string | `./` | See [Using Select2 with AMD or CommonJS loaders](/builds-and-modules#using-select2-with-amd-or-commonjs-loaders). | +| `amdLanguageBase` | string | `./i18n/` | See [Using Select2 with AMD or CommonJS loaders](/builds-and-modules#using-select2-with-amd-or-commonjs-loaders). | +| `closeOnSelect` | boolean | `true` | Controls whether the dropdown is [closed after a selection is made](/dropdown#forcing-the-dropdown-to-remain-open-after-selection). | +| `containerCss` | object | null | Adds custom CSS to the container. Expects key-value pairs: `{ 'css-property': 'value' }` | +| `containerCssClass` | string | `''` | | +| `data` | array of objects | `null` | Allows rendering dropdown options from an [array](/data-sources/arrays). | +| `dataAdapter` | | `SelectAdapter` | Used to override the built-in [DataAdapter](/advanced/default-adapters/data). | +| `debug` | boolean | `false` | Enable debugging messages in the browser console. | +| `dir` | | | +| `disabled` | boolean | `false` | When set to `true`, the select control will be disabled. | +| `dropdownAdapter` | | `DropdownAdapter` | Used to override the built-in [DropdownAdapter](/advanced/default-adapters/dropdown) | +| `dropdownAutoWidth` | boolean | `false` | | +| `dropdownCss` | object | null | Adds custom CSS to the dropdown. Expects key-value pairs: `{ 'css-property': 'value' }` | +| `dropdownCssClass` | string | `''` | | +| `dropdownParent` | jQuery selector or DOM node | `$(document.body)` | Allows you to [customize placement](/dropdown#dropdown-placement) of the dropdown. | +| `escapeMarkup` | callback | `Utils.escapeMarkup` | Handles [automatic escaping of content rendered by custom templates](/dropdown#built-in-escaping). | +| `initSelection` | callback | | See [`initSelection`](/upgrading/migrating-from-35#removed-the-requirement-of-initselection). **This option was deprecated in Select2 v4.0, and will be removed in v4.1.** | +| `language` | string or object | `EnglishTranslation` | Specify the [language used for Select2 messages](/i18n#message-translations). | +| `matcher` | A callback taking search `params` and the `data` object. | | Handles custom [search matching](/searching#customizing-how-results-are-matched). | +| `maximumInputLength` | integer | `0` | [Maximum number of characters](/searching#maximum-search-term-length) that may be provided for a search term. | +| `maximumSelectionLength` | integer | `0` | The maximum number of items that may be selected in a multi-select control. If the value of this option is less than 1, the number of selected items will not be limited. +| `minimumInputLength` | integer | `0` | [Minimum number of characters required to start a search.](/searching#minimum-search-term-length) | +| `minimumResultsForSearch` | integer | `0` | The minimum number of results required to [display the search box](/searching#limiting-display-of-the-search-box-to-large-result-sets). | +| `multiple` | boolean | `false` | This option enables multi-select (pillbox) mode. Select2 will automatically map the value of the `multiple` HTML attribute to this option during initialization. | +| `placeholder` | string or object | `null` | Specifies the [placeholder](/placeholders) for the control. | +| `query` | A function taking `params` (including a `callback`) | `Query` | **This option was deprecated in Select2 v4.0, and will be removed in v4.1.** | +| `resultsAdapter` | | `ResultsAdapter` | Used to override the built-in [ResultsAdapter](/advanced/default-adapters/results). | +| `selectionAdapter` | | `SingleSelection` or `MultipleSelection`, depending on the value of `multiple`. | Used to override the built-in [SelectionAdapter](/advanced/default-adapters/selection). | +| `selectOnClose` | boolean | `false` | Implements [automatic selection](/dropdown#automatic-selection) when the dropdown is closed. | +| `sorter` | callback | | | +| `tags` | boolean / array of objects | `false` | Used to enable [free text responses](/tagging). | +| `templateResult` | callback | | Customizes the way that [search results are rendered](/dropdown#templating). | +| `templateSelection` | callback | | Customizes the way that [selections are rendered](/selections#templating). | +| `theme` | string | `default` | Allows you to set the [theme](/appearance#themes). | +| `tokenizer` | callback | | A callback that handles [automatic tokenization of free-text entry](/tagging#automatic-tokenization-into-tags). | +| `tokenSeparators` | array | `[]` | The list of characters that should be used as token separators. | +| `width` | string | `resolve` | Supports [customization of the container width](/appearance#container-width). | +| `scrollAfterSelect` | boolean | `false` | If `true`, resolves issue for multiselects using `closeOnSelect: false` that caused the list of results to scroll to the first selection after each select/unselect (see https://github.com/select2/select2/pull/5150). This behaviour was intentional to deal with infinite scroll UI issues (if you need this behavior, set `false`) but it created an issue with multiselect dropdown boxes of fixed length. This pull request adds a configurable option to toggle between these two desirable behaviours. | diff --git a/netbox/project-static/select2-4.0.12/docs/pages/03.configuration/02.defaults/docs.md b/netbox/project-static/select2-4.0.12/docs/pages/03.configuration/02.defaults/docs.md new file mode 100644 index 000000000..a5a9a6355 --- /dev/null +++ b/netbox/project-static/select2-4.0.12/docs/pages/03.configuration/02.defaults/docs.md @@ -0,0 +1,31 @@ +--- +title: Global defaults +taxonomy: + category: docs +--- + +In some cases, you need to set the default options for all instances of Select2 in your web application. This is especially useful when you are migrating from past versions of Select2, or you are using non-standard options like [custom AMD builds](/getting-started/builds-and-modules#using-select2-with-amd-or-commonjs-loaders). Select2 exposes the default options through `$.fn.select2.defaults`, which allows you to set them globally. + +When setting options globally, any past defaults that have been set will be overridden. Default options are only used when an option is requested that has not been set during initialization. + +You can set default options by calling `$.fn.select2.defaults.set("key", "value")`. For example: + +``` +$.fn.select2.defaults.set("theme", "classic"); +``` + +## Nested options + +To set a default values for cache, use the same notation used for [HTML `data-*` attributes](/configuration/data-attributes). Two dashes (`--`) will be replaced by a level of nesting, and a single dash (`-`) will convert the key to a camelCase string: + +``` +$.fn.select2.defaults.set("ajax--cache", false); +``` + +## Resetting default options + +You can reset the default options to their initial values by calling + +``` +$.fn.select2.defaults.reset(); +``` diff --git a/netbox/project-static/select2-4.0.12/docs/pages/03.configuration/03.data-attributes/docs.md b/netbox/project-static/select2-4.0.12/docs/pages/03.configuration/03.data-attributes/docs.md new file mode 100644 index 000000000..7f4c7e8cf --- /dev/null +++ b/netbox/project-static/select2-4.0.12/docs/pages/03.configuration/03.data-attributes/docs.md @@ -0,0 +1,64 @@ +--- +title: data-* attributes +taxonomy: + category: docs +--- + +It is recommended that you declare your configuration options by [passing in an object](/configuration) when initializing Select2. However, you may also define your configuration options by using the HTML5 `data-*` attributes, which will override any options set when initializing Select2 and any [defaults](/configuration/defaults). + +``` + +``` + +>>> Some options are not supported as `data-*`, for example `disabled` as it's not a Javascript option, but it's an HTML [attribute](/configuration/options-api). + +## Nested (subkey) options + +Sometimes, you have options that are nested under a top-level option. For example, the options under the `ajax` option: + +``` +$(".js-example-data-ajax").select2({ + ajax: { + url: "http://example.org/api/test", + cache: false + } +}); +``` + +To write these options as `data-*` attributes, each level of nesting should be separated by two dashes (`--`): + +``` + +``` + +The value of the option is subject to jQuery's [parsing rules](https://api.jquery.com/data/#data-html5) for HTML5 data attributes. + +>>> Due to [a jQuery bug](https://github.com/jquery/jquery/issues/2070), nested options using `data-*` attributes [do not work in jQuery 1.x](https://github.com/select2/select2/issues/2969). + +## `camelCase` options + +HTML data attributes are case-insensitive, so any options which contain capital letters will be parsed as if they were all lowercase. Because Select2 has many options which are camelCase, where words are separated by uppercase letters, you must write these options out with dashes instead. So an option that would normally be called `allowClear` should instead be defined as `allow-clear`. + +This means that declaring your ` + ... + +``` + +Will be interpreted the same as initializing Select2 as... + +``` +$("select").select2({ + tags: "true", + placeholder: "Select an option", + allowClear: true +}); +``` diff --git a/netbox/project-static/select2-4.0.12/docs/pages/03.configuration/docs.md b/netbox/project-static/select2-4.0.12/docs/pages/03.configuration/docs.md new file mode 100644 index 000000000..46db73eb6 --- /dev/null +++ b/netbox/project-static/select2-4.0.12/docs/pages/03.configuration/docs.md @@ -0,0 +1,13 @@ +--- +title: Configuration +taxonomy: + category: docs +--- + +To configure custom options when you initialize Select2, simply pass an object in your call to `.select2()`: + +``` +$('.js-example-basic-single').select2({ + placeholder: 'Select an option' +}); +``` diff --git a/netbox/project-static/select2-4.0.12/docs/pages/04.appearance/docs.md b/netbox/project-static/select2-4.0.12/docs/pages/04.appearance/docs.md new file mode 100644 index 000000000..c499d31dc --- /dev/null +++ b/netbox/project-static/select2-4.0.12/docs/pages/04.appearance/docs.md @@ -0,0 +1,216 @@ +--- +title: Appearance +taxonomy: + category: docs +process: + twig: true +never_cache_twig: true +--- + +The appearance of your Select2 controls can be customized via the standard HTML attributes for `` elements. You can also initialize Select2 with `disabled: true` to get the same effect. + +
+

+ +

+ +

+ +

+
+ + +
+
+ +

+
+
+
+## Labels
+
+You can, and should, use a `
+  

+

+ +

+ + +``` + + + +``` + + + +## Container width + +Select2 will try to match the width of the original element as closely as possible. Sometimes this isn't perfect, in which case you may manually set the `width` [configuration option](/configuration): + + + + + + + + + + + + + + + + + + + + + + + + + + +
ValueDescription
'element' + Uses the computed element width from any applicable CSS rules. +
'style' + Width is determined from the select element's style attribute. If no style attribute is found, null is returned as the width. +
'resolve' + Uses the style attribute value if available, falling back to the computed element width as necessary. +
'<value>' + Valid CSS values can be passed as a string (e.g. width: '80%'). +
+ +### Example + +The two Select2 boxes below are styled to `50%` and `75%` width respectively to support responsive design: + +
+

+ +

+

+ +

+
+ +``` + + +``` + +

+
+
+
+>>>> Select2 will do its best to resolve the percent width specified via a CSS class, but it is not always possible. The best way to ensure that Select2 is using a percent based width is to inline the `style` declaration into the tag.
+
+## Themes
+
+Select2 supports custom themes using the `theme` option so you can style Select2 to match the rest of your application.
+
+These examples use the `classic` theme, which matches the old look of Select2.
+
+
+

+ +

+

+ +

+
+ +

+
+
+
+Various display options of the Select2 component can be changed.  You can access the ``) and any attributes on those elements using `.element`.
diff --git a/netbox/project-static/select2-4.0.12/docs/pages/05.options/docs.md b/netbox/project-static/select2-4.0.12/docs/pages/05.options/docs.md
new file mode 100644
index 000000000..7b56c7111
--- /dev/null
+++ b/netbox/project-static/select2-4.0.12/docs/pages/05.options/docs.md
@@ -0,0 +1,81 @@
+---
+title: Options
+taxonomy:
+    category: docs
+process:
+    twig: true
+never_cache_twig: true
+---
+
+A traditional `` element that contains `` elements will be converted into data objects using the following rules:
+
+```
+{
+  "text": "label attribute",
+  "children": [ option data object, ... ],
+  "element": HTMLOptGroupElement
+}
+```
+
+>>> Options sourced from [other data sources](/data-sources) must conform to this this same internal representation.  See ["The Select2 data format"](/data-sources/formats) for details.
+
+## Dropdown option groups
+
+In HTML, `` element:
+
+```
+
+```
+
+Select2 will automatically pick these up and render them appropriately in the dropdown.
+
+### Hierarchical options
+
+Only a single level of nesting is allowed per the HTML specification. If you nest an `` within another ``, Select2 will not be able to detect the extra level of nesting and errors may be triggered as a result.
+
+Furthermore, `` elements **cannot** be made selectable.  This is a limitation of the HTML specification and is not a limitation that Select2 can overcome.
+
+If you wish to create a true hierarchy of selectable options, use an `` and [change the style with CSS](http://stackoverflow.com/q/30820215/359284#30948247).  Please note that this approach may be considered "less accessible" as it relies on CSS styling, rather than the semantic meaning of ``, to generate the effect.
+
+## Disabling options
+
+Select2 will correctly handle disabled options, both with data coming from a standard select (when the `disabled` attribute is set) and from remote sources, where the object has `disabled: true` set.
+
+
+ +
+ +

+
+```
+
+```
+
+
diff --git a/netbox/project-static/select2-4.0.12/docs/pages/06.data-sources/01.formats/docs.md b/netbox/project-static/select2-4.0.12/docs/pages/06.data-sources/01.formats/docs.md
new file mode 100644
index 000000000..73d044b22
--- /dev/null
+++ b/netbox/project-static/select2-4.0.12/docs/pages/06.data-sources/01.formats/docs.md
@@ -0,0 +1,134 @@
+---
+title: The Select2 data format
+taxonomy:
+    category: docs
+---
+
+Select2 can render programmatically supplied data from an array or remote data source (AJAX) as dropdown options.  In order to accomplish this, Select2 expects a very specific data format.  This format consists of a JSON object containing an array of objects keyed by the `results` key.
+
+```
+{
+  "results": [
+    {
+      "id": 1,
+      "text": "Option 1"
+    },
+    {
+      "id": 2,
+      "text": "Option 2"
+    }
+  ],
+  "pagination": {
+    "more": true
+  }
+}
+```
+
+Select2 requires that each object contain an `id` and a `text` property.  Additional parameters passed in with data objects will be included on the data objects that Select2 exposes.
+
+The response object may also contain pagination data, if you would like to use the "infinite scroll" feature.  This should be specified under the `pagination` key.
+
+## Selected and disabled options
+
+You can also supply the `selected` and `disabled` properties for the options in this data structure.  For example:
+
+```
+{
+  "results": [
+    {
+      "id": 1,
+      "text": "Option 1"
+    },
+    {
+      "id": 2,
+      "text": "Option 2",
+      "selected": true
+    },
+    {
+      "id": 3,
+      "text": "Option 3",
+      "disabled": true
+    }
+  ]
+}
+```
+
+In this case, Option 2 will be pre-selected, and Option 3 will be [disabled](/options#disabling-options).
+
+## Transforming data into the required format
+
+### Generating `id` properties
+
+Select2 requires that the `id` property is used to uniquely identify the options that are displayed in the results list. If you use a property other than `id` (like `pk`) to uniquely identify an option, you need to map your old property to `id` before passing it to Select2.
+
+If you cannot do this on your server or you are in a situation where the API cannot be changed, you can do this in JavaScript before passing it to Select2:
+
+```
+var data = $.map(yourArrayData, function (obj) {
+  obj.id = obj.id || obj.pk; // replace pk with your identifier
+
+  return obj;
+});
+```
+
+### Generating `text` properties
+
+Just like with the `id` property, Select2 requires that the text that should be displayed for an option is stored in the `text` property. You can map this property from any existing property using the following JavaScript:
+
+```
+var data = $.map(yourArrayData, function (obj) {
+  obj.text = obj.text || obj.name; // replace name with the property used for the text
+
+  return obj;
+});
+```
+
+## Automatic string casting
+
+Because the `value` attribute on a `
+
+**In your HTML:**
+
+```
+
+```
+
+**In your Javascript:**
+
+```
+$('.js-data-example-ajax').select2({
+  ajax: {
+    url: 'https://api.github.com/search/repositories',
+    dataType: 'json'
+    // Additional AJAX parameters go here; see the end of this chapter for the full code of this example
+  }
+});
+```
+
+You can configure how Select2 searches for remote data using the `ajax` option.  Select2 will pass any options in the `ajax` object to jQuery's `$.ajax` function, or the `transport` function you specify.
+
+>>> For **remote data sources only**, Select2 does not create a new `
+    
+    
+    
+
+```
+
+The options that you create should have `selected="selected"` set so Select2 and the browser knows that they should be selected. The `value` attribute of the option should also be set to the value that will be returned from the server for the result, so Select2 can highlight it as selected in the dropdown. The text within the option should also reflect the value that should be displayed by default for the option.
+
+## Advanced matching of searches
+
+In past versions of Select2 the `matcher` callback processed options at every level, which limited the control that you had when displaying results, especially in cases where there was nested data. The `matcher` function was only given the individual option, even if it was a nested options, without any context.
+
+With the new [matcher function](/searching), only the root-level options are matched and matchers are expected to limit the results of any children options that they contain. This allows developers to customize how options within groups can be displayed, and modify how the results are returned.
+ 
+### Wrapper for old-style `matcher` callbacks
+
+For backwards compatibility, a wrapper function has been created that allows old-style matcher functions to be converted to the new style. 
+
+This wrapper function is only bundled in the [full version of Select2](/getting-started/builds-and-modules).  You can retrieve the function from the `select2/compat/matcher` module, which should just wrap the old matcher function.
+
+
+ +
+ +

+
+
+
+>>>> This will work for any matchers that only took in the search term and the text of the option as parameters. If your matcher relied on the third parameter containing the jQuery element representing the original `
+    
+
+```
+
+You would have previously had to get the placeholder option through the `placeholderOption`, but now you can do it through the `placeholder` option by setting an `id`.
+
+```
+$("select").select2({
+    placeholder: {
+        id: "-1",
+        placeholder: "Select an option"
+    }
+});
+```
+
+And Select2 will automatically display the placeholder when the value of the select is `-1`, which it will be by default. This does not break the old functionality of Select2 where the placeholder option was blank by default.
+
+## Display reflects the actual order of the values
+
+In past versions of Select2, choices were displayed in the order that they were selected. In cases where Select2 was used on a `
+```
+
+...then you should now declare it as...
+
+```
+
+```
+
+## Deprecated and removed methods
+
+As Select2 now uses a `` element instead. If you needed the second parameter (`triggerChange`), you should also call `.trigger("change")` on the element.
+
+```
+$("select").val("1").trigger("change"); // instead of $("select").select2("val", "1");
+```
+
+### `.select2("enable")`
+
+Select2 will respect the `disabled` property of the underlying select element. In order to enable or disable Select2, you should call `.prop('disabled', true/false)` on the ` 0 %} min="{{- min_chars -}}" {% endif %}
+            required
+            placeholder="{{"PLUGIN_SIMPLESEARCH.SEARCH_PLACEHOLDER"|t}}"
+            value="{{ query }}"
+            data-search-invalid="{{ "PLUGIN_SIMPLESEARCH.SEARCH_FIELD_MINIMUM_CHARACTERS"|t(min_chars)|raw }}"
+            data-search-separator="{{ config.system.param_sep }}"
+            data-search-input="{{ base_url }}{{ config.plugins.simplesearch.route == '@self' ? '' : (config.plugins.simplesearch.route == '/' ? '' : config.plugins.simplesearch.route) }}/query"
+        />
+        {% if config.plugins.simplesearch.display_button %}
+            
+        {% endif %}
+    
+
diff --git a/netbox/project-static/select2-4.0.12/docs/plugins/simplesearch/templates/simplesearch_results.html.twig b/netbox/project-static/select2-4.0.12/docs/plugins/simplesearch/templates/simplesearch_results.html.twig
new file mode 100644
index 000000000..215eeafbd
--- /dev/null
+++ b/netbox/project-static/select2-4.0.12/docs/plugins/simplesearch/templates/simplesearch_results.html.twig
@@ -0,0 +1,24 @@
+{% extends 'partials/simplesearch_base.html.twig' %}
+
+{% block content %}
+    
+

{{"PLUGIN_SIMPLESEARCH.SEARCH_RESULTS"|t}}

+
+ {% include 'partials/simplesearch_searchbox.html.twig' %} +
+ +

+ {% if query %} + {% set count = search_results ? search_results.count : 0 %} + {% if count == 1 %} + {{ "PLUGIN_SIMPLESEARCH.SEARCH_RESULTS_SUMMARY_SINGULAR"|t(query)|raw }} + {% else %} + {{ "PLUGIN_SIMPLESEARCH.SEARCH_RESULTS_SUMMARY_PLURAL"|t(query, count)|raw }} + {% endif %} + {% endif %} +

+ {% for page in search_results %} + {% include 'partials/simplesearch_item.html.twig' with {'page':page} %} + {% endfor %} +
+{% endblock %} diff --git a/netbox/project-static/select2-4.0.12/docs/plugins/simplesearch/templates/simplesearch_results.json.twig b/netbox/project-static/select2-4.0.12/docs/plugins/simplesearch/templates/simplesearch_results.json.twig new file mode 100644 index 000000000..d62f50298 --- /dev/null +++ b/netbox/project-static/select2-4.0.12/docs/plugins/simplesearch/templates/simplesearch_results.json.twig @@ -0,0 +1,5 @@ +{"results":[ +{%- for search_result in search_results -%} +{{- search_result.route|json_encode -}}{{ not loop.last ? ',' }} +{%- endfor -%} +]} diff --git a/netbox/project-static/select2-4.0.12/docs/screenshot.jpg b/netbox/project-static/select2-4.0.12/docs/screenshot.jpg new file mode 100644 index 000000000..8fa0b4e60 Binary files /dev/null and b/netbox/project-static/select2-4.0.12/docs/screenshot.jpg differ diff --git a/netbox/project-static/select2-4.0.12/docs/themes/.gitkeep b/netbox/project-static/select2-4.0.12/docs/themes/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/netbox/project-static/select2-4.0.12/docs/themes/learn2/CHANGELOG.md b/netbox/project-static/select2-4.0.12/docs/themes/learn2/CHANGELOG.md new file mode 100644 index 000000000..929f7c479 --- /dev/null +++ b/netbox/project-static/select2-4.0.12/docs/themes/learn2/CHANGELOG.md @@ -0,0 +1,102 @@ +# v1.7.0 +## 05/xx/2017 + +1. [](#improved) + * Added default page template. + * Added blueprints for docs and chapter pages + +# v1.6.3 +## 01/31/2017 + +1. [](#bugfix) + * Fixed changelog date + +# v1.6.2 +## 01/31/2017 + +1. [](#bugfix) + * Fixed a PHP 7.1 issue + +# v1.6.1 +## 01/24/2017 + +1. [](#new) + * Updated to FontAwesome 4.7.0 with [Grav icon](http://fontawesome.io/icon/grav/) + +# v1.6.0 +## 07/14/2016 + +1. [](#new) + * Added the spanish language +1. [](#improved) + * Remove unneeded streams from Theme YAML + * Set the page language from Grav's Language configuration. Default to english. +1. [](#bugfix) + * Fix an issue on iOS 9+ Safari scaling + +# v1.5.0 +## 01/06/2016 + +1. [](#new) + * Added keyboard prev/next navigation +1. [](#improved) + * Various language updates +1. [](#bugfix) + * Fixed a typo in CSS + +# v1.4.2 +## 12/18/2015 + +1. [](#bugfix) + * Fixed clipboard for Safari + +# v1.4.1 +## 12/11/2015 + +1. [](#new) + * Support new sidebar scrollbar + * New subtle `subtitle` styling + +# v1.4.0 +## 10/07/2015 + +1. [](#new) + * Added 1-click copy-to-clipboard feature for `code` and `pre` tags + * Added German translations + * Configurable root page +1. [](#improved) + * Wrapped topbar to remove it from error pages +1. [](#bugfix) + * Fix for bad YAML + * Fix for bad HTML in github note + +# v1.3.0 +## 09/11/2015 + +1. [](#new) + * Added configurable Google analytics code + +# v1.2.0 +## 08/25/2015 + +1. [](#improved) + * Added blueprints for Grav Admin plugin + +# v1.1.0 +## 07/19/2015 + +1. [](#new) + * Added search highlight support + * Added a footer + +# v1.0.1 +## 06/2/2015 + +1. [](#new) + * Added support for 2+ page levels + +# v1.0.0 +## 06/17/2015 + +1. [](#new) + * ChangeLog started... diff --git a/netbox/project-static/select2-4.0.12/docs/themes/learn2/LICENSE b/netbox/project-static/select2-4.0.12/docs/themes/learn2/LICENSE new file mode 100644 index 000000000..484793ad1 --- /dev/null +++ b/netbox/project-static/select2-4.0.12/docs/themes/learn2/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Grav + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/netbox/project-static/select2-4.0.12/docs/themes/learn2/README.md b/netbox/project-static/select2-4.0.12/docs/themes/learn2/README.md new file mode 100644 index 000000000..0160c67d0 --- /dev/null +++ b/netbox/project-static/select2-4.0.12/docs/themes/learn2/README.md @@ -0,0 +1,79 @@ +# Learn2 + +![Learn2](screenshot.jpg) + +Learn2 is the default [Grav Learn](http://learn.getgrav.org) theme. Simple, fast and modern. + +# Installation + +Installing the Learn2 theme can be done in one of two ways. Our GPM (Grav Package Manager) installation method enables you to quickly and easily install the theme with a simple terminal command, while the manual method enables you to do so via a zip file. + +The theme is designed to be used to provide a documentation site. You can see this in action at [](http://learn.getgrav.org) + +## GPM Installation (Preferred) + +The simplest way to install this theme is via the [Grav Package Manager (GPM)](http://learn.getgrav.org/advanced/grav-gpm) through your system's Terminal (also called the command line). From the root of your Grav install type: + + bin/gpm install learn2 + +This will install the Learn2 theme into your `/user/themes` directory within Grav. Its files can be found under `/your/site/grav/user/themes/learn2`. + +## Manual Installation + +To install this theme, just download the zip version of this repository and unzip it under `/your/site/grav/user/themes`. Then, rename the folder to `learn2`. You can find these files either on [GitHub](https://github.com/getgrav/grav-theme-learn2) or via [GetGrav.org](http://getgrav.org/downloads/themes). + +You should now have all the theme files under + + /your/site/grav/user/themes/learn2 + +>> NOTE: This theme is a modular component for Grav which requires the [Grav](http://github.com/getgrav/grav), [Error](https://github.com/getgrav/grav-theme-error) and [Problems](https://github.com/getgrav/grav-plugin-problems) plugins. + +# Updating + +As development for the Learn2 theme continues, new versions may become available that add additional features and functionality, improve compatibility with newer Grav releases, and generally provide a better user experience. Updating Learn2 is easy, and can be done through Grav's GPM system, as well as manually. + +## GPM Update (Preferred) + +The simplest way to update this theme is via the [Grav Package Manager (GPM)](http://learn.getgrav.org/advanced/grav-gpm). You can do this with this by navigating to the root directory of your Grav install using your system's Terminal (also called command line) and typing the following: + + bin/gpm update learn2 + +This command will check your Grav install to see if your Learn2 theme is due for an update. If a newer release is found, you will be asked whether or not you wish to update. To continue, type `y` and hit enter. The theme will automatically update and clear Grav's cache. + +## Manual Update + +Manually updating Learn2 is pretty simple. Here is what you will need to do to get this done: + +* Delete the `your/site/user/themes/learn2` directory. +* Download the new version of the Learn2 theme from either [GitHub](https://github.com/getgrav/grav-theme-learn2) or [GetGrav.org](http://getgrav.org/downloads/themes#extras). +* Unzip the zip file in `your/site/user/themes` and rename the resulting folder to `learn2`. +* Clear the Grav cache. The simplest way to do this is by going to the root Grav directory in terminal and typing `bin/grav clear-cache`. + +> Note: Any changes you have made to any of the files listed under this directory will also be removed and replaced by the new set. Any files located elsewhere (for example a YAML settings file placed in `user/config/themes`) will remain intact. + +## Features + +* Lightweight and minimal for optimal performance +* Fully responsive with off-page mobile navigation +* SCSS based CSS source files for easy customization +* Built specifically for providing easy to read documentation +* Fontawesome icon support + +### Supported Page Templates + +* "Docs" template +* "Chapter" template +* Error view template + + +## Setup + +If you want to set Learn2 as the default theme, you can do so by following these steps: + +* Navigate to `/your/site/grav/user/config`. +* Open the **system.yaml** file. +* Change the `theme:` setting to `theme: learn2`. +* Save your changes. +* Clear the Grav cache. The simplest way to do this is by going to the root Grav directory in Terminal and typing `bin/grav clear-cache`. + +Once this is done, you should be able to see the new theme on the frontend. Keep in mind any customizations made to the previous theme will not be reflected as all of the theme and templating information is now being pulled from the **learn2** folder. diff --git a/netbox/project-static/select2-4.0.12/docs/themes/learn2/blueprints.yaml b/netbox/project-static/select2-4.0.12/docs/themes/learn2/blueprints.yaml new file mode 100644 index 000000000..34bff4076 --- /dev/null +++ b/netbox/project-static/select2-4.0.12/docs/themes/learn2/blueprints.yaml @@ -0,0 +1,66 @@ +name: Learn2 +version: 1.6.3 +description: "Learn2 is a new modern documentation theme for Grav" +icon: book +author: + name: Team Grav + email: devs@getgrav.org + url: http://getgrav.org +homepage: https://github.com/getgrav/grav-theme-learn2 +demo: http://learn.getgrav.org +keywords: heme, docs, modern, fast, responsive, html5, css3 +bugs: https://github.com/getgrav/grav-theme-learn2/issues +license: MIT + +form: + validation: loose + fields: + top_level_version: + type: toggle + label: Top Level Version + highlight: 1 + default: 0 + options: + 1: Enabled + 0: Disabled + validate: + type: bool + + home_url: + type: text + label: Home URL + placeholder: http://getgrav.org + validate: + type: text + + google_analytics_code: + type: text + label: Google Analytics Code + placeholder: UA-XXXXXXXX-X + validate: + type: text + + github.position: + type: select + size: medium + classes: fancy + label: GitHub Position + options: + top: Top + bottom: Bottom + off: Off + + github.tree: + type: text + label: GitHub Tree + default: https://github.com/getgrav/grav-skeleton-rtfm-site/blob/develop/ + + github.commits: + type: text + label: GitHub Tree + default: https://github.com/getgrav/grav-skeleton-rtfm-site/commits/develop/ + + github.commits: + type: text + label: GitHub Tree + default: https://github.com/getgrav/grav-skeleton-rtfm-site/commits/develop/ diff --git a/netbox/project-static/select2-4.0.12/docs/themes/learn2/blueprints/chapter.yaml b/netbox/project-static/select2-4.0.12/docs/themes/learn2/blueprints/chapter.yaml new file mode 100644 index 000000000..baa2b9164 --- /dev/null +++ b/netbox/project-static/select2-4.0.12/docs/themes/learn2/blueprints/chapter.yaml @@ -0,0 +1,4 @@ +title: Chapter +'@extends': + type: default + context: blueprints://pages diff --git a/netbox/project-static/select2-4.0.12/docs/themes/learn2/blueprints/docs.yaml b/netbox/project-static/select2-4.0.12/docs/themes/learn2/blueprints/docs.yaml new file mode 100644 index 000000000..f1d430ab2 --- /dev/null +++ b/netbox/project-static/select2-4.0.12/docs/themes/learn2/blueprints/docs.yaml @@ -0,0 +1,4 @@ +title: Docs +'@extends': + type: default + context: blueprints://pages diff --git a/netbox/project-static/select2-4.0.12/docs/themes/learn2/css-compiled/nucleus.css b/netbox/project-static/select2-4.0.12/docs/themes/learn2/css-compiled/nucleus.css new file mode 100644 index 000000000..ed2c0a38a --- /dev/null +++ b/netbox/project-static/select2-4.0.12/docs/themes/learn2/css-compiled/nucleus.css @@ -0,0 +1,617 @@ +*, *::before, *::after { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; } + +@-webkit-viewport { + width: device-width; } +@-moz-viewport { + width: device-width; } +@-ms-viewport { + width: device-width; } +@-o-viewport { + width: device-width; } +@viewport { + width: device-width; } +html { + font-size: 100%; + -ms-text-size-adjust: 100%; + -webkit-text-size-adjust: 100%; } + +body { + margin: 0; } + +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +main, +nav, +section, +summary { + display: block; } + +audio, +canvas, +progress, +video { + display: inline-block; + vertical-align: baseline; } + +audio:not([controls]) { + display: none; + height: 0; } + +[hidden], +template { + display: none; } + +a { + background: transparent; + text-decoration: none; } + +a:active, +a:hover { + outline: 0; } + +abbr[title] { + border-bottom: 1px dotted; } + +b, +strong { + font-weight: bold; } + +dfn { + font-style: italic; } + +mark { + background: #FFFF27; + color: #333; } + +sub, +sup { + font-size: 0.8rem; + line-height: 0; + position: relative; + vertical-align: baseline; } + +sup { + top: -0.5em; } + +sub { + bottom: -0.25em; } + +img { + border: 0; + max-width: 100%; } + +svg:not(:root) { + overflow: hidden; } + +figure { + margin: 1em 40px; } + +hr { + height: 0; } + +pre { + overflow: auto; } + +button, +input, +optgroup, +select, +textarea { + color: inherit; + font: inherit; + margin: 0; } + +button { + overflow: visible; } + +button, +select { + text-transform: none; } + +button, +html input[type="button"], +input[type="reset"], +input[type="submit"] { + -webkit-appearance: button; + cursor: pointer; } + +button[disabled], +html input[disabled] { + cursor: default; } + +button::-moz-focus-inner, +input::-moz-focus-inner { + border: 0; + padding: 0; } + +input { + line-height: normal; } + +input[type="checkbox"], +input[type="radio"] { + padding: 0; } + +input[type="number"]::-webkit-inner-spin-button, +input[type="number"]::-webkit-outer-spin-button { + height: auto; } + +input[type="search"] { + -webkit-appearance: textfield; } + +input[type="search"]::-webkit-search-cancel-button, +input[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; } + +legend { + border: 0; + padding: 0; } + +textarea { + overflow: auto; } + +optgroup { + font-weight: bold; } + +table { + border-collapse: collapse; + border-spacing: 0; + table-layout: fixed; + width: 100%; } + +tr, td, th { + vertical-align: middle; } + +th, td { + padding: 0.425rem 0; } + +th { + text-align: left; } + +.container { + width: 75em; + margin: 0 auto; + padding: 0; } + @media only all and (min-width: 60em) and (max-width: 74.938em) { + .container { + width: 60em; } } + @media only all and (min-width: 48em) and (max-width: 59.938em) { + .container { + width: 48em; } } + @media only all and (min-width: 30.063em) and (max-width: 47.938em) { + .container { + width: 30em; } } + @media only all and (max-width: 30em) { + .container { + width: 100%; } } + +.grid { + display: -webkit-box; + display: -moz-box; + display: box; + display: -webkit-flex; + display: -moz-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-flow: row; + -moz-flex-flow: row; + flex-flow: row; + list-style: none; + margin: 0; + padding: 0; } + @media only all and (max-width: 47.938em) { + .grid { + -webkit-flex-flow: row wrap; + -moz-flex-flow: row wrap; + flex-flow: row wrap; } } + +.block { + -webkit-box-flex: 1; + -moz-box-flex: 1; + box-flex: 1; + -webkit-flex: 1; + -moz-flex: 1; + -ms-flex: 1; + flex: 1; + min-width: 0; + min-height: 0; } + @media only all and (max-width: 47.938em) { + .block { + -webkit-box-flex: 0; + -moz-box-flex: 0; + box-flex: 0; + -webkit-flex: 0 100%; + -moz-flex: 0 100%; + -ms-flex: 0 100%; + flex: 0 100%; } } + +.content { + margin: 0.625rem; + padding: 0.938rem; } + +@media only all and (max-width: 47.938em) { + body [class*="size-"] { + -webkit-box-flex: 0; + -moz-box-flex: 0; + box-flex: 0; + -webkit-flex: 0 100%; + -moz-flex: 0 100%; + -ms-flex: 0 100%; + flex: 0 100%; } } + +.size-1-2 { + -webkit-box-flex: 0; + -moz-box-flex: 0; + box-flex: 0; + -webkit-flex: 0 50%; + -moz-flex: 0 50%; + -ms-flex: 0 50%; + flex: 0 50%; } + +.size-1-3 { + -webkit-box-flex: 0; + -moz-box-flex: 0; + box-flex: 0; + -webkit-flex: 0 33.33333%; + -moz-flex: 0 33.33333%; + -ms-flex: 0 33.33333%; + flex: 0 33.33333%; } + +.size-1-4 { + -webkit-box-flex: 0; + -moz-box-flex: 0; + box-flex: 0; + -webkit-flex: 0 25%; + -moz-flex: 0 25%; + -ms-flex: 0 25%; + flex: 0 25%; } + +.size-1-5 { + -webkit-box-flex: 0; + -moz-box-flex: 0; + box-flex: 0; + -webkit-flex: 0 20%; + -moz-flex: 0 20%; + -ms-flex: 0 20%; + flex: 0 20%; } + +.size-1-6 { + -webkit-box-flex: 0; + -moz-box-flex: 0; + box-flex: 0; + -webkit-flex: 0 16.66667%; + -moz-flex: 0 16.66667%; + -ms-flex: 0 16.66667%; + flex: 0 16.66667%; } + +.size-1-7 { + -webkit-box-flex: 0; + -moz-box-flex: 0; + box-flex: 0; + -webkit-flex: 0 14.28571%; + -moz-flex: 0 14.28571%; + -ms-flex: 0 14.28571%; + flex: 0 14.28571%; } + +.size-1-8 { + -webkit-box-flex: 0; + -moz-box-flex: 0; + box-flex: 0; + -webkit-flex: 0 12.5%; + -moz-flex: 0 12.5%; + -ms-flex: 0 12.5%; + flex: 0 12.5%; } + +.size-1-9 { + -webkit-box-flex: 0; + -moz-box-flex: 0; + box-flex: 0; + -webkit-flex: 0 11.11111%; + -moz-flex: 0 11.11111%; + -ms-flex: 0 11.11111%; + flex: 0 11.11111%; } + +.size-1-10 { + -webkit-box-flex: 0; + -moz-box-flex: 0; + box-flex: 0; + -webkit-flex: 0 10%; + -moz-flex: 0 10%; + -ms-flex: 0 10%; + flex: 0 10%; } + +.size-1-11 { + -webkit-box-flex: 0; + -moz-box-flex: 0; + box-flex: 0; + -webkit-flex: 0 9.09091%; + -moz-flex: 0 9.09091%; + -ms-flex: 0 9.09091%; + flex: 0 9.09091%; } + +.size-1-12 { + -webkit-box-flex: 0; + -moz-box-flex: 0; + box-flex: 0; + -webkit-flex: 0 8.33333%; + -moz-flex: 0 8.33333%; + -ms-flex: 0 8.33333%; + flex: 0 8.33333%; } + +@media only all and (min-width: 48em) and (max-width: 59.938em) { + .size-tablet-1-2 { + -webkit-box-flex: 0; + -moz-box-flex: 0; + box-flex: 0; + -webkit-flex: 0 50%; + -moz-flex: 0 50%; + -ms-flex: 0 50%; + flex: 0 50%; } + + .size-tablet-1-3 { + -webkit-box-flex: 0; + -moz-box-flex: 0; + box-flex: 0; + -webkit-flex: 0 33.33333%; + -moz-flex: 0 33.33333%; + -ms-flex: 0 33.33333%; + flex: 0 33.33333%; } + + .size-tablet-1-4 { + -webkit-box-flex: 0; + -moz-box-flex: 0; + box-flex: 0; + -webkit-flex: 0 25%; + -moz-flex: 0 25%; + -ms-flex: 0 25%; + flex: 0 25%; } + + .size-tablet-1-5 { + -webkit-box-flex: 0; + -moz-box-flex: 0; + box-flex: 0; + -webkit-flex: 0 20%; + -moz-flex: 0 20%; + -ms-flex: 0 20%; + flex: 0 20%; } + + .size-tablet-1-6 { + -webkit-box-flex: 0; + -moz-box-flex: 0; + box-flex: 0; + -webkit-flex: 0 16.66667%; + -moz-flex: 0 16.66667%; + -ms-flex: 0 16.66667%; + flex: 0 16.66667%; } + + .size-tablet-1-7 { + -webkit-box-flex: 0; + -moz-box-flex: 0; + box-flex: 0; + -webkit-flex: 0 14.28571%; + -moz-flex: 0 14.28571%; + -ms-flex: 0 14.28571%; + flex: 0 14.28571%; } + + .size-tablet-1-8 { + -webkit-box-flex: 0; + -moz-box-flex: 0; + box-flex: 0; + -webkit-flex: 0 12.5%; + -moz-flex: 0 12.5%; + -ms-flex: 0 12.5%; + flex: 0 12.5%; } + + .size-tablet-1-9 { + -webkit-box-flex: 0; + -moz-box-flex: 0; + box-flex: 0; + -webkit-flex: 0 11.11111%; + -moz-flex: 0 11.11111%; + -ms-flex: 0 11.11111%; + flex: 0 11.11111%; } + + .size-tablet-1-10 { + -webkit-box-flex: 0; + -moz-box-flex: 0; + box-flex: 0; + -webkit-flex: 0 10%; + -moz-flex: 0 10%; + -ms-flex: 0 10%; + flex: 0 10%; } + + .size-tablet-1-11 { + -webkit-box-flex: 0; + -moz-box-flex: 0; + box-flex: 0; + -webkit-flex: 0 9.09091%; + -moz-flex: 0 9.09091%; + -ms-flex: 0 9.09091%; + flex: 0 9.09091%; } + + .size-tablet-1-12 { + -webkit-box-flex: 0; + -moz-box-flex: 0; + box-flex: 0; + -webkit-flex: 0 8.33333%; + -moz-flex: 0 8.33333%; + -ms-flex: 0 8.33333%; + flex: 0 8.33333%; } } +@media only all and (max-width: 47.938em) { + @supports not (flex-wrap: wrap) { + .grid { + display: block; + -webkit-box-lines: inherit; + -moz-box-lines: inherit; + box-lines: inherit; + -webkit-flex-wrap: inherit; + -moz-flex-wrap: inherit; + -ms-flex-wrap: inherit; + flex-wrap: inherit; } + + .block { + display: block; + -webkit-box-flex: inherit; + -moz-box-flex: inherit; + box-flex: inherit; + -webkit-flex: inherit; + -moz-flex: inherit; + -ms-flex: inherit; + flex: inherit; } } } +.first-block { + -webkit-box-ordinal-group: 0; + -webkit-order: -1; + -ms-flex-order: -1; + order: -1; } + +.last-block { + -webkit-box-ordinal-group: 2; + -webkit-order: 1; + -ms-flex-order: 1; + order: 1; } + +.fixed-blocks { + -webkit-flex-flow: row wrap; + -moz-flex-flow: row wrap; + flex-flow: row wrap; } + .fixed-blocks .block { + -webkit-box-flex: inherit; + -moz-box-flex: inherit; + box-flex: inherit; + -webkit-flex: inherit; + -moz-flex: inherit; + -ms-flex: inherit; + flex: inherit; + width: 25%; } + @media only all and (min-width: 60em) and (max-width: 74.938em) { + .fixed-blocks .block { + width: 33.33333%; } } + @media only all and (min-width: 48em) and (max-width: 59.938em) { + .fixed-blocks .block { + width: 50%; } } + @media only all and (max-width: 47.938em) { + .fixed-blocks .block { + width: 100%; } } + +body { + font-size: 1.05rem; + line-height: 1.7; } + +h1, h2, h3, h4, h5, h6 { + margin: 0.85rem 0 1.7rem 0; + text-rendering: optimizeLegibility; } + +h1 { + font-size: 3.25rem; } + +h2 { + font-size: 2.55rem; } + +h3 { + font-size: 2.15rem; } + +h4 { + font-size: 1.8rem; } + +h5 { + font-size: 1.4rem; } + +h6 { + font-size: 0.9rem; } + +p { + margin: 1.7rem 0; } + +ul, ol { + margin-top: 1.7rem; + margin-bottom: 1.7rem; } + ul ul, ul ol, ol ul, ol ol { + margin-top: 0; + margin-bottom: 0; } + +blockquote { + margin: 1.7rem 0; + padding-left: 0.85rem; } + +cite { + display: block; + font-size: 0.925rem; } + cite:before { + content: "\2014 \0020"; } + +pre { + margin: 1.7rem 0; + padding: 0.938rem; } + +code { + vertical-align: bottom; } + +small { + font-size: 0.925rem; } + +hr { + border-left: none; + border-right: none; + border-top: none; + margin: 1.7rem 0; } + +fieldset { + border: 0; + padding: 0.938rem; + margin: 0 0 1.7rem 0; } + +input, +label, +select { + display: block; } + +label { + margin-bottom: 0.425rem; } + label.required:after { + content: "*"; } + label abbr { + display: none; } + +textarea, input[type="email"], input[type="number"], input[type="password"], input[type="search"], input[type="tel"], input[type="text"], input[type="url"], input[type="color"], input[type="date"], input[type="datetime"], input[type="datetime-local"], input[type="month"], input[type="time"], input[type="week"], select[multiple=multiple] { + -webkit-transition: border-color; + -moz-transition: border-color; + transition: border-color; + border-radius: 0.1875rem; + margin-bottom: 0.85rem; + padding: 0.425rem 0.425rem; + width: 100%; } + textarea:focus, input[type="email"]:focus, input[type="number"]:focus, input[type="password"]:focus, input[type="search"]:focus, input[type="tel"]:focus, input[type="text"]:focus, input[type="url"]:focus, input[type="color"]:focus, input[type="date"]:focus, input[type="datetime"]:focus, input[type="datetime-local"]:focus, input[type="month"]:focus, input[type="time"]:focus, input[type="week"]:focus, select[multiple=multiple]:focus { + outline: none; } + +textarea { + resize: vertical; } + +input[type="checkbox"], input[type="radio"] { + display: inline; + margin-right: 0.425rem; } + +input[type="file"] { + width: 100%; } + +select { + width: auto; + max-width: 100%; + margin-bottom: 1.7rem; } + +button, +input[type="submit"] { + cursor: pointer; + user-select: none; + vertical-align: middle; + white-space: nowrap; + border: inherit; } + +/*# sourceMappingURL=nucleus.css.map */ diff --git a/netbox/project-static/select2-4.0.12/docs/themes/learn2/css-compiled/nucleus.css.map b/netbox/project-static/select2-4.0.12/docs/themes/learn2/css-compiled/nucleus.css.map new file mode 100644 index 000000000..8e4a50ef8 --- /dev/null +++ b/netbox/project-static/select2-4.0.12/docs/themes/learn2/css-compiled/nucleus.css.map @@ -0,0 +1,7 @@ +{ +"version": 3, +"mappings": "AAAA,sBAAuB;ECSf,kBAAoB,EDRP,UAAU;ECavB,eAAiB,EDbJ,UAAU;EC4BvB,UAAY,ED5BC,UAAU;;AAG/B,iBAAqC;EAAnB,KAAK,EAAC,YAAY;AACpC,cAAkC;EAAnB,KAAK,EAAC,YAAY;AACjC,aAAiC;EAAnB,KAAK,EAAC,YAAY;AAChC,YAAgC;EAAnB,KAAK,EAAC,YAAY;AAC/B,SAA6B;EAAnB,KAAK,EAAC,YAAY;AAE5B,IAAK;EACJ,SAAS,EAAE,IAAI;EACf,oBAAoB,EAAE,IAAI;EAC1B,wBAAwB,EAAE,IAAI;;AAG/B,IAAK;EACJ,MAAM,EAAE,CAAC;;AAGV;;;;;;;;;;;OAWQ;EACP,OAAO,EAAE,KAAK;;AAGf;;;KAGM;EACL,OAAO,EAAE,YAAY;EACrB,cAAc,EAAE,QAAQ;;AAGzB,qBAAsB;EACrB,OAAO,EAAE,IAAI;EACb,MAAM,EAAE,CAAC;;AAGV;QACS;EACR,OAAO,EAAE,IAAI;;AAGd,CAAE;EACD,UAAU,EAAE,WAAW;EACvB,eAAe,EAAE,IAAI;;AAGtB;OACQ;EACP,OAAO,EAAE,CAAC;;AAGX,WAAY;EACX,aAAa,EAAE,UAAU;;AAG1B;MACO;EACN,WAAW,EAAE,IAAI;;AAGlB,GAAI;EACH,UAAU,EAAE,MAAM;;AAGnB,IAAK;EACJ,UAAU,EAAE,OAAO;EACnB,KAAK,EAAE,IAAI;;AAGZ;GACI;EACH,SAAS,EAAE,MAAuB;EAClC,WAAW,EAAE,CAAC;EACd,QAAQ,EAAE,QAAQ;EAClB,cAAc,EAAE,QAAQ;;AAGzB,GAAI;EACH,GAAG,EAAE,MAAM;;AAGZ,GAAI;EACH,MAAM,EAAE,OAAO;;AAGhB,GAAI;EACH,MAAM,EAAE,CAAC;EACT,SAAS,EAAE,IAAI;;AAGhB,cAAe;EACd,QAAQ,EAAE,MAAM;;AAGjB,MAAO;EACN,MAAM,EAAE,QAAQ;;AAGjB,EAAG;EACF,MAAM,EAAE,CAAC;;AAGV,GAAI;EACH,QAAQ,EAAE,IAAI;;AAUf;;;;QAIS;EACR,KAAK,EAAE,OAAO;EACd,IAAI,EAAE,OAAO;EACb,MAAM,EAAE,CAAC;;AAGV,MAAO;EACN,QAAQ,EAAE,OAAO;;AAGlB;MACO;EACN,cAAc,EAAE,IAAI;;AAGrB;;;oBAGqB;EACpB,kBAAkB,EAAE,MAAM;EAC1B,MAAM,EAAE,OAAO;;AAGhB;oBACqB;EACpB,MAAM,EAAE,OAAO;;AAGhB;uBACwB;EACvB,MAAM,EAAE,CAAC;EACT,OAAO,EAAE,CAAC;;AAGX,KAAM;EACL,WAAW,EAAE,MAAM;;AAGpB;mBACoB;EACnB,OAAO,EAAE,CAAC;;AAGX;+CACgD;EAC/C,MAAM,EAAE,IAAI;;AAGb,oBAAqB;EACpB,kBAAkB,EAAE,SAAS;;AAG9B;+CACgD;EAC/C,kBAAkB,EAAE,IAAI;;AAGzB,MAAO;EACN,MAAM,EAAE,CAAC;EACT,OAAO,EAAE,CAAC;;AAGX,QAAS;EACR,QAAQ,EAAE,IAAI;;AAGf,QAAS;EACR,WAAW,EAAE,IAAI;;AAGlB,KAAM;EACL,eAAe,EAAE,QAAQ;EACzB,cAAc,EAAE,CAAC;EACjB,YAAY,EAAE,KAAK;EACnB,KAAK,EAAE,IAAI;;AAGZ,UAAW;EACV,cAAc,EAAE,MAAM;;AAGvB,MAAO;EACN,OAAO,EAAE,UAAuB;;AAGjC,EAAG;EACF,UAAU,EAAE,IAAI;;AEtNjB,UAAW;EACV,KAAK,ECDqB,IAAQ;EDElC,MAAM,EAAE,MAAM;EACd,OAAO,EAAE,CAAC;EEET,+DAA4G;IFL9G,UAAW;MAKT,KAAK,ECJgB,IAAQ;ECO7B,+DAAqG;IFRvG,UAAW;MAQT,KAAK,ECNe,IAAQ;ECS5B,mEAAkH;IFXpH,UAAW;MAWT,KAAK,ECRmB,IAAQ;ECWhC,qCAA+D;IFdjE,UAAW;MAcT,KAAK,ECVe,IAAI;;ADe1B,KAAM;EGiDE,OAAO,EAAE,WAAW;EACpB,OAAO,EAAE,QAAQ;EACjB,OAAO,EAAE,GAAG;EAGZ,OAAO,EAAE,YAAY;EACrB,OAAO,EAAE,SAAS;EAClB,OAAO,EAAE,WAAW;EACpB,OAAO,EAAE,IAAI;EJpEb,iBAAoB,ECaR,GAAG;EDRf,cAAiB,ECQL,GAAG;EDOf,SAAY,ECPA,GAAG;EACtB,UAAU,EAAE,IAAI;EAChB,MAAM,EAAE,CAAC;EACT,OAAO,EAAE,CAAC;EEJT,yCAAiE;IFDnE,KAAM;MDXE,iBAAoB,ECkBP,QAAQ;MDbrB,cAAiB,ECaJ,QAAQ;MDErB,SAAY,ECFC,QAAQ;;AAI7B,MAAO;EDtBC,gBAAoB,EI6FZ,CAAc;EJxFtB,aAAiB,EIwFT,CAAc;EJzEtB,QAAY,EIyEJ,CAAc;EJ7FtB,YAAoB,ECuBb,CAAC;EDlBR,SAAiB,ECkBV,CAAC;EDbR,QAAgB,ECaT,CAAC;EDHR,IAAY,ECGL,CAAC;EACZ,SAAS,EAAE,CAAC;EACZ,UAAU,EAAE,CAAC;EEbf,yCAAiE;IFUnE,MAAO;MDtBC,gBAAoB,EI6FZ,CAAc;MJxFtB,aAAiB,EIwFT,CAAc;MJzEtB,QAAY,EIyEJ,CAAc;MJ7FtB,YAAoB,EC2BZ,MAAM;MDtBd,SAAiB,ECsBT,MAAM;MDjBd,QAAgB,ECiBR,MAAM;MDPd,IAAY,ECOJ,MAAM;;AAKtB,QAAS;EACR,MAAM,EIzCa,QAAQ;EJ0C3B,OAAO,EIzCa,QAAQ;;AFmB3B,yCAAiE;EFyBnE,qBAAsB;IDrCd,gBAAoB,EI6FZ,CAAc;IJxFtB,aAAiB,EIwFT,CAAc;IJzEtB,QAAY,EIyEJ,CAAc;IJ7FtB,YAAoB,ECuCZ,MAAM;IDlCd,SAAiB,ECkCT,MAAM;ID7Bd,QAAgB,EC6BR,MAAM;IDnBd,IAAY,ECmBJ,MAAM;;AAKtB,SAAU;ED5CF,gBAAoB,EI6FZ,CAAc;EJxFtB,aAAiB,EIwFT,CAAc;EJzEtB,QAAY,EIyEJ,CAAc;EJ7FtB,YAAoB,EC6Cb,KAAiB;EDxCxB,SAAiB,ECwCV,KAAiB;EDnCxB,QAAgB,ECmCT,KAAiB;EDzBxB,IAAY,ECyBL,KAAiB;;AAGhC,SAAU;EDhDF,gBAAoB,EI6FZ,CAAc;EJxFtB,aAAiB,EIwFT,CAAc;EJzEtB,QAAY,EIyEJ,CAAc;EJ7FtB,YAAoB,ECiDb,WAAiB;ED5CxB,SAAiB,EC4CV,WAAiB;EDvCxB,QAAgB,ECuCT,WAAiB;ED7BxB,IAAY,EC6BL,WAAiB;;AAGhC,SAAU;EDpDF,gBAAoB,EI6FZ,CAAc;EJxFtB,aAAiB,EIwFT,CAAc;EJzEtB,QAAY,EIyEJ,CAAc;EJ7FtB,YAAoB,ECqDb,KAAiB;EDhDxB,SAAiB,ECgDV,KAAiB;ED3CxB,QAAgB,EC2CT,KAAiB;EDjCxB,IAAY,ECiCL,KAAiB;;AAGhC,SAAU;EDxDF,gBAAoB,EI6FZ,CAAc;EJxFtB,aAAiB,EIwFT,CAAc;EJzEtB,QAAY,EIyEJ,CAAc;EJ7FtB,YAAoB,ECyDb,KAAiB;EDpDxB,SAAiB,ECoDV,KAAiB;ED/CxB,QAAgB,EC+CT,KAAiB;EDrCxB,IAAY,ECqCL,KAAiB;;AAGhC,SAAU;ED5DF,gBAAoB,EI6FZ,CAAc;EJxFtB,aAAiB,EIwFT,CAAc;EJzEtB,QAAY,EIyEJ,CAAc;EJ7FtB,YAAoB,EC6Db,WAAiB;EDxDxB,SAAiB,ECwDV,WAAiB;EDnDxB,QAAgB,ECmDT,WAAiB;EDzCxB,IAAY,ECyCL,WAAiB;;AAGhC,SAAU;EDhEF,gBAAoB,EI6FZ,CAAc;EJxFtB,aAAiB,EIwFT,CAAc;EJzEtB,QAAY,EIyEJ,CAAc;EJ7FtB,YAAoB,ECiEb,WAAiB;ED5DxB,SAAiB,EC4DV,WAAiB;EDvDxB,QAAgB,ECuDT,WAAiB;ED7CxB,IAAY,EC6CL,WAAiB;;AAGhC,SAAU;EDpEF,gBAAoB,EI6FZ,CAAc;EJxFtB,aAAiB,EIwFT,CAAc;EJzEtB,QAAY,EIyEJ,CAAc;EJ7FtB,YAAoB,ECqEb,OAAiB;EDhExB,SAAiB,ECgEV,OAAiB;ED3DxB,QAAgB,EC2DT,OAAiB;EDjDxB,IAAY,ECiDL,OAAiB;;AAGhC,SAAU;EDxEF,gBAAoB,EI6FZ,CAAc;EJxFtB,aAAiB,EIwFT,CAAc;EJzEtB,QAAY,EIyEJ,CAAc;EJ7FtB,YAAoB,ECyEb,WAAiB;EDpExB,SAAiB,ECoEV,WAAiB;ED/DxB,QAAgB,EC+DT,WAAiB;EDrDxB,IAAY,ECqDL,WAAiB;;AAGhC,UAAW;ED5EH,gBAAoB,EI6FZ,CAAc;EJxFtB,aAAiB,EIwFT,CAAc;EJzEtB,QAAY,EIyEJ,CAAc;EJ7FtB,YAAoB,EC6Eb,KAAkB;EDxEzB,SAAiB,ECwEV,KAAkB;EDnEzB,QAAgB,ECmET,KAAkB;EDzDzB,IAAY,ECyDL,KAAkB;;AAGjC,UAAW;EDhFH,gBAAoB,EI6FZ,CAAc;EJxFtB,aAAiB,EIwFT,CAAc;EJzEtB,QAAY,EIyEJ,CAAc;EJ7FtB,YAAoB,ECiFb,UAAkB;ED5EzB,SAAiB,EC4EV,UAAkB;EDvEzB,QAAgB,ECuET,UAAkB;ED7DzB,IAAY,EC6DL,UAAkB;;AAGjC,UAAW;EDpFH,gBAAoB,EI6FZ,CAAc;EJxFtB,aAAiB,EIwFT,CAAc;EJzEtB,QAAY,EIyEJ,CAAc;EJ7FtB,YAAoB,ECqFb,UAAkB;EDhFzB,SAAiB,ECgFV,UAAkB;ED3EzB,QAAgB,EC2ET,UAAkB;EDjEzB,IAAY,ECiEL,UAAkB;;AErF/B,+DAAqG;EFyFtG,gBAAiB;IDzFV,gBAAoB,EI6FZ,CAAc;IJxFtB,aAAiB,EIwFT,CAAc;IJzEtB,QAAY,EIyEJ,CAAc;IJ7FtB,YAAoB,EC0FZ,KAAiB;IDrFzB,SAAiB,ECqFT,KAAiB;IDhFzB,QAAgB,ECgFR,KAAiB;IDtEzB,IAAY,ECsEJ,KAAiB;;EAGhC,gBAAiB;ID7FV,gBAAoB,EI6FZ,CAAc;IJxFtB,aAAiB,EIwFT,CAAc;IJzEtB,QAAY,EIyEJ,CAAc;IJ7FtB,YAAoB,EC8FZ,WAAiB;IDzFzB,SAAiB,ECyFT,WAAiB;IDpFzB,QAAgB,ECoFR,WAAiB;ID1EzB,IAAY,EC0EJ,WAAiB;;EAGhC,gBAAiB;IDjGV,gBAAoB,EI6FZ,CAAc;IJxFtB,aAAiB,EIwFT,CAAc;IJzEtB,QAAY,EIyEJ,CAAc;IJ7FtB,YAAoB,ECkGZ,KAAiB;ID7FzB,SAAiB,EC6FT,KAAiB;IDxFzB,QAAgB,ECwFR,KAAiB;ID9EzB,IAAY,EC8EJ,KAAiB;;EAGhC,gBAAiB;IDrGV,gBAAoB,EI6FZ,CAAc;IJxFtB,aAAiB,EIwFT,CAAc;IJzEtB,QAAY,EIyEJ,CAAc;IJ7FtB,YAAoB,ECsGZ,KAAiB;IDjGzB,SAAiB,ECiGT,KAAiB;ID5FzB,QAAgB,EC4FR,KAAiB;IDlFzB,IAAY,ECkFJ,KAAiB;;EAGhC,gBAAiB;IDzGV,gBAAoB,EI6FZ,CAAc;IJxFtB,aAAiB,EIwFT,CAAc;IJzEtB,QAAY,EIyEJ,CAAc;IJ7FtB,YAAoB,EC0GZ,WAAiB;IDrGzB,SAAiB,ECqGT,WAAiB;IDhGzB,QAAgB,ECgGR,WAAiB;IDtFzB,IAAY,ECsFJ,WAAiB;;EAGhC,gBAAiB;ID7GV,gBAAoB,EI6FZ,CAAc;IJxFtB,aAAiB,EIwFT,CAAc;IJzEtB,QAAY,EIyEJ,CAAc;IJ7FtB,YAAoB,EC8GZ,WAAiB;IDzGzB,SAAiB,ECyGT,WAAiB;IDpGzB,QAAgB,ECoGR,WAAiB;ID1FzB,IAAY,EC0FJ,WAAiB;;EAGhC,gBAAiB;IDjHV,gBAAoB,EI6FZ,CAAc;IJxFtB,aAAiB,EIwFT,CAAc;IJzEtB,QAAY,EIyEJ,CAAc;IJ7FtB,YAAoB,ECkHZ,OAAiB;ID7GzB,SAAiB,EC6GT,OAAiB;IDxGzB,QAAgB,ECwGR,OAAiB;ID9FzB,IAAY,EC8FJ,OAAiB;;EAGhC,gBAAiB;IDrHV,gBAAoB,EI6FZ,CAAc;IJxFtB,aAAiB,EIwFT,CAAc;IJzEtB,QAAY,EIyEJ,CAAc;IJ7FtB,YAAoB,ECsHZ,WAAiB;IDjHzB,SAAiB,ECiHT,WAAiB;ID5GzB,QAAgB,EC4GR,WAAiB;IDlGzB,IAAY,ECkGJ,WAAiB;;EAGhC,iBAAkB;IDzHX,gBAAoB,EI6FZ,CAAc;IJxFtB,aAAiB,EIwFT,CAAc;IJzEtB,QAAY,EIyEJ,CAAc;IJ7FtB,YAAoB,EC0HZ,KAAkB;IDrH1B,SAAiB,ECqHT,KAAkB;IDhH1B,QAAgB,ECgHR,KAAkB;IDtG1B,IAAY,ECsGJ,KAAkB;;EAGjC,iBAAkB;ID7HX,gBAAoB,EI6FZ,CAAc;IJxFtB,aAAiB,EIwFT,CAAc;IJzEtB,QAAY,EIyEJ,CAAc;IJ7FtB,YAAoB,EC8HZ,UAAkB;IDzH1B,SAAiB,ECyHT,UAAkB;IDpH1B,QAAgB,ECoHR,UAAkB;ID1G1B,IAAY,EC0GJ,UAAkB;;EAGjC,iBAAkB;IDjIX,gBAAoB,EI6FZ,CAAc;IJxFtB,aAAiB,EIwFT,CAAc;IJzEtB,QAAY,EIyEJ,CAAc;IJ7FtB,YAAoB,ECkIZ,UAAkB;ID7H1B,SAAiB,EC6HT,UAAkB;IDxH1B,QAAgB,ECwHR,UAAkB;ID9G1B,IAAY,EC8GJ,UAAkB;AEtHhC,yCAAiE;EF4HlE,+BASC;IARA,KAAM;MACL,OAAO,EAAE,KAAK;MD1IT,iBAAoB,EIsJZ,OAAM;MJjJd,cAAiB,EIiJT,OAAM;MJlId,SAAY,EIkIJ,OAAM;MJtJd,iBAAoB,EIsJZ,OAAM;MJjJd,cAAiB,EIiJT,OAAM;MJ5Id,aAAgB,EI4IR,OAAM;MJlId,SAAY,EIkIJ,OAAM;;IHTpB,MAAO;MACN,OAAO,EAAE,KAAK;MD9IT,gBAAoB,EI6FZ,OAAc;MJxFtB,aAAiB,EIwFT,OAAc;MJzEtB,QAAY,EIyEJ,OAAc;MJ7FtB,YAAoB,EI6FZ,OAAc;MJxFtB,SAAiB,EIwFT,OAAc;MJnFtB,QAAgB,EImFR,OAAc;MJzEtB,IAAY,EIyEJ,OAAc;AHwD9B,YAAa;EACX,yBAAyB,EAAE,CAAC;EAC5B,aAAa,EAAE,EAAE;EACjB,cAAc,EAAE,EAAE;EAClB,KAAK,EAAE,EAAE;;AAGX,WAAY;EACV,yBAAyB,EAAE,CAAC;EAC5B,aAAa,EAAE,CAAC;EAChB,cAAc,EAAE,CAAC;EACjB,KAAK,EAAE,CAAC;;AAIV,aAAc;EDpKN,iBAAoB,ECqKR,QAAQ;EDhKpB,cAAiB,ECgKL,QAAQ;EDjJpB,SAAY,ECiJA,QAAQ;EAC3B,oBAAO;IDtKA,gBAAoB,EI6FZ,OAAc;IJxFtB,aAAiB,EIwFT,OAAc;IJzEtB,QAAY,EIyEJ,OAAc;IJ7FtB,YAAoB,EI6FZ,OAAc;IJxFtB,SAAiB,EIwFT,OAAc;IJnFtB,QAAgB,EImFR,OAAc;IJzEtB,IAAY,EIyEJ,OAAc;IH2E5B,KAAK,EI5Ke,GAAe;IFCnC,+DAA4G;MFyK7G,oBAAO;QAIL,KAAK,EI7KgB,SAAe;IFGrC,+DAAqG;MFsKtG,oBAAO;QAOL,KAAK,EI/Ke,GAAe;IFcpC,yCAAiE;MF0JlE,oBAAO;QAUL,KAAK,EAAE,IAAI;;AKxLd,IAAK;EACJ,SAAS,ECDU,OAAO;EDE1B,WAAW,ECDU,GAAG;;ADKzB,sBAAuB;EACtB,MAAM,EAAE,kBAAuC;EAC/C,cAAc,EAAE,kBAAkB;;AAGnC,EAAG;EACF,SAAS,ECRsB,OAAuB;;ADWvD,EAAG;EACF,SAAS,ECXsB,OAAuB;;ADcvD,EAAG;EACF,SAAS,ECdsB,OAAuB;;ADiBvD,EAAG;EACF,SAAS,ECjBsB,MAAuB;;ADoBvD,EAAG;EACF,SAAS,ECpBsB,MAAuB;;ADuBvD,EAAG;EACF,SAAS,ECvBsB,MAAuB;;AD2BvD,CAAE;EACD,MAAM,EAAE,QAAiB;;AAI1B,MAAO;EACN,UAAU,EC9BS,MAAwB;ED+B3C,aAAa,EC/BM,MAAwB;EDgC3C,0BAAO;IACN,UAAU,EAAE,CAAC;IACb,aAAa,EAAE,CAAC;;AAKlB,UAAW;EACV,MAAM,EAAE,QAAiB;EACzB,YAAY,EAAE,OAAmB;;AAGlC,IAAK;EACJ,OAAO,EAAE,KAAK;EACd,SAAS,EAAE,QAAuB;EAClC,WAAS;IACJ,OAAO,EAAE,aAAa;;AAK5B,GAAI;EACH,MAAM,EAAE,QAAiB;EACxB,OAAO,EDlEY,QAAQ;;ACqE7B,IAAK;EACJ,cAAc,EAAE,MAAM;;AAIvB,KAAM;EACL,SAAS,EAAE,QAAuB;;AAGnC,EAAG;EACF,WAAW,EAAE,IAAI;EACjB,YAAY,EAAE,IAAI;EAClB,UAAU,EAAE,IAAI;EAChB,MAAM,EAAE,QAAiB;;AEpF1B,QAAS;EACR,MAAM,EAAE,CAAC;EACT,OAAO,EHAa,QAAQ;EGC5B,MAAM,EAAE,YAAqB;;AAG9B;;MAEO;EACN,OAAO,EAAE,KAAK;;AAGf,KAAM;EACL,aAAa,EAAE,QAAmB;EAElC,oBAAiB;IAChB,OAAO,EAAE,GAAG;EAGb,UAAK;IACJ,OAAO,EAAE,IAAI;;AAIf,kVAAyD;ERfjD,kBAAoB,EAAE,YAAM;EAK5B,eAAiB,EAAE,YAAM;EAezB,UAAY,EAAE,YAAM;EQH3B,aAAa,ECzBS,SAAM;ED0B5B,aAAa,EAAE,OAAmB;EAClC,OAAO,EAAE,iBAA2C;EACpD,KAAK,EAAE,IAAI;EAEX,kbAAQ;IACP,OAAO,EAAE,IAAI;;AAIf,QAAS;EACR,MAAM,EAAE,QAAQ;;AAGjB,2CAA4C;EAC3C,OAAO,EAAE,MAAM;EACf,YAAY,EAAE,QAAmB;;AAGlC,kBAAmB;EAClB,KAAK,EAAE,IAAI;;AAGZ,MAAO;EACN,KAAK,EAAE,IAAI;EACX,SAAS,EAAE,IAAI;EACf,aAAa,EDvCM,MAAwB;;AC0C5C;oBACqB;EACpB,MAAM,EAAE,OAAO;EACf,WAAW,EAAE,IAAI;EACjB,cAAc,EAAE,MAAM;EACtB,WAAW,EAAE,MAAM;EACnB,MAAM,EAAE,OAAO", +"sources": ["../scss/nucleus/_core.scss","../scss/vendor/bourbon/addons/_prefixer.scss","../scss/nucleus/_flex.scss","../scss/configuration/nucleus/_breakpoints.scss","../scss/nucleus/mixins/_breakpoints.scss","../scss/vendor/bourbon/css3/_flex-box.scss","../scss/configuration/nucleus/_layout.scss","../scss/nucleus/_typography.scss","../scss/configuration/nucleus/_typography.scss","../scss/nucleus/_forms.scss","../scss/configuration/nucleus/_core.scss"], +"names": [], +"file": "nucleus.css" +} \ No newline at end of file diff --git a/netbox/project-static/select2-4.0.12/docs/themes/learn2/css-compiled/theme.css b/netbox/project-static/select2-4.0.12/docs/themes/learn2/css-compiled/theme.css new file mode 100644 index 000000000..2965e5fd4 --- /dev/null +++ b/netbox/project-static/select2-4.0.12/docs/themes/learn2/css-compiled/theme.css @@ -0,0 +1,940 @@ +@charset "UTF-8"; +@import url(//fonts.googleapis.com/css?family=Montserrat:400|Muli:300,400|Inconsolata); +#top-github-link, #body #breadcrumbs { + position: relative; + top: 50%; + -webkit-transform: translateY(-50%); + -moz-transform: translateY(-50%); + -o-transform: translateY(-50%); + -ms-transform: translateY(-50%); + transform: translateY(-50%); } + +.button, .button-secondary { + display: inline-block; + padding: 7px 12px; } + .button:active, .button-secondary:active { + margin: 2px 0 -2px 0; } + +body { + background: #fff; + color: #555; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + +a { + color: #1694CA; } + a:hover { + color: #0e6185; } + +pre { + position: relative; } + +.bg { + background: #fff; + border: 1px solid #eaeaea; } + +b, strong, label, th { + font-weight: 600; } + +.default-animation, #header #logo-svg, #header #logo-svg path, #sidebar, #sidebar ul, #body, #body .padding, #body .nav { + -webkit-transition: all 0.5s ease; + -moz-transition: all 0.5s ease; + transition: all 0.5s ease; } + +fieldset { + border: 1px solid #ddd; } + +textarea, input[type="email"], input[type="number"], input[type="password"], input[type="search"], input[type="tel"], input[type="text"], input[type="url"], input[type="color"], input[type="date"], input[type="datetime"], input[type="datetime-local"], input[type="month"], input[type="time"], input[type="week"], select[multiple=multiple] { + background-color: white; + border: 1px solid #ddd; + box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.06); } + textarea:hover, input[type="email"]:hover, input[type="number"]:hover, input[type="password"]:hover, input[type="search"]:hover, input[type="tel"]:hover, input[type="text"]:hover, input[type="url"]:hover, input[type="color"]:hover, input[type="date"]:hover, input[type="datetime"]:hover, input[type="datetime-local"]:hover, input[type="month"]:hover, input[type="time"]:hover, input[type="week"]:hover, select[multiple=multiple]:hover { + border-color: #c4c4c4; } + textarea:focus, input[type="email"]:focus, input[type="number"]:focus, input[type="password"]:focus, input[type="search"]:focus, input[type="tel"]:focus, input[type="text"]:focus, input[type="url"]:focus, input[type="color"]:focus, input[type="date"]:focus, input[type="datetime"]:focus, input[type="datetime-local"]:focus, input[type="month"]:focus, input[type="time"]:focus, input[type="week"]:focus, select[multiple=multiple]:focus { + border-color: #1694CA; + box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.06), 0 0 5px rgba(19, 131, 179, 0.7); } + +#header { + background: #1694CA; + color: #fff; + text-align: center; + padding: 1rem; } + #header a { + display: inline-block; } + #header #logo-svg { + width: 8rem; + height: 2rem; } + #header #logo-svg path { + fill: #fff; } + +.searchbox { + margin-top: 0.5rem; + position: relative; + border: 1px solid #19a5e1; + background: #1383b3; + border-radius: 4px; } + .searchbox label { + color: rgba(255, 255, 255, 0.8); + position: absolute; + left: 10px; + top: 3px; } + .searchbox span { + color: rgba(255, 255, 255, 0.6); + position: absolute; + right: 10px; + top: 3px; + cursor: pointer; } + .searchbox span:hover { + color: rgba(255, 255, 255, 0.9); } + .searchbox input { + display: inline-block; + color: #fff; + width: 100%; + height: 30px; + background: transparent; + border: 0; + padding: 0 25px 0 30px; + margin: 0; + font-weight: 400; } + .searchbox input::-webkit-input-placeholder { + color: rgba(255, 255, 255, 0.6); } + .searchbox input::-moz-placeholder { + color: rgba(255, 255, 255, 0.6); } + .searchbox input:-moz-placeholder { + color: rgba(255, 255, 255, 0.6); } + .searchbox input:-ms-input-placeholder { + color: rgba(255, 255, 255, 0.6); } + +#sidebar-toggle { + display: none; } + @media only all and (max-width: 47.938em) { + #sidebar-toggle { + display: inline-block; } } + +#sidebar { + background-color: #38424D; + position: fixed; + top: 0; + width: 300px; + bottom: 0; + left: 0; + font-weight: 500; + font-size: 15px; } + #sidebar a { + color: #bbbbbb; } + #sidebar a:hover { + color: #d5d5d5; } + #sidebar a.subtitle { + color: rgba(187, 187, 187, 0.6); } + #sidebar hr { + border-bottom: 1px solid #323a44; } + #sidebar a.padding { + padding: 0 1rem; } + #sidebar h5 { + margin: 2rem 0 0; + position: relative; + line-height: 2; } + #sidebar h5 a { + display: block; + margin-left: 0; + margin-right: 0; + padding-left: 1rem; + padding-right: 1rem; } + #sidebar h5 i { + color: rgba(187, 187, 187, 0.6); + position: absolute; + right: 0.6rem; + top: 0.7rem; + font-size: 80%; } + #sidebar h5.parent a { + background: #293038; + color: #c8c8c8 !important; } + #sidebar h5.active a { + background: #fff; + color: #555 !important; } + #sidebar h5.active i { + color: #555 !important; } + #sidebar h5 + ul.topics { + display: none; + margin-top: 0; } + #sidebar h5.parent + ul.topics, #sidebar h5.active + ul.topics { + display: block; } + #sidebar ul { + list-style: none; + padding: 0; + margin: 0; } + #sidebar ul.searched a { + color: #888888; } + #sidebar ul.searched .search-match a { + color: #d5d5d5; } + #sidebar ul.searched .search-match a:hover { + color: #eeeeee; } + #sidebar ul.topics { + margin: 0 1rem; } + #sidebar ul.topics.searched ul { + display: block; } + #sidebar ul.topics ul { + display: none; + padding-bottom: 1rem; } + #sidebar ul.topics ul ul { + padding-bottom: 0; } + #sidebar ul.topics li.parent ul, #sidebar ul.topics > li.active ul { + display: block; } + #sidebar ul.topics > li > a { + line-height: 2rem; + font-size: 1.1rem; } + #sidebar ul.topics > li > a b { + opacity: 0.5; + font-weight: normal; } + #sidebar ul.topics > li > a .fa { + margin-top: 9px; } + #sidebar ul.topics > li.parent, #sidebar ul.topics > li.active { + background: #2d353e; + margin-left: -1rem; + margin-right: -1rem; + padding-left: 1rem; + padding-right: 1rem; } + #sidebar ul li.active > a { + background: #fff; + color: #555 !important; + margin-left: -1rem; + margin-right: -1rem; + padding-left: 1rem; + padding-right: 1rem; } + #sidebar ul li { + padding: 0; } + #sidebar ul li.visited + span { + margin-right: 16px; } + #sidebar ul li a { + display: block; + padding: 2px 0; } + #sidebar ul li a span { + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + display: block; } + #sidebar ul li > a { + padding: 4px 0; } + #sidebar ul li .fa { + display: none; + float: right; + font-size: 13px; + min-width: 16px; + margin: 4px 0 0 0; + text-align: right; } + #sidebar ul li.visited > a .read-icon { + color: #1694CA; + display: inline; } + #sidebar ul li li { + padding-left: 1rem; + text-indent: 0.2rem; } + +#main { + background: #f7f7f7; + margin: 0 0 1.563rem 0; } + +#body { + position: relative; + margin-left: 300px; + min-height: 100%; } + #body img, #body .video-container { + margin: 3rem auto; + display: block; + text-align: center; } + #body img.border, #body .video-container.border { + border: 2px solid #e6e6e6 !important; + padding: 2px; } + #body img.shadow, #body .video-container.shadow { + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); } + #body .bordered { + border: 1px solid #ccc; } + #body .padding { + padding: 3rem 6rem; } + @media only all and (max-width: 59.938em) { + #body .padding { + position: static; + padding: 15px 3rem; } } + @media only all and (max-width: 47.938em) { + #body .padding { + padding: 5px 1rem; } } + #body h1 + hr { + margin-top: -1.7rem; + margin-bottom: 3rem; } + @media only all and (max-width: 59.938em) { + #body #navigation { + position: static; + margin-right: 0 !important; + width: 100%; + display: table; } } + #body .nav { + position: fixed; + top: 0; + bottom: 0; + width: 4rem; + font-size: 50px; + height: 100%; + cursor: pointer; + display: table; + text-align: center; } + #body .nav > i { + display: table-cell; + vertical-align: middle; + text-align: center; } + @media only all and (max-width: 59.938em) { + #body .nav { + display: table-cell; + position: static; + top: auto; + width: 50%; + text-align: center; + height: 100px; + line-height: 100px; + padding-top: 0; } + #body .nav > i { + display: inline-block; } } + #body .nav:hover { + background: #F6F6F6; } + #body .nav.nav-pref { + left: 0; } + #body .nav.nav-next { + right: 0; } + +#body-inner { + margin-bottom: 5rem; } + +#chapter { + display: flex; + align-items: center; + justify-content: center; + height: 100%; + padding: 2rem 0; } + #chapter #body-inner { + padding-bottom: 3rem; + max-width: 80%; } + #chapter h3 { + font-family: "Muli", "Helvetica", "Tahoma", "Geneva", "Arial", sans-serif; + font-weight: 400; + text-align: center; } + #chapter h1 { + font-size: 5rem; + border-bottom: 4px solid #F0F2F4; } + #chapter p { + text-align: center; + font-size: 1.2rem; } + +#footer { + padding: 3rem 1rem; + color: #a2a2a2; + font-size: 13px; } + #footer p { + margin: 0; } + +body { + font-family: "Muli", "Helvetica", "Tahoma", "Geneva", "Arial", sans-serif; + letter-spacing: -0.03rem; + font-weight: 400; } + +h1, h2, h3, h4, h5, h6 { + font-family: "Montserrat", "Helvetica", "Tahoma", "Geneva", "Arial", sans-serif; + font-weight: 400; + text-rendering: optimizeLegibility; + line-height: 150%; + letter-spacing: -0px; } + +h1 { + text-align: center; + letter-spacing: -3px; } + +h2 { + letter-spacing: -2px; } + +h3 { + letter-spacing: -1px; } + +blockquote { + border-left: 10px solid #F0F2F4; } + blockquote p { + font-size: 1.1rem; + color: #999; } + blockquote cite { + display: block; + text-align: right; + color: #666; + font-size: 1.2rem; } + +blockquote { + position: relative; } + +blockquote blockquote { + position: static; } + +blockquote > blockquote > blockquote { + margin: 0; } + blockquote > blockquote > blockquote p { + padding: 15px; + display: block; + font-size: 1rem; + margin-top: 0rem; + margin-bottom: 0rem; + color: #666; } + blockquote > blockquote > blockquote p:first-child:before { + position: absolute; + top: 2px; + color: #fff; + font-family: FontAwesome; + content: ''; + left: 10px; } + blockquote > blockquote > blockquote p:first-child:after { + position: absolute; + top: 2px; + color: #fff; + left: 2rem; + font-weight: bold; + content: 'Info'; } + blockquote > blockquote > blockquote > p { + margin-left: -71px; + border-top: 30px solid #F0B37E; + background: #FFF2DB; } + blockquote > blockquote > blockquote > blockquote > p { + margin-left: -94px; + border-top: 30px solid rgba(217, 83, 79, 0.8); + background: #FAE2E2; } + blockquote > blockquote > blockquote > blockquote > p:first-child:after { + content: 'Warning'; } + blockquote > blockquote > blockquote > blockquote > blockquote > p { + margin-left: -118px; + border-top: 30px solid #6AB0DE; + background: #E7F2FA; } + blockquote > blockquote > blockquote > blockquote > blockquote > p:first-child:after { + content: 'Note'; } + blockquote > blockquote > blockquote > blockquote > blockquote > blockquote > p { + margin-left: -142px; + border-top: 30px solid rgba(92, 184, 92, 0.8); + background: #E6F9E6; } + blockquote > blockquote > blockquote > blockquote > blockquote > blockquote > p:first-child:after { + content: 'Tip'; } + +code, +kbd, +pre, +samp { + font-family: "Inconsolata", monospace; } + +code { + background: #f9f2f4; + color: #9c1d3d; + padding: .2rem .4rem; + border-radius: 3px; } + +pre { + padding: 1rem; + margin: 2rem 0; + background: #f6f6f6; + border: 1px solid #ddd; + border-radius: 2px; + line-height: 1.15; + font-size: 1rem; } + pre code { + color: #237794; + background: inherit; + font-size: 1rem; } + +hr { + border-bottom: 4px solid #F0F2F4; } + +.page-title { + margin-top: -25px; + padding: 25px; + float: left; + clear: both; + background: #1694CA; + color: #fff; } + +#body a.anchor-link { + color: #ccc; } +#body a.anchor-link:hover { + color: #1694CA; } + +.scrollbar-inner > .scroll-element .scroll-element_track { + background-color: rgba(255, 255, 255, 0.3); } + +.scrollbar-inner > .scroll-element .scroll-bar { + background-color: #b5d1eb; } + +.scrollbar-inner > .scroll-element:hover .scroll-bar { + background-color: #ccc; } + +.scrollbar-inner > .scroll-element.scroll-draggable .scroll-bar { + background-color: #ccc; } + +table { + border: 1px solid #eaeaea; + table-layout: auto; } + +th { + background: #f7f7f7; + padding: 0.5rem; } + +td { + padding: 0.5rem; + border: 1px solid #eaeaea; } + +.button { + background: #1694CA; + color: #fff; + box-shadow: 0 3px 0 #1380ae; } + .button:hover { + background: #1380ae; + box-shadow: 0 3px 0 #106c93; + color: #fff; } + .button:active { + box-shadow: 0 1px 0 #106c93; } + +.button-secondary { + background: #F8B450; + color: #fff; + box-shadow: 0 3px 0 #f7a733; } + .button-secondary:hover { + background: #f7a733; + box-shadow: 0 3px 0 #f69b15; + color: #fff; } + .button-secondary:active { + box-shadow: 0 1px 0 #f69b15; } + +.bullets { + margin: 1.7rem 0; + margin-left: -0.85rem; + margin-right: -0.85rem; + overflow: auto; } + +.bullet { + float: left; + padding: 0 0.85rem; } + +.two-column-bullet { + width: 50%; } + @media only all and (max-width: 47.938em) { + .two-column-bullet { + width: 100%; } } + +.three-column-bullet { + width: 33.33333%; } + @media only all and (max-width: 47.938em) { + .three-column-bullet { + width: 100%; } } + +.four-column-bullet { + width: 25%; } + @media only all and (max-width: 47.938em) { + .four-column-bullet { + width: 100%; } } + +.bullet-icon { + float: left; + background: #1694CA; + padding: 0.875rem; + width: 3.5rem; + height: 3.5rem; + border-radius: 50%; + color: #fff; + font-size: 1.75rem; + text-align: center; } + +.bullet-icon-1 { + background: #1694CA; } + +.bullet-icon-2 { + background: #16cac4; } + +.bullet-icon-3 { + background: #b2ca16; } + +.bullet-content { + margin-left: 4.55rem; } + +.tooltipped { + position: relative; } + +.tooltipped:after { + position: absolute; + z-index: 1000000; + display: none; + padding: 5px 8px; + font: normal normal 11px/1.5 "Muli", "Helvetica", "Tahoma", "Geneva", "Arial", sans-serif; + color: #fff; + text-align: center; + text-decoration: none; + text-shadow: none; + text-transform: none; + letter-spacing: normal; + word-wrap: break-word; + white-space: pre; + pointer-events: none; + content: attr(aria-label); + background: rgba(0, 0, 0, 0.8); + border-radius: 3px; + -webkit-font-smoothing: subpixel-antialiased; } + +.tooltipped:before { + position: absolute; + z-index: 1000001; + display: none; + width: 0; + height: 0; + color: rgba(0, 0, 0, 0.8); + pointer-events: none; + content: ""; + border: 5px solid transparent; } + +.tooltipped:hover:before, .tooltipped:hover:after, +.tooltipped:active:before, +.tooltipped:active:after, +.tooltipped:focus:before, +.tooltipped:focus:after { + display: inline-block; + text-decoration: none; } + +.tooltipped-s:after, +.tooltipped-se:after, +.tooltipped-sw:after { + top: 100%; + right: 50%; + margin-top: 5px; } +.tooltipped-s:before, +.tooltipped-se:before, +.tooltipped-sw:before { + top: auto; + right: 50%; + bottom: -5px; + margin-right: -5px; + border-bottom-color: rgba(0, 0, 0, 0.8); } + +.tooltipped-se:after { + right: auto; + left: 50%; + margin-left: -15px; } + +.tooltipped-sw:after { + margin-right: -15px; } + +.tooltipped-n:after, +.tooltipped-ne:after, +.tooltipped-nw:after { + right: 50%; + bottom: 100%; + margin-bottom: 5px; } +.tooltipped-n:before, +.tooltipped-ne:before, +.tooltipped-nw:before { + top: -5px; + right: 50%; + bottom: auto; + margin-right: -5px; + border-top-color: rgba(0, 0, 0, 0.8); } + +.tooltipped-ne:after { + right: auto; + left: 50%; + margin-left: -15px; } + +.tooltipped-nw:after { + margin-right: -15px; } + +.tooltipped-s:after, +.tooltipped-n:after { + transform: translateX(50%); } + +.tooltipped-w:after { + right: 100%; + bottom: 50%; + margin-right: 5px; + transform: translateY(50%); } +.tooltipped-w:before { + top: 50%; + bottom: 50%; + left: -5px; + margin-top: -5px; + border-left-color: rgba(0, 0, 0, 0.8); } + +.tooltipped-e:after { + bottom: 50%; + left: 100%; + margin-left: 5px; + transform: translateY(50%); } +.tooltipped-e:before { + top: 50%; + right: -5px; + bottom: 50%; + margin-top: -5px; + border-right-color: rgba(0, 0, 0, 0.8); } + +/*************** SCROLLBAR BASE CSS ***************/ +.highlightable { + padding: 25px 0 15px; } + +.scroll-wrapper { + overflow: hidden !important; + padding: 0 !important; + position: relative; } + +.scroll-wrapper > .scroll-content { + border: none !important; + box-sizing: content-box !important; + height: auto; + left: 0; + margin: 0; + max-height: none; + max-width: none !important; + overflow: scroll !important; + padding: 0; + position: relative !important; + top: 0; + width: auto !important; } + +.scroll-wrapper > .scroll-content::-webkit-scrollbar { + height: 0; + width: 0; } + +.scroll-element { + display: none; } + +.scroll-element, .scroll-element div { + box-sizing: content-box; } + +.scroll-element.scroll-x.scroll-scrollx_visible, +.scroll-element.scroll-y.scroll-scrolly_visible { + display: block; } + +.scroll-element .scroll-bar, +.scroll-element .scroll-arrow { + cursor: default; } + +.scroll-textarea > .scroll-content { + overflow: hidden !important; } + +.scroll-textarea > .scroll-content > textarea { + border: none !important; + box-sizing: border-box; + height: 100% !important; + margin: 0; + max-height: none !important; + max-width: none !important; + overflow: scroll !important; + outline: none; + padding: 2px; + position: relative !important; + top: 0; + width: 100% !important; } + +.scroll-textarea > .scroll-content > textarea::-webkit-scrollbar { + height: 0; + width: 0; } + +/*************** SIMPLE INNER SCROLLBAR ***************/ +.scrollbar-inner > .scroll-element, +.scrollbar-inner > .scroll-element div { + border: none; + margin: 0; + padding: 0; + position: absolute; + z-index: 10; } + +.scrollbar-inner > .scroll-element div { + display: block; + height: 100%; + left: 0; + top: 0; + width: 100%; } + +.scrollbar-inner > .scroll-element.scroll-x { + bottom: 2px; + height: 8px; + left: 0; + width: 100%; } + +.scrollbar-inner > .scroll-element.scroll-y { + height: 100%; + right: 2px; + top: 0; + width: 8px; } + +.scrollbar-inner > .scroll-element .scroll-element_outer { + overflow: hidden; } + +.scrollbar-inner > .scroll-element .scroll-element_outer, +.scrollbar-inner > .scroll-element .scroll-element_track, +.scrollbar-inner > .scroll-element .scroll-bar { + -webkit-border-radius: 8px; + -moz-border-radius: 8px; + border-radius: 8px; } + +.scrollbar-inner > .scroll-element .scroll-element_track, +.scrollbar-inner > .scroll-element .scroll-bar { + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=30)"; + filter: alpha(opacity=30); + opacity: 0.3; } + +/* update scrollbar offset if both scrolls are visible */ +.scrollbar-inner > .scroll-element.scroll-x.scroll-scrolly_visible .scroll-element_track { + left: -12px; } + +.scrollbar-inner > .scroll-element.scroll-y.scroll-scrollx_visible .scroll-element_track { + top: -12px; } + +.scrollbar-inner > .scroll-element.scroll-x.scroll-scrolly_visible .scroll-element_size { + left: -12px; } + +.scrollbar-inner > .scroll-element.scroll-y.scroll-scrollx_visible .scroll-element_size { + top: -12px; } + +.lightbox-active #body { + overflow: visible; } + .lightbox-active #body .padding { + overflow: visible; } + +#github-contrib i { + vertical-align: middle; } + +.featherlight img { + margin: 0 !important; } + +.lifecycle #body-inner ul { + list-style: none; + margin: 0; + padding: 2rem 0 0; + position: relative; } +.lifecycle #body-inner ol { + margin: 1rem 0 1rem 0; + padding: 2rem; + position: relative; } + .lifecycle #body-inner ol li { + margin-left: 1rem; } + .lifecycle #body-inner ol strong, .lifecycle #body-inner ol label, .lifecycle #body-inner ol th { + text-decoration: underline; } + .lifecycle #body-inner ol ol { + margin-left: -1rem; } +.lifecycle #body-inner h3[class*='level'] { + font-size: 20px; + position: absolute; + margin: 0; + padding: 4px 10px; + right: 0; + z-index: 1000; + color: #fff; + background: #1ABC9C; } +.lifecycle #body-inner ol h3 { + margin-top: 1rem !important; + right: 2rem !important; } +.lifecycle #body-inner .level-1 + ol { + background: #f6fefc; + border: 4px solid #1ABC9C; + color: #16A085; } + .lifecycle #body-inner .level-1 + ol h3 { + background: #2ECC71; } +.lifecycle #body-inner .level-2 + ol { + background: #f7fdf9; + border: 4px solid #2ECC71; + color: #27AE60; } + .lifecycle #body-inner .level-2 + ol h3 { + background: #3498DB; } +.lifecycle #body-inner .level-3 + ol { + background: #f3f9fd; + border: 4px solid #3498DB; + color: #2980B9; } + .lifecycle #body-inner .level-3 + ol h3 { + background: #34495E; } +.lifecycle #body-inner .level-4 + ol { + background: #e4eaf0; + border: 4px solid #34495E; + color: #2C3E50; } + .lifecycle #body-inner .level-4 + ol h3 { + background: #34495E; } + +#top-bar { + background: #F6F6F6; + border-radius: 2px; + margin: 0rem -1rem 2rem; + padding: 0 1rem; + height: 0; + min-height: 3rem; } + +#top-github-link { + position: relative; + z-index: 1; + float: right; + display: block; } + +#body #breadcrumbs { + height: auto; + display: block; + margin-bottom: 0; + padding-left: 0; + line-height: 1.4; } + #body #breadcrumbs span { + padding: 0 0.1rem; } + +@media only all and (max-width: 59.938em) { + #sidebar { + width: 230px; } + + #body { + margin-left: 230px; } } +@media only all and (max-width: 47.938em) { + #sidebar { + width: 230px; + left: -230px; } + + #body { + margin-left: 0; + width: 100%; } + + .sidebar-hidden { + overflow: hidden; } + .sidebar-hidden #sidebar { + left: 0; } + .sidebar-hidden #body { + margin-left: 230px; + overflow: hidden; } + .sidebar-hidden #overlay { + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; + z-index: 10; + background: rgba(255, 255, 255, 0.5); + cursor: pointer; } } +.copy-to-clipboard { + background-image: url(../images/clippy.svg); + background-position: 50% 50%; + background-size: 16px 16px; + background-repeat: no-repeat; + width: 27px; + height: 1.45rem; + top: -1px; + display: inline-block; + vertical-align: middle; + position: relative; + color: #3c3c3c; + background-color: #f9f2f4; + margin-left: -.2rem; + cursor: pointer; + border-radius: 0 2px 2px 0; } + .copy-to-clipboard:hover { + background-color: #f1e1e5; } + pre .copy-to-clipboard { + position: absolute; + right: 4px; + top: 4px; + background-color: #eee; + border-color: #ddd; + border-radius: 2px; } + pre .copy-to-clipboard:hover { + background-color: #d9d9d9; } + +.parent-element { + -webkit-transform-style: preserve-3d; + -moz-transform-style: preserve-3d; + transform-style: preserve-3d; } + +/*# sourceMappingURL=theme.css.map */ diff --git a/netbox/project-static/select2-4.0.12/docs/themes/learn2/css-compiled/theme.css.map b/netbox/project-static/select2-4.0.12/docs/themes/learn2/css-compiled/theme.css.map new file mode 100644 index 000000000..b73450315 --- /dev/null +++ b/netbox/project-static/select2-4.0.12/docs/themes/learn2/css-compiled/theme.css.map @@ -0,0 +1,7 @@ +{ +"version": 3, +"mappings": ";AACQ,sFAA8E;ACStF,oCAAgB;EACf,QAAQ,EAAE,QAAQ;EAClB,GAAG,EAAE,GAAG;EACR,iBAAiB,EAAE,gBAAgB;EACnC,cAAc,EAAE,gBAAgB;EAChC,YAAY,EAAE,gBAAgB;EAC9B,aAAa,EAAE,gBAAgB;EAC/B,SAAS,EAAE,gBAAgB;;ACjB5B,0BAAQ;EACP,OAAO,EAAE,YAAY;EACrB,OAAO,EAAE,QAAQ;EACjB,wCAAS;IACR,MAAM,EAAE,YAAY;;ACJtB,IAAK;EACJ,UAAU,ECiBI,IAAI;EDhBlB,KAAK,ECwCY,IAAU;EDvCxB,sBAAsB,EAAE,WAAW;EACnC,uBAAuB,EAAE,SAAS;;AAGtC,CAAE;EACD,KAAK,EEPM,OAAY;EFQvB,OAAQ;IACP,KAAK,EAAE,OAAyB;;AAIlC,GAAI;EACH,QAAQ,EAAE,QAAQ;;AAGnB,GAAI;EACH,UAAU,EAAE,IAAI;EAChB,MAAM,EAAE,iBAAsB;;AAG/B,oBAAU;EACN,WAAW,EEVI,GAAG;;AFatB,uHAAmB;EGlBX,kBAAoB,EAAE,aAAM;EAK5B,eAAiB,EAAE,aAAM;EAezB,UAAY,EAAE,aAAM;;AC7B5B,QAAS;EACR,MAAM,EAAE,cAA4B;;AAGrC,kVAAyD;EACxD,gBAAgB,EAAE,KAAK;EACvB,MAAM,EAAE,cAA4B;EACpC,UAAU,EHOW,mCAAqC;EGL1D,kbAAQ;IACP,YAAY,EHAc,OAA8B;EGGzD,kbAAQ;IACP,YAAY,EFbF,OAAY;IEctB,UAAU,EHAc,oEAAwE;;AIflG,OAAQ;EACJ,UAAU,EHAF,OAAY;EGCpB,KAAK,EJEK,IAAI;EIDd,UAAU,EAAE,MAAM;EAElB,OAAO,EAAE,IAAI;EAEb,SAAE;IACE,OAAO,EAAE,YAAY;EAGzB,iBAAU;IAEN,KAAK,EHQA,IAAI;IGPT,MAAM,EHQA,IAAI;IGNV,sBAAK;MAED,IAAI,EJdF,IAAI;;AImBlB,UAAW;EACP,UAAU,EAAE,MAAM;EAClB,QAAQ,EAAE,QAAQ;EAElB,MAAM,EAAE,iBAAiC;EACzC,UAAU,EAAE,OAAqB;EACjC,aAAa,EAAE,GAAG;EAElB,gBAAM;IACF,KAAK,EAAE,wBAAiB;IACxB,QAAQ,EAAE,QAAQ;IAClB,IAAI,EAAE,IAAI;IACV,GAAG,EAAE,GAAG;EAGZ,eAAK;IACD,KAAK,EAAE,wBAAiB;IACxB,QAAQ,EAAE,QAAQ;IAClB,KAAK,EAAE,IAAI;IACX,GAAG,EAAE,GAAG;IACR,MAAM,EAAE,OAAO;IAEf,qBAAQ;MACJ,KAAK,EAAE,wBAAiB;EAIhC,gBAAM;IACF,OAAO,EAAE,YAAY;IACrB,KAAK,EJhDC,IAAI;IIiDV,KAAK,EAAE,IAAI;IACX,MAAM,EAAE,IAAI;IACZ,UAAU,EAAE,WAAW;IACvB,MAAM,EAAE,CAAC;IACT,OAAO,EAAE,aAAa;IACtB,MAAM,EAAE,CAAC;IACT,WAAW,EH3CG,GAAG;IIbrB,2CAA8B;MD2DtB,KAAK,EAAE,wBAAiB;IC3DhC,kCAA8B;MD2DtB,KAAK,EAAE,wBAAiB;IC3DhC,iCAA8B;MD2DtB,KAAK,EAAE,wBAAiB;IC3DhC,sCAA8B;MD2DtB,KAAK,EAAE,wBAAiB;;AE9DpC,eAAgB;EACZ,OAAO,EAAE,IAAI;ECoBf,yCAAiE;IDrBnE,eAAgB;MAIP,OAAO,EAAE,YAAY;;AAK9B,QAAS;EAEL,gBAAgB,ELPP,OAAO;EKQhB,QAAQ,EAAE,KAAK;EACf,GAAG,EAAE,CAAC;EACN,KAAK,ELZO,KAAK;EKajB,MAAM,EAAE,CAAC;EACT,IAAI,EAAE,CAAC;EACP,WAAW,ELFM,GAAG;EKGpB,SAAS,EAAE,IAAI;EAEf,UAAE;IACE,KAAK,ELfE,OAAO;IKgBd,gBAAQ;MACJ,KAAK,EAAE,OAA2B;IAEtC,mBAAW;MACP,KAAK,EAAE,wBAAwB;EAIvC,WAAG;IACC,aAAa,EAAE,iBAAiC;EAGpD,kBAAU;IACN,OAAO,EAAE,MAAM;EAGnB,WAAG;IACC,MAAM,EAAE,QAAQ;IAChB,QAAQ,EAAE,QAAQ;IAClB,WAAW,EAAE,CAAC;IAEd,aAAE;MACE,OAAO,EAAE,KAAK;MACd,WAAW,EAAE,CAAC;MACd,YAAY,EAAE,CAAC;MACf,YAAY,EAAE,IAAI;MAClB,aAAa,EAAE,IAAI;IAGvB,aAAE;MACE,KAAK,EAAE,wBAAwB;MAC/B,QAAQ,EAAE,QAAQ;MAClB,KAAK,EAAE,MAAM;MACb,GAAG,EAAE,MAAM;MACX,SAAS,EAAE,GAAG;IAId,oBAAE;MACE,UAAU,EAAE,OAAuB;MACnC,KAAK,EAAE,kBAAqC;IAKhD,oBAAE;MACE,UAAU,ENhEZ,IAAI;MMiEF,KAAK,EAAE,eAAqB;IAGhC,oBAAE;MACE,KAAK,EAAE,eAAqB;EAOxC,uBAAe;IACX,OAAO,EAAE,IAAI;IACb,UAAU,EAAE,CAAC;EAIb,8DAAY;IACR,OAAO,EAAE,KAAK;EAKtB,WAAG;IAEC,UAAU,EAAE,IAAI;IAChB,OAAO,EAAE,CAAC;IACV,MAAM,EAAE,CAAC;IAGL,sBAAE;MACE,KAAK,EAAE,OAA0B;IAIjC,oCAAE;MACE,KAAK,EAAE,OAA2B;MAClC,0CAAQ;QACJ,KAAK,EAAE,OAA2B;IAMlD,kBAAS;MACL,MAAM,EAAE,MAAM;MAGV,8BAAG;QACC,OAAO,EAAE,KAAK;MAItB,qBAAG;QACC,OAAO,EAAE,IAAI;QACb,cAAc,EAAE,IAAI;QAEpB,wBAAG;UACC,cAAc,EAAE,CAAC;MAIzB,kEAA6B;QACzB,OAAO,EAAE,KAAK;MAId,2BAAI;QACA,WAAW,EAAE,IAAI;QACjB,SAAS,EAAE,MAAM;QAEjB,6BAAE;UACE,OAAO,EAAE,GAAG;UACZ,WAAW,EAAE,MAAM;QAGvB,+BAAI;UACA,UAAU,EAAE,GAAG;MAIvB,8DAAmB;QACf,UAAU,EAAE,OAAuB;QACnC,WAAW,EAAE,KAAK;QAClB,YAAY,EAAE,KAAK;QACnB,YAAY,EAAE,IAAI;QAClB,aAAa,EAAE,IAAI;IAK/B,yBAAc;MACV,UAAU,EN7JR,IAAI;MM8JN,KAAK,EAAE,eAAqB;MAC5B,WAAW,EAAE,KAAK;MAClB,YAAY,EAAE,KAAK;MACnB,YAAY,EAAE,IAAI;MAClB,aAAa,EAAE,IAAI;IAGvB,cAAG;MACC,OAAO,EAAE,CAAC;MACV,6BAAiB;QACb,YAAY,EAAE,IAAI;MAEtB,gBAAE;QACE,OAAO,EAAE,KAAK;QACd,OAAO,EAAE,KAAK;QACd,qBAAK;UACD,aAAa,EAAE,QAAQ;UACvB,QAAQ,EAAE,MAAM;UAChB,WAAW,EAAE,MAAM;UACnB,OAAO,EAAE,KAAK;MAGtB,kBAAI;QACA,OAAO,EAAE,KAAK;MAGlB,kBAAI;QACA,OAAO,EAAE,IAAI;QACb,KAAK,EAAE,KAAK;QACZ,SAAS,EAAE,IAAI;QACf,SAAS,EAAE,IAAI;QACf,MAAM,EAAE,SAAS;QACjB,UAAU,EAAE,KAAK;MAIjB,qCAAe;QACX,KAAK,ELtMb,OAAY;QKuMJ,OAAO,EAAE,MAAM;MAIvB,iBAAG;QACC,YAAY,EAAE,IAAI;QAClB,WAAW,EAAE,MAAM;;AE9MnC,KAAM;EACL,UAAU,ERiCI,OAAO;EQhCrB,MAAM,EAAE,cAAwC;;AAGjD,KAAM;EAiBF,QAAQ,EAAE,QAAQ;EAClB,WAAW,EPrBC,KAAK;EOsBjB,UAAU,EAAE,IAAI;EAlBhB,iCAAsB;IAClB,MAAM,EAAE,SAAS;IACjB,OAAO,EAAE,KAAK;IACd,UAAU,EAAE,MAAM;IAElB,+CAAS;MACL,MAAM,EAAE,4BAA4B;MACpC,OAAO,EAAE,GAAG;IAGhB,+CAAS;MACL,UAAU,EAAE,8BAA8B;EASlD,eAAU;IACN,MAAM,EAAE,cAAc;EAG1B,cAAS;IAEL,OAAO,EAAE,SAA0B;IDRzC,yCAAkE;MCMhE,cAAS;QAKD,QAAQ,EAAE,MAAM;QAChB,OAAO,EAAE,SAA0B;IDf7C,yCAAiE;MCS/D,cAAS;QAUD,OAAO,EAAE,QAAQ;EAIzB,aAAQ;IACJ,UAAU,EAAE,OAAO;IACnB,aAAa,EAAE,IAAI;EDtBzB,yCAAkE;ICyBhE,iBAAY;MAGJ,QAAQ,EAAE,MAAM;MAChB,YAAY,EAAE,YAAY;MAC1B,KAAK,EAAE,IAAI;MACX,OAAO,EAAE,KAAK;EAItB,UAAK;IAED,QAAQ,EAAE,KAAK;IACf,GAAG,EAAE,CAAC;IACN,MAAM,EAAE,CAAC;IACT,KAAK,EP9CC,IAAI;IO+CV,SAAS,EAAE,IAAI;IACf,MAAM,EAAE,IAAI;IACZ,MAAM,EAAE,OAAO;IACf,OAAO,EAAE,KAAK;IACd,UAAU,EAAE,MAAM;IAClB,cAAI;MACA,OAAO,EAAE,UAAU;MACnB,cAAc,EAAE,MAAM;MACtB,UAAU,EAAE,MAAM;IDjD5B,yCAAkE;MCmChE,UAAK;QAkBG,OAAO,EAAE,UAAU;QACnB,QAAQ,EAAE,MAAM;QAChB,GAAG,EAAE,IAAI;QACT,KAAK,EAAE,GAAG;QACV,UAAU,EAAE,MAAM;QAClB,MAAM,EAAE,KAAK;QACb,WAAW,EAAE,KAAK;QAClB,WAAW,EAAE,CAAC;QACd,cAAI;UACA,OAAO,EAAE,YAAY;IAK7B,gBAAQ;MACJ,UAAU,EPpFV,OAAO;IOuFX,mBAAW;MACP,IAAI,EAAE,CAAC;IAGX,mBAAW;MACP,KAAK,EAAE,CAAC;;AAKpB,WAAY;EACR,aAAa,EAAE,IAAI;;AAIvB,QAAS;EAEL,OAAO,EAAE,IAAI;EACb,WAAW,EAAE,MAAM;EACnB,eAAe,EAAE,MAAM;EACvB,MAAM,EAAE,IAAI;EACZ,OAAO,EAAE,MAAM;EAEf,oBAAY;IACR,cAAc,EAAE,IAAI;IACpB,SAAS,EAAE,GAAG;EAGlB,WAAG;IACC,WAAW,EZzHa,4DAA4D;IY0HpF,WAAW,EP7GG,GAAG;IO8GjB,UAAU,EAAE,MAAM;EAGtB,WAAG;IACC,SAAS,EAAE,IAAI;IACf,aAAa,EAAE,iBAAqB;EAGxC,UAAE;IACE,UAAU,EAAE,MAAM;IAClB,SAAS,EAAE,MAAM;;AAIzB,OAAQ;EACJ,OAAO,EAAE,SAAS;EAClB,KAAK,EAAE,OAA0B;EACjC,SAAS,EAAE,IAAI;EAEf,SAAE;IACE,MAAM,EAAE,CAAC;;ACjJjB,IAAK;EACJ,WAAW,EbCoB,4DAA4D;EaAxF,cAAc,EAAE,QAAQ;EAC3B,WAAW,EAAE,GAAG;;AAIjB,sBAAuB;EACtB,WAAW,EbLoB,kEAAkE;EaMjG,WAAW,EAAE,GAAG;EAChB,cAAc,EAAE,kBAAkB;EAClC,WAAW,EAAE,IAAI;EACjB,cAAc,EAAE,IAAI;;AAGrB,EAAG;EACF,UAAU,EAAE,MAAM;EAClB,cAAc,EAAE,IAAI;;AAGrB,EAAG;EACF,cAAc,EAAE,IAAI;;AAGrB,EAAG;EACF,cAAc,EAAE,IAAI;;AAIrB,UAAW;EACV,WAAW,EAAE,kBAAsB;EACnC,YAAE;IACD,SAAS,EAAE,MAAM;IACjB,KAAK,EAAE,IAAI;EAEZ,eAAK;IACJ,OAAO,EAAE,KAAK;IACd,UAAU,EAAE,KAAK;IACjB,KAAK,EAAE,IAAI;IACX,SAAS,EAAE,MAAM;;AAKnB,UAAW;EACP,QAAQ,EAAE,QAAQ;;AAGtB,qBAAsB;EAClB,QAAQ,EAAE,MAAM;;AAGpB,oCAAqC;EAEpC,MAAM,EAAE,CAAC;EAET,sCAAE;IACD,OAAO,EAAE,IAAI;IACb,OAAO,EAAE,KAAK;IACd,SAAS,EAAE,IAAI;IACf,UAAU,EAAE,IAAI;IAChB,aAAa,EAAE,IAAI;IACb,KAAK,EAAE,IAAI;IAGP,yDAAS;MACL,QAAQ,EAAE,QAAQ;MAClB,GAAG,EAAE,GAAG;MACR,KAAK,ETjEP,IAAI;MSkEF,WAAW,EAAE,WAAW;MACxB,OAAO,EAAE,GAAG;MACZ,IAAI,EAAE,IAAI;IAEd,wDAAQ;MACJ,QAAQ,EAAE,QAAQ;MAClB,GAAG,EAAE,GAAG;MACR,KAAK,ETzEP,IAAI;MS0EF,IAAI,EAAE,IAAI;MACV,WAAW,EAAE,IAAI;MACjB,OAAO,EAAE,MAAM;EAK9B,wCAAI;IAEH,WAAW,EAAE,KAAK;IAClB,UAAU,EAAE,kBAAkB;IAC9B,UAAU,EAAE,OAAO;EAGpB,qDAAiB;IAEhB,WAAW,EAAE,KAAK;IAClB,UAAU,EAAE,iCAA6B;IACzC,UAAU,EAAE,OAAO;IACb,uEAAoB;MAChB,OAAO,EAAE,SAAS;EAI7B,kEAA8B;IAE7B,WAAW,EAAE,MAAM;IACnB,UAAU,EAAE,kBAAkB;IAC9B,UAAU,EAAE,OAAO;IACb,oFAAoB;MAChB,OAAO,EAAE,MAAM;EAI1B,+EAA2C;IAE1C,WAAW,EAAE,MAAM;IACnB,UAAU,EAAE,iCAA6B;IACzC,UAAU,EAAE,OAAO;IACb,iGAAoB;MAChB,OAAO,EAAE,KAAK;;AAO1B;;;IAGK;EACJ,WAAW,Eb5HoB,wBAAwB;;Aa+HxD,IAAK;EACJ,UAAU,ETnFI,OAAO;ESoFrB,KAAK,EAAE,OAAsB;EAC7B,OAAO,EAAE,WAAW;EACnB,aAAa,EAAE,GAAG;;AAGpB,GAAI;EACH,OAAO,EAAE,IAAI;EACb,MAAM,EAAE,MAAM;EACd,UAAU,ET1FG,OAAO;ES2FpB,MAAM,EAAE,cAA4B;EACpC,aAAa,EAAE,GAAG;EAClB,WAAW,EAAE,IAAI;EACjB,SAAS,EAAE,IAAI;EAEf,QAAK;IACJ,KAAK,ETlGS,OAAO;ISmGrB,UAAU,EAAE,OAAO;IACnB,SAAS,EAAE,IAAI;;AAKjB,EAAG;EACF,aAAa,EAAE,iBAAqB;;AAIrC,WAAY;EACX,UAAU,EAAE,KAAK;EACjB,OAAO,EAAE,IAAI;EACb,KAAK,EAAE,IAAI;EACX,KAAK,EAAE,IAAI;EACX,UAAU,ERrKC,OAAY;EQsKvB,KAAK,ETnKQ,IAAI;;ASwKd,mBAAc;EAAE,KAAK,EAAE,IAAI;AAC3B,yBAAoB;EAAE,KAAK,ER5KnB,OAAY;;AQgLxB,wDAAyD;EAAE,gBAAgB,EAAE,wBAAiB;;AAC9F,8CAA+C;EAAE,gBAAgB,EAAE,OAAoB;;AACvF,oDAAqD;EAAE,gBAAgB,EAAE,IAAI;;AAC7E,+DAAgE;EAAE,gBAAgB,EAAE,IAAI;;ACpLxF,KAAM;EACL,MAAM,EAAE,iBAAwC;EAC7C,YAAY,EAAE,IAAI;;AAGtB,EAAG;EAEF,UAAU,EAAE,OAA+B;EAC3C,OAAO,EAAE,MAAM;;AAGhB,EAAG;EACF,OAAO,EAAE,MAAM;EACf,MAAM,EAAE,iBAAwC;;ACbjD,OAAQ;EbSP,UAAU,EGRC,OAAY;EHSvB,KAAK,EENQ,IAAI;EFOjB,UAAU,EAAE,eAA0B;EACtC,aAAQ;IACP,UAAU,EAAE,OAAkB;IAC9B,UAAU,EAAE,eAA2B;IACvC,KAAK,EEXO,IAAI;EFajB,cAAS;IACR,UAAU,EAAE,eAA2B;;AabzC,iBAAkB;EbIjB,UAAU,EENS,OAAO;EFO1B,KAAK,EENQ,IAAI;EFOjB,UAAU,EAAE,eAA0B;EACtC,uBAAQ;IACP,UAAU,EAAE,OAAkB;IAC9B,UAAU,EAAE,eAA2B;IACvC,KAAK,EEXO,IAAI;EFajB,wBAAS;IACR,UAAU,EAAE,eAA2B;;AclBzC,QAAS;EACR,MAAM,EAAE,QAAiB;EACzB,WAAW,EAAE,QAAoB;EACjC,YAAY,EAAE,QAAoB;EAClC,QAAQ,EAAE,IAAI;;AAGf,OAAQ;EACP,KAAK,EAAE,IAAI;EACX,OAAO,EAAE,SAAqB;;AAG/B,kBAAmB;EfUlB,KAAK,EAAE,GAAsB;EUD5B,yCAAiE;IKTnE,kBAAmB;MfUlB,KAAK,EAAE,IAAsB;;AeH9B,oBAAqB;EfGpB,KAAK,EAAE,SAAsB;EUD5B,yCAAiE;IKFnE,oBAAqB;MfGpB,KAAK,EAAE,IAAsB;;AeI9B,mBAAoB;EfJnB,KAAK,EAAE,GAAsB;EUD5B,yCAAiE;IKKnE,mBAAoB;MfJnB,KAAK,EAAE,IAAsB;;AeW9B,YAAa;EACZ,KAAK,EAAE,IAAI;EACX,UAAU,EXlCC,OAAY;EWmCvB,OAAO,EAAE,QAAqB;EAC9B,KAAK,ECrCgB,MAAM;EDsC3B,MAAM,ECtCe,MAAM;EDuC3B,aAAa,EAAE,GAAG;EAClB,KAAK,EZpCQ,IAAI;EYqCjB,SAAS,EAAE,OAAqB;EAChC,UAAU,EAAE,MAAM;;AAGnB,cAAe;EACd,UAAU,EX7CC,OAAY;;AWgDxB,cAAe;EACd,UAAU,EC/Ca,OAA6B;;ADkDrD,cAAe;EACd,UAAU,EClDa,OAA8B;;ADqDtD,eAAgB;EACf,WAAW,EAAE,OAAuB;;AEtDrC,WAAY;EACV,QAAQ,EAAE,QAAQ;;AAIpB,iBAAkB;EAChB,QAAQ,EAAE,QAAQ;EAClB,OAAO,EAAE,OAAO;EAChB,OAAO,EAAE,IAAI;EACb,OAAO,EAAE,OAAO;EAChB,IAAI,EAAE,mFAA2C;EACjD,KAAK,EAbc,IAAI;EAcvB,UAAU,EAAE,MAAM;EAClB,eAAe,EAAE,IAAI;EACrB,WAAW,EAAE,IAAI;EACjB,cAAc,EAAE,IAAI;EACpB,cAAc,EAAE,MAAM;EACtB,SAAS,EAAE,UAAU;EACrB,WAAW,EAAE,GAAG;EAChB,cAAc,EAAE,IAAI;EACpB,OAAO,EAAE,gBAAgB;EACzB,UAAU,EAxBe,kBAAkB;EAyB3C,aAAa,EAAE,GAAG;EAClB,sBAAsB,EAAE,oBAAoB;;AAI9C,kBAAmB;EACjB,QAAQ,EAAE,QAAQ;EAClB,OAAO,EAAE,OAAO;EAChB,OAAO,EAAE,IAAI;EACb,KAAK,EAAE,CAAC;EACR,MAAM,EAAE,CAAC;EACT,KAAK,EApCoB,kBAAkB;EAqC3C,cAAc,EAAE,IAAI;EACpB,OAAO,EAAE,EAAE;EACX,MAAM,EAAE,qBAAqB;;AAO7B;;;;uBACQ;EACN,OAAO,EAAE,YAAY;EACrB,eAAe,EAAE,IAAI;;AAQvB;;oBAAQ;EACN,GAAG,EAAE,IAAI;EACT,KAAK,EAAE,GAAG;EACV,UAAU,EAAE,GAAG;AAGjB;;qBAAS;EACP,GAAG,EAAE,IAAI;EACT,KAAK,EAAE,GAAG;EACV,MAAM,EAAE,IAAI;EACZ,YAAY,EAAE,IAAI;EAClB,mBAAmB,EApEI,kBAAkB;;AAyE3C,oBAAQ;EACN,KAAK,EAAE,IAAI;EACX,IAAI,EAAE,GAAG;EACT,WAAW,EAAE,KAAK;;AAItB,oBAAqB;EACnB,YAAY,EAAE,KAAK;;AAOnB;;oBAAQ;EACN,KAAK,EAAE,GAAG;EACV,MAAM,EAAE,IAAI;EACZ,aAAa,EAAE,GAAG;AAGpB;;qBAAS;EACP,GAAG,EAAE,IAAI;EACT,KAAK,EAAE,GAAG;EACV,MAAM,EAAE,IAAI;EACZ,YAAY,EAAE,IAAI;EAClB,gBAAgB,EAnGO,kBAAkB;;AAwG3C,oBAAQ;EACN,KAAK,EAAE,IAAI;EACX,IAAI,EAAE,GAAG;EACT,WAAW,EAAE,KAAK;;AAItB,oBAAqB;EACnB,YAAY,EAAE,KAAK;;AAIrB;mBACoB;EAClB,SAAS,EAAE,eAAe;;AAK1B,mBAAQ;EACN,KAAK,EAAE,IAAI;EACX,MAAM,EAAE,GAAG;EACX,YAAY,EAAE,GAAG;EACjB,SAAS,EAAE,eAAe;AAG5B,oBAAS;EACP,GAAG,EAAE,GAAG;EACR,MAAM,EAAE,GAAG;EACX,IAAI,EAAE,IAAI;EACV,UAAU,EAAE,IAAI;EAChB,iBAAiB,EAvIM,kBAAkB;;AA6I3C,mBAAQ;EACN,MAAM,EAAE,GAAG;EACX,IAAI,EAAE,IAAI;EACV,WAAW,EAAE,GAAG;EAChB,SAAS,EAAE,eAAe;AAG5B,oBAAS;EACP,GAAG,EAAE,GAAG;EACR,KAAK,EAAE,IAAI;EACX,MAAM,EAAE,GAAG;EACX,UAAU,EAAE,IAAI;EAChB,kBAAkB,EAzJK,kBAAkB;;ACD7C,oDAAoD;AAEpD,cAAe;EACX,OAAO,EAAE,WAAW;;AAGxB,eAAgB;EACZ,QAAQ,EAAE,iBAAiB;EAC3B,OAAO,EAAE,YAAY;EACrB,QAAQ,EAAE,QAAQ;;AAGtB,iCAAkC;EAC9B,MAAM,EAAE,eAAe;EACvB,UAAU,EAAE,sBAAsB;EAClC,MAAM,EAAE,IAAI;EACZ,IAAI,EAAE,CAAC;EACP,MAAM,EAAE,CAAC;EACT,UAAU,EAAE,IAAI;EAChB,SAAS,EAAE,eAAe;EAC1B,QAAQ,EAAE,iBAAiB;EAC3B,OAAO,EAAE,CAAC;EACV,QAAQ,EAAE,mBAAmB;EAC7B,GAAG,EAAE,CAAC;EACN,KAAK,EAAE,eAAe;;AAG1B,oDAAqD;EACjD,MAAM,EAAE,CAAC;EACT,KAAK,EAAE,CAAC;;AAGZ,eAAgB;EACZ,OAAO,EAAE,IAAI;;AAEjB,oCAAqC;EACjC,UAAU,EAAE,WAAW;;AAG3B;+CACgD;EAC5C,OAAO,EAAE,KAAK;;AAGlB;6BAC8B;EAC1B,MAAM,EAAE,OAAO;;AAMnB,kCAAmC;EAC/B,QAAQ,EAAE,iBAAiB;;AAE/B,6CAA8C;EAC1C,MAAM,EAAE,eAAe;EACvB,UAAU,EAAE,UAAU;EACtB,MAAM,EAAE,eAAe;EACvB,MAAM,EAAE,CAAC;EACT,UAAU,EAAE,eAAe;EAC3B,SAAS,EAAE,eAAe;EAC1B,QAAQ,EAAE,iBAAiB;EAC3B,OAAO,EAAE,IAAI;EACb,OAAO,EAAE,GAAG;EACZ,QAAQ,EAAE,mBAAmB;EAC7B,GAAG,EAAE,CAAC;EACN,KAAK,EAAE,eAAe;;AAE1B,gEAAiE;EAC7D,MAAM,EAAE,CAAC;EACT,KAAK,EAAE,CAAC;;AAMZ,wDAAwD;AAExD;sCAEA;EACI,MAAM,EAAE,IAAI;EACZ,MAAM,EAAE,CAAC;EACT,OAAO,EAAE,CAAC;EACV,QAAQ,EAAE,QAAQ;EAClB,OAAO,EAAE,EAAE;;AAGf,sCAAuC;EACnC,OAAO,EAAE,KAAK;EACd,MAAM,EAAE,IAAI;EACZ,IAAI,EAAE,CAAC;EACP,GAAG,EAAE,CAAC;EACN,KAAK,EAAE,IAAI;;AAGf,2CAA4C;EACxC,MAAM,EAAE,GAAG;EACX,MAAM,EAAE,GAAG;EACX,IAAI,EAAE,CAAC;EACP,KAAK,EAAE,IAAI;;AAGf,2CAA4C;EACxC,MAAM,EAAE,IAAI;EACZ,KAAK,EAAE,GAAG;EACV,GAAG,EAAE,CAAC;EACN,KAAK,EAAE,GAAG;;AAGd,wDAAyD;EACrD,QAAQ,EAAE,MAAM;;AAGpB;;8CAE+C;EAC3C,qBAAqB,EAAE,GAAG;EAC1B,kBAAkB,EAAE,GAAG;EACvB,aAAa,EAAE,GAAG;;AAGtB;8CAC+C;EAC3C,UAAU,EAAC,qDAAqD;EAChE,MAAM,EAAE,iBAAiB;EACzB,OAAO,EAAE,GAAG;;AAIhB,yDAAyD;AAEzD,wFAAyF;EAAE,IAAI,EAAE,KAAK;;AACtG,wFAAyF;EAAE,GAAG,EAAE,KAAK;;AAGrG,uFAAwF;EAAE,IAAI,EAAE,KAAK;;AACrG,uFAAwF;EAAE,GAAG,EAAE,KAAK;;ACpInG,sBAAM;EACL,QAAQ,EAAE,OAAO;EACjB,+BAAS;IACR,QAAQ,EAAE,OAAO;;AAOnB,iBAAE;EACD,cAAc,EAAE,MAAM;;AAKxB,iBAAkB;EACd,MAAM,EAAE,YAAY;;AAQtB,yBAAG;EACF,UAAU,EAAE,IAAI;EAChB,MAAM,EAAE,CAAC;EACT,OAAO,EAAE,QAAQ;EACjB,QAAQ,EAAE,QAAQ;AAGnB,yBAAG;EACF,MAAM,EAAE,aAAa;EACrB,OAAO,EAAE,IAAI;EACb,QAAQ,EAAE,QAAQ;EAElB,4BAAG;IAAC,WAAW,EAAE,IAAI;EAErB,+FAAO;IACN,eAAe,EAAE,SAAS;EAG3B,4BAAG;IACF,WAAW,EAAE,KAAK;AAKpB,yCAAmB;EAClB,SAAS,EAAE,IAAI;EACf,QAAQ,EAAE,QAAQ;EAClB,MAAM,EAAE,CAAC;EACT,OAAO,EAAE,QAAQ;EACjB,KAAK,EAAE,CAAC;EACR,OAAO,EAAE,IAAI;EACb,KAAK,EAAE,IAAI;EACX,UAAU,EftCD,OAAO;AeyCjB,4BAAM;EACL,UAAU,EAAE,eAAe;EAC3B,KAAK,EAAE,eAAe;AAGvB,oCAAc;EACb,UAAU,EAAE,OAAuB;EACnC,MAAM,EAAE,iBAAoB;EAC5B,KAAK,EfhDI,OAAO;EeiDhB,uCAAG;IACF,UAAU,EfjDJ,OAAO;AesDf,oCAAe;EACd,UAAU,EAAE,OAAqB;EACjC,MAAM,EAAE,iBAAkB;EAC1B,KAAK,EfxDI,OAAO;EeyDhB,uCAAG;IACF,UAAU,EfzDA,OAAO;Ae6DnB,oCAAc;EACb,UAAU,EAAE,OAAyB;EACrC,MAAM,EAAE,iBAAsB;EAC9B,KAAK,Ef/DM,OAAO;EegElB,uCAAG;IACF,UAAU,Ef9DA,OAAO;AekEnB,oCAAa;EACZ,UAAU,EAAE,OAAyB;EACrC,MAAM,EAAE,iBAAsB;EAC9B,KAAK,EfpEQ,OAAO;EeqEpB,uCAAG;IACF,UAAU,EfvEA,OAAO;;Ae6ErB,QAAS;EACL,UAAU,EfvGF,OAAO;EewGf,aAAa,EAAE,GAAG;EAClB,MAAM,EAAE,eAAe;EACvB,OAAO,EAAE,MAAM;EACf,MAAM,EAAE,CAAC;EACT,UAAU,EAAE,IAAI;;AAIpB,gBAAiB;EAGb,QAAQ,EAAE,QAAQ;EAClB,OAAO,EAAE,CAAC;EAEV,KAAK,EAAE,KAAK;EACZ,OAAO,EAAE,KAAK;;AAIlB,kBAAmB;EAIf,MAAM,EAAE,IAAI;EACZ,OAAO,EAAE,KAAK;EACd,aAAa,EAAE,CAAC;EAChB,YAAY,EAAE,CAAC;EAEf,WAAW,EAAE,GAAG;EAEhB,uBAAK;IACD,OAAO,EAAE,QAAQ;;ATvHvB,yCAAkE;ES6HhE,QAAS;IACL,KAAK,EfnJW,KAAK;;EeqJzB,KAAM;IACF,WAAW,EftJK,KAAK;AMkB3B,yCAAiE;ESwI/D,QAAS;IACL,KAAK,Ef3JW,KAAK;Ie4JrB,IAAI,EAAE,MAAwB;;EAElC,KAAM;IACF,WAAW,EAAE,CAAC;IACd,KAAK,EAAE,IAAI;;EAGf,eAAgB;IACZ,QAAQ,EAAE,MAAM;IAEhB,wBAAS;MACL,IAAI,EAAE,CAAC;IAEX,qBAAM;MACF,WAAW,Ef1KC,KAAK;Me4KjB,QAAQ,EAAE,MAAM;IAEpB,wBAAS;MACL,QAAQ,EAAE,QAAQ;MAClB,IAAI,EAAC,CAAC;MACN,KAAK,EAAE,CAAC;MACR,GAAG,EAAE,CAAC;MACN,MAAM,EAAE,CAAC;MACT,OAAO,EAAC,EAAE;MACV,UAAU,EAAE,wBAAoB;MAChC,MAAM,EAAE,OAAO;AAM3B,kBAAmB;EACjB,gBAAgB,EAAE,yBAAyB;EAC3C,mBAAmB,EAAE,OAAO;EAC5B,eAAe,EAAE,SAAS;EAC1B,iBAAiB,EAAE,SAAS;EAC5B,KAAK,EAAE,IAAI;EACX,MAAM,EAAE,OAA6B;EACtC,GAAG,EAAE,IAAI;EACR,OAAO,EAAE,YAAY;EACrB,cAAc,EAAE,MAAM;EACtB,QAAQ,EAAE,QAAQ;EAClB,KAAK,EAAE,OAAsB;EAC7B,gBAAgB,EhBzJH,OAAO;EgB0JpB,WAAW,EAAE,MAAM;EACnB,MAAM,EAAE,OAAO;EACf,aAAa,EAAE,WAAW;EAE1B,wBAAQ;IACN,gBAAgB,EAAE,OAAoB;EAGxC,sBAAM;IACJ,QAAQ,EAAE,QAAQ;IAClB,KAAK,EAAE,GAAG;IACV,GAAG,EAAE,GAAG;IACR,gBAAgB,EAAE,IAAI;IACtB,YAAY,EAAE,IAAI;IAClB,aAAa,EAAE,GAAG;IAElB,4BAAQ;MACN,gBAAgB,EAAE,OAAO;;AAM/B,eAAgB;EACd,uBAAuB,EAAE,WAAW;EACpC,oBAAoB,EAAE,WAAW;EACjC,eAAe,EAAE,WAAW", +"sources": ["../scss/theme/_fonts.scss","../scss/nucleus/mixins/_utilities.scss","../scss/theme/modules/_buttons.scss","../scss/theme/_core.scss","../scss/configuration/theme/_colors.scss","../scss/theme/_configuration.scss","../scss/vendor/bourbon/addons/_prefixer.scss","../scss/theme/_forms.scss","../scss/theme/_header.scss","../scss/vendor/bourbon/css3/_placeholder.scss","../scss/theme/_nav.scss","../scss/nucleus/mixins/_breakpoints.scss","../scss/theme/_main.scss","../scss/theme/_typography.scss","../scss/theme/_tables.scss","../scss/theme/_buttons.scss","../scss/theme/_bullets.scss","../scss/configuration/theme/_bullets.scss","../scss/theme/_tooltips.scss","../scss/theme/_scrollbar.scss","../scss/theme/_custom.scss"], +"names": [], +"file": "theme.css" +} \ No newline at end of file diff --git a/netbox/project-static/select2-4.0.12/docs/themes/learn2/css/featherlight.min.css b/netbox/project-static/select2-4.0.12/docs/themes/learn2/css/featherlight.min.css new file mode 100644 index 000000000..f225bec51 --- /dev/null +++ b/netbox/project-static/select2-4.0.12/docs/themes/learn2/css/featherlight.min.css @@ -0,0 +1,8 @@ +/** + * Featherlight - ultra slim jQuery lightbox + * Version 1.2.3 - http://noelboss.github.io/featherlight/ + * + * Copyright 2015, Noël Raoul Bossart (http://www.noelboss.com) + * MIT Licensed. +**/ +@media all{.featherlight{display:none;position:fixed;top:0;right:0;bottom:0;left:0;z-index:2147483647;text-align:center;white-space:nowrap;cursor:pointer;background:#333;background:rgba(0,0,0,0)}.featherlight:last-of-type{background:rgba(0,0,0,.8)}.featherlight:before{content:'';display:inline-block;height:100%;vertical-align:middle;margin-right:-.25em}.featherlight .featherlight-content{position:relative;text-align:left;vertical-align:middle;display:inline-block;overflow:auto;padding:25px 25px 0;border-bottom:25px solid transparent;min-width:30%;margin-left:5%;margin-right:5%;max-height:95%;background:#fff;cursor:auto;white-space:normal}.featherlight .featherlight-inner{display:block}.featherlight .featherlight-close-icon{position:absolute;z-index:9999;top:0;right:0;line-height:25px;width:25px;cursor:pointer;text-align:center;font:Arial,sans-serif;background:#fff;background:rgba(255,255,255,.3);color:#000}.featherlight .featherlight-image{width:100%}.featherlight-iframe .featherlight-content{border-bottom:0;padding:0}.featherlight iframe{border:0}}@media only screen and (max-width:1024px){.featherlight .featherlight-content{margin-left:10px;margin-right:10px;max-height:98%;padding:10px 10px 0;border-bottom:10px solid transparent}} \ No newline at end of file diff --git a/netbox/project-static/select2-4.0.12/docs/themes/learn2/css/font-awesome.min.css b/netbox/project-static/select2-4.0.12/docs/themes/learn2/css/font-awesome.min.css new file mode 100644 index 000000000..540440ce8 --- /dev/null +++ b/netbox/project-static/select2-4.0.12/docs/themes/learn2/css/font-awesome.min.css @@ -0,0 +1,4 @@ +/*! + * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome + * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) + */@font-face{font-family:'FontAwesome';src:url('../fonts/fontawesome-webfont.eot?v=4.7.0');src:url('../fonts/fontawesome-webfont.eot?#iefix&v=4.7.0') format('embedded-opentype'),url('../fonts/fontawesome-webfont.woff2?v=4.7.0') format('woff2'),url('../fonts/fontawesome-webfont.woff?v=4.7.0') format('woff'),url('../fonts/fontawesome-webfont.ttf?v=4.7.0') format('truetype'),url('../fonts/fontawesome-webfont.svg?v=4.7.0#fontawesomeregular') format('svg');font-weight:normal;font-style:normal}.fa{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571429em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14285714em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em;text-align:center}.fa-li.fa-lg{left:-1.85714286em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left{margin-right:.3em}.fa.fa-pull-right{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scale(1, -1);-ms-transform:scale(1, -1);transform:scale(1, -1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-remove:before,.fa-close:before,.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before,.fa-bar-chart:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook-f:before,.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-feed:before,.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-desc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-asc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before,.fa-gratipay:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa-space-shuttle:before{content:"\f197"}.fa-slack:before{content:"\f198"}.fa-envelope-square:before{content:"\f199"}.fa-wordpress:before{content:"\f19a"}.fa-openid:before{content:"\f19b"}.fa-institution:before,.fa-bank:before,.fa-university:before{content:"\f19c"}.fa-mortar-board:before,.fa-graduation-cap:before{content:"\f19d"}.fa-yahoo:before{content:"\f19e"}.fa-google:before{content:"\f1a0"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-square:before{content:"\f1a2"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-stumbleupon:before{content:"\f1a4"}.fa-delicious:before{content:"\f1a5"}.fa-digg:before{content:"\f1a6"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-drupal:before{content:"\f1a9"}.fa-joomla:before{content:"\f1aa"}.fa-language:before{content:"\f1ab"}.fa-fax:before{content:"\f1ac"}.fa-building:before{content:"\f1ad"}.fa-child:before{content:"\f1ae"}.fa-paw:before{content:"\f1b0"}.fa-spoon:before{content:"\f1b1"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-recycle:before{content:"\f1b8"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-tree:before{content:"\f1bb"}.fa-spotify:before{content:"\f1bc"}.fa-deviantart:before{content:"\f1bd"}.fa-soundcloud:before{content:"\f1be"}.fa-database:before{content:"\f1c0"}.fa-file-pdf-o:before{content:"\f1c1"}.fa-file-word-o:before{content:"\f1c2"}.fa-file-excel-o:before{content:"\f1c3"}.fa-file-powerpoint-o:before{content:"\f1c4"}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:"\f1c5"}.fa-file-zip-o:before,.fa-file-archive-o:before{content:"\f1c6"}.fa-file-sound-o:before,.fa-file-audio-o:before{content:"\f1c7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"}.fa-file-code-o:before{content:"\f1c9"}.fa-vine:before{content:"\f1ca"}.fa-codepen:before{content:"\f1cb"}.fa-jsfiddle:before{content:"\f1cc"}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:"\f1cd"}.fa-circle-o-notch:before{content:"\f1ce"}.fa-ra:before,.fa-resistance:before,.fa-rebel:before{content:"\f1d0"}.fa-ge:before,.fa-empire:before{content:"\f1d1"}.fa-git-square:before{content:"\f1d2"}.fa-git:before{content:"\f1d3"}.fa-y-combinator-square:before,.fa-yc-square:before,.fa-hacker-news:before{content:"\f1d4"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-qq:before{content:"\f1d6"}.fa-wechat:before,.fa-weixin:before{content:"\f1d7"}.fa-send:before,.fa-paper-plane:before{content:"\f1d8"}.fa-send-o:before,.fa-paper-plane-o:before{content:"\f1d9"}.fa-history:before{content:"\f1da"}.fa-circle-thin:before{content:"\f1db"}.fa-header:before{content:"\f1dc"}.fa-paragraph:before{content:"\f1dd"}.fa-sliders:before{content:"\f1de"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-bomb:before{content:"\f1e2"}.fa-soccer-ball-o:before,.fa-futbol-o:before{content:"\f1e3"}.fa-tty:before{content:"\f1e4"}.fa-binoculars:before{content:"\f1e5"}.fa-plug:before{content:"\f1e6"}.fa-slideshare:before{content:"\f1e7"}.fa-twitch:before{content:"\f1e8"}.fa-yelp:before{content:"\f1e9"}.fa-newspaper-o:before{content:"\f1ea"}.fa-wifi:before{content:"\f1eb"}.fa-calculator:before{content:"\f1ec"}.fa-paypal:before{content:"\f1ed"}.fa-google-wallet:before{content:"\f1ee"}.fa-cc-visa:before{content:"\f1f0"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-bell-slash:before{content:"\f1f6"}.fa-bell-slash-o:before{content:"\f1f7"}.fa-trash:before{content:"\f1f8"}.fa-copyright:before{content:"\f1f9"}.fa-at:before{content:"\f1fa"}.fa-eyedropper:before{content:"\f1fb"}.fa-paint-brush:before{content:"\f1fc"}.fa-birthday-cake:before{content:"\f1fd"}.fa-area-chart:before{content:"\f1fe"}.fa-pie-chart:before{content:"\f200"}.fa-line-chart:before{content:"\f201"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-bicycle:before{content:"\f206"}.fa-bus:before{content:"\f207"}.fa-ioxhost:before{content:"\f208"}.fa-angellist:before{content:"\f209"}.fa-cc:before{content:"\f20a"}.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:"\f20b"}.fa-meanpath:before{content:"\f20c"}.fa-buysellads:before{content:"\f20d"}.fa-connectdevelop:before{content:"\f20e"}.fa-dashcube:before{content:"\f210"}.fa-forumbee:before{content:"\f211"}.fa-leanpub:before{content:"\f212"}.fa-sellsy:before{content:"\f213"}.fa-shirtsinbulk:before{content:"\f214"}.fa-simplybuilt:before{content:"\f215"}.fa-skyatlas:before{content:"\f216"}.fa-cart-plus:before{content:"\f217"}.fa-cart-arrow-down:before{content:"\f218"}.fa-diamond:before{content:"\f219"}.fa-ship:before{content:"\f21a"}.fa-user-secret:before{content:"\f21b"}.fa-motorcycle:before{content:"\f21c"}.fa-street-view:before{content:"\f21d"}.fa-heartbeat:before{content:"\f21e"}.fa-venus:before{content:"\f221"}.fa-mars:before{content:"\f222"}.fa-mercury:before{content:"\f223"}.fa-intersex:before,.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-venus-double:before{content:"\f226"}.fa-mars-double:before{content:"\f227"}.fa-venus-mars:before{content:"\f228"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-neuter:before{content:"\f22c"}.fa-genderless:before{content:"\f22d"}.fa-facebook-official:before{content:"\f230"}.fa-pinterest-p:before{content:"\f231"}.fa-whatsapp:before{content:"\f232"}.fa-server:before{content:"\f233"}.fa-user-plus:before{content:"\f234"}.fa-user-times:before{content:"\f235"}.fa-hotel:before,.fa-bed:before{content:"\f236"}.fa-viacoin:before{content:"\f237"}.fa-train:before{content:"\f238"}.fa-subway:before{content:"\f239"}.fa-medium:before{content:"\f23a"}.fa-yc:before,.fa-y-combinator:before{content:"\f23b"}.fa-optin-monster:before{content:"\f23c"}.fa-opencart:before{content:"\f23d"}.fa-expeditedssl:before{content:"\f23e"}.fa-battery-4:before,.fa-battery:before,.fa-battery-full:before{content:"\f240"}.fa-battery-3:before,.fa-battery-three-quarters:before{content:"\f241"}.fa-battery-2:before,.fa-battery-half:before{content:"\f242"}.fa-battery-1:before,.fa-battery-quarter:before{content:"\f243"}.fa-battery-0:before,.fa-battery-empty:before{content:"\f244"}.fa-mouse-pointer:before{content:"\f245"}.fa-i-cursor:before{content:"\f246"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-sticky-note:before{content:"\f249"}.fa-sticky-note-o:before{content:"\f24a"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-diners-club:before{content:"\f24c"}.fa-clone:before{content:"\f24d"}.fa-balance-scale:before{content:"\f24e"}.fa-hourglass-o:before{content:"\f250"}.fa-hourglass-1:before,.fa-hourglass-start:before{content:"\f251"}.fa-hourglass-2:before,.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-3:before,.fa-hourglass-end:before{content:"\f253"}.fa-hourglass:before{content:"\f254"}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:"\f255"}.fa-hand-stop-o:before,.fa-hand-paper-o:before{content:"\f256"}.fa-hand-scissors-o:before{content:"\f257"}.fa-hand-lizard-o:before{content:"\f258"}.fa-hand-spock-o:before{content:"\f259"}.fa-hand-pointer-o:before{content:"\f25a"}.fa-hand-peace-o:before{content:"\f25b"}.fa-trademark:before{content:"\f25c"}.fa-registered:before{content:"\f25d"}.fa-creative-commons:before{content:"\f25e"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-tripadvisor:before{content:"\f262"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-get-pocket:before{content:"\f265"}.fa-wikipedia-w:before{content:"\f266"}.fa-safari:before{content:"\f267"}.fa-chrome:before{content:"\f268"}.fa-firefox:before{content:"\f269"}.fa-opera:before{content:"\f26a"}.fa-internet-explorer:before{content:"\f26b"}.fa-tv:before,.fa-television:before{content:"\f26c"}.fa-contao:before{content:"\f26d"}.fa-500px:before{content:"\f26e"}.fa-amazon:before{content:"\f270"}.fa-calendar-plus-o:before{content:"\f271"}.fa-calendar-minus-o:before{content:"\f272"}.fa-calendar-times-o:before{content:"\f273"}.fa-calendar-check-o:before{content:"\f274"}.fa-industry:before{content:"\f275"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-map-o:before{content:"\f278"}.fa-map:before{content:"\f279"}.fa-commenting:before{content:"\f27a"}.fa-commenting-o:before{content:"\f27b"}.fa-houzz:before{content:"\f27c"}.fa-vimeo:before{content:"\f27d"}.fa-black-tie:before{content:"\f27e"}.fa-fonticons:before{content:"\f280"}.fa-reddit-alien:before{content:"\f281"}.fa-edge:before{content:"\f282"}.fa-credit-card-alt:before{content:"\f283"}.fa-codiepie:before{content:"\f284"}.fa-modx:before{content:"\f285"}.fa-fort-awesome:before{content:"\f286"}.fa-usb:before{content:"\f287"}.fa-product-hunt:before{content:"\f288"}.fa-mixcloud:before{content:"\f289"}.fa-scribd:before{content:"\f28a"}.fa-pause-circle:before{content:"\f28b"}.fa-pause-circle-o:before{content:"\f28c"}.fa-stop-circle:before{content:"\f28d"}.fa-stop-circle-o:before{content:"\f28e"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-hashtag:before{content:"\f292"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-percent:before{content:"\f295"}.fa-gitlab:before{content:"\f296"}.fa-wpbeginner:before{content:"\f297"}.fa-wpforms:before{content:"\f298"}.fa-envira:before{content:"\f299"}.fa-universal-access:before{content:"\f29a"}.fa-wheelchair-alt:before{content:"\f29b"}.fa-question-circle-o:before{content:"\f29c"}.fa-blind:before{content:"\f29d"}.fa-audio-description:before{content:"\f29e"}.fa-volume-control-phone:before{content:"\f2a0"}.fa-braille:before{content:"\f2a1"}.fa-assistive-listening-systems:before{content:"\f2a2"}.fa-asl-interpreting:before,.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-deafness:before,.fa-hard-of-hearing:before,.fa-deaf:before{content:"\f2a4"}.fa-glide:before{content:"\f2a5"}.fa-glide-g:before{content:"\f2a6"}.fa-signing:before,.fa-sign-language:before{content:"\f2a7"}.fa-low-vision:before{content:"\f2a8"}.fa-viadeo:before{content:"\f2a9"}.fa-viadeo-square:before{content:"\f2aa"}.fa-snapchat:before{content:"\f2ab"}.fa-snapchat-ghost:before{content:"\f2ac"}.fa-snapchat-square:before{content:"\f2ad"}.fa-pied-piper:before{content:"\f2ae"}.fa-first-order:before{content:"\f2b0"}.fa-yoast:before{content:"\f2b1"}.fa-themeisle:before{content:"\f2b2"}.fa-google-plus-circle:before,.fa-google-plus-official:before{content:"\f2b3"}.fa-fa:before,.fa-font-awesome:before{content:"\f2b4"}.fa-handshake-o:before{content:"\f2b5"}.fa-envelope-open:before{content:"\f2b6"}.fa-envelope-open-o:before{content:"\f2b7"}.fa-linode:before{content:"\f2b8"}.fa-address-book:before{content:"\f2b9"}.fa-address-book-o:before{content:"\f2ba"}.fa-vcard:before,.fa-address-card:before{content:"\f2bb"}.fa-vcard-o:before,.fa-address-card-o:before{content:"\f2bc"}.fa-user-circle:before{content:"\f2bd"}.fa-user-circle-o:before{content:"\f2be"}.fa-user-o:before{content:"\f2c0"}.fa-id-badge:before{content:"\f2c1"}.fa-drivers-license:before,.fa-id-card:before{content:"\f2c2"}.fa-drivers-license-o:before,.fa-id-card-o:before{content:"\f2c3"}.fa-quora:before{content:"\f2c4"}.fa-free-code-camp:before{content:"\f2c5"}.fa-telegram:before{content:"\f2c6"}.fa-thermometer-4:before,.fa-thermometer:before,.fa-thermometer-full:before{content:"\f2c7"}.fa-thermometer-3:before,.fa-thermometer-three-quarters:before{content:"\f2c8"}.fa-thermometer-2:before,.fa-thermometer-half:before{content:"\f2c9"}.fa-thermometer-1:before,.fa-thermometer-quarter:before{content:"\f2ca"}.fa-thermometer-0:before,.fa-thermometer-empty:before{content:"\f2cb"}.fa-shower:before{content:"\f2cc"}.fa-bathtub:before,.fa-s15:before,.fa-bath:before{content:"\f2cd"}.fa-podcast:before{content:"\f2ce"}.fa-window-maximize:before{content:"\f2d0"}.fa-window-minimize:before{content:"\f2d1"}.fa-window-restore:before{content:"\f2d2"}.fa-times-rectangle:before,.fa-window-close:before{content:"\f2d3"}.fa-times-rectangle-o:before,.fa-window-close-o:before{content:"\f2d4"}.fa-bandcamp:before{content:"\f2d5"}.fa-grav:before{content:"\f2d6"}.fa-etsy:before{content:"\f2d7"}.fa-imdb:before{content:"\f2d8"}.fa-ravelry:before{content:"\f2d9"}.fa-eercast:before{content:"\f2da"}.fa-microchip:before{content:"\f2db"}.fa-snowflake-o:before{content:"\f2dc"}.fa-superpowers:before{content:"\f2dd"}.fa-wpexplorer:before{content:"\f2de"}.fa-meetup:before{content:"\f2e0"}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto} diff --git a/netbox/project-static/select2-4.0.12/docs/themes/learn2/css/nucleus-ie10.css b/netbox/project-static/select2-4.0.12/docs/themes/learn2/css/nucleus-ie10.css new file mode 100644 index 000000000..3111047e9 --- /dev/null +++ b/netbox/project-static/select2-4.0.12/docs/themes/learn2/css/nucleus-ie10.css @@ -0,0 +1,9 @@ +button { + overflow: visible; +} + +input[type="checkbox"], +input[type="radio"] { + box-sizing: border-box; + padding: 0; +} \ No newline at end of file diff --git a/netbox/project-static/select2-4.0.12/docs/themes/learn2/css/nucleus-ie9.css b/netbox/project-static/select2-4.0.12/docs/themes/learn2/css/nucleus-ie9.css new file mode 100644 index 000000000..46df37630 --- /dev/null +++ b/netbox/project-static/select2-4.0.12/docs/themes/learn2/css/nucleus-ie9.css @@ -0,0 +1,62 @@ +/* IE9 Resets and Normalization */ +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +main, +nav, +section, +summary { + display: block; +} + +audio, +canvas, +progress, +video { + display: inline-block; +} + +[hidden], +template { + display: none; +} + +abbr[title] { + border-bottom: 1px dotted; +} + +img { + border: 0; +} + +svg:not(:root) { + overflow: hidden; +} + +figure { + margin: 1em 40px; +} + +button { + overflow: visible; +} + +input[type="checkbox"], +input[type="radio"] { + box-sizing: border-box; + padding: 0; +} + +legend { + border: 0; + padding: 0; +} + +textarea { + overflow: auto; +} \ No newline at end of file diff --git a/netbox/project-static/select2-4.0.12/docs/themes/learn2/css/pure-0.5.0/grids-min.css b/netbox/project-static/select2-4.0.12/docs/themes/learn2/css/pure-0.5.0/grids-min.css new file mode 100644 index 000000000..82bf81639 --- /dev/null +++ b/netbox/project-static/select2-4.0.12/docs/themes/learn2/css/pure-0.5.0/grids-min.css @@ -0,0 +1,15 @@ +/*! +Pure v0.5.0-rc-1 +Copyright 2014 Yahoo! Inc. All rights reserved. +Licensed under the BSD License. +https://github.com/yui/pure/blob/master/LICENSE.md +*/ +.pure-g{letter-spacing:-.31em;*letter-spacing:normal;*word-spacing:-.43em;text-rendering:optimizespeed;font-family:FreeSans,Arimo,"Droid Sans",Helvetica,Arial,sans-serif;display:-webkit-flex;-webkit-flex-flow:row wrap;display:-ms-flexbox;-ms-flex-flow:row wrap}.opera-only :-o-prefocus,.pure-g{word-spacing:-.43em}.pure-u{display:inline-block;*display:inline;zoom:1;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-g [class *="pure-u"]{font-family:sans-serif}.pure-u-1,.pure-u-1-1,.pure-u-1-2,.pure-u-1-3,.pure-u-2-3,.pure-u-1-4,.pure-u-3-4,.pure-u-1-5,.pure-u-2-5,.pure-u-3-5,.pure-u-4-5,.pure-u-5-5,.pure-u-1-6,.pure-u-5-6,.pure-u-1-8,.pure-u-3-8,.pure-u-5-8,.pure-u-7-8,.pure-u-1-12,.pure-u-5-12,.pure-u-7-12,.pure-u-11-12,.pure-u-1-24,.pure-u-2-24,.pure-u-3-24,.pure-u-4-24,.pure-u-5-24,.pure-u-6-24,.pure-u-7-24,.pure-u-8-24,.pure-u-9-24,.pure-u-10-24,.pure-u-11-24,.pure-u-12-24,.pure-u-13-24,.pure-u-14-24,.pure-u-15-24,.pure-u-16-24,.pure-u-17-24,.pure-u-18-24,.pure-u-19-24,.pure-u-20-24,.pure-u-21-24,.pure-u-22-24,.pure-u-23-24,.pure-u-24-24{display:inline-block;*display:inline;zoom:1;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-u-1-24{width:4.1667%;*width:4.1357%}.pure-u-1-12,.pure-u-2-24{width:8.3333%;*width:8.3023%}.pure-u-1-8,.pure-u-3-24{width:12.5%;*width:12.469%}.pure-u-1-6,.pure-u-4-24{width:16.6667%;*width:16.6357%}.pure-u-1-5{width:20%;*width:19.969%}.pure-u-5-24{width:20.8333%;*width:20.8023%}.pure-u-1-4,.pure-u-6-24{width:25%;*width:24.969%}.pure-u-7-24{width:29.1667%;*width:29.1357%}.pure-u-1-3,.pure-u-8-24{width:33.3333%;*width:33.3023%}.pure-u-3-8,.pure-u-9-24{width:37.5%;*width:37.469%}.pure-u-2-5{width:40%;*width:39.969%}.pure-u-5-12,.pure-u-10-24{width:41.6667%;*width:41.6357%}.pure-u-11-24{width:45.8333%;*width:45.8023%}.pure-u-1-2,.pure-u-12-24{width:50%;*width:49.969%}.pure-u-13-24{width:54.1667%;*width:54.1357%}.pure-u-7-12,.pure-u-14-24{width:58.3333%;*width:58.3023%}.pure-u-3-5{width:60%;*width:59.969%}.pure-u-5-8,.pure-u-15-24{width:62.5%;*width:62.469%}.pure-u-2-3,.pure-u-16-24{width:66.6667%;*width:66.6357%}.pure-u-17-24{width:70.8333%;*width:70.8023%}.pure-u-3-4,.pure-u-18-24{width:75%;*width:74.969%}.pure-u-19-24{width:79.1667%;*width:79.1357%}.pure-u-4-5{width:80%;*width:79.969%}.pure-u-5-6,.pure-u-20-24{width:83.3333%;*width:83.3023%}.pure-u-7-8,.pure-u-21-24{width:87.5%;*width:87.469%}.pure-u-11-12,.pure-u-22-24{width:91.6667%;*width:91.6357%}.pure-u-23-24{width:95.8333%;*width:95.8023%}.pure-u-1,.pure-u-1-1,.pure-u-5-5,.pure-u-24-24{width:100%} + +/* Custom */ +[class *="pure-u"] {display:inline-block;*display:inline;zoom:1;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto;} +.pure-u-1-7 {width: 14.285%;}.pure-u-2-7 {width: 28.571%;}.pure-u-3-7 {width: 42.857%;}.pure-u-4-7 {width: 57.142%;}.pure-u-5-7 {width: 71.428%;}.pure-u-6-7 {width: 85.714%;} +.pure-u-1-9 {width: 11.111%;}.pure-u-2-9 {width: 22.222%;}.pure-u-3-9 {width: 33.333%;}.pure-u-4-9 {width: 44.444%;}.pure-u-5-9 {width: 55.555%;}.pure-u-6-9 {width: 66.666%;}.pure-u-7-9 {width: 77.777%;}.pure-u-8-9 {width: 88.888%;} +.pure-u-1-10 {width: 10%;}.pure-u-2-10 {width: 20%;}.pure-u-3-10 {width: 30%;}.pure-u-4-10 {width: 40%;}.pure-u-5-10 {width: 50%;}.pure-u-6-10 {width: 60%;}.pure-u-7-10 {width: 70%;}.pure-u-8-10 {width: 80%;}.pure-u-9-10 {width: 90%;} + +.pure-u-1-11 {width: 9.090%;}.pure-u-2-11 {width: 18.181%;}.pure-u-3-11 {width: 27.272%;}.pure-u-4-11 {width: 36.363%;}.pure-u-5-11 {width: 45.454%;}.pure-u-6-11 {width: 54.545%;}.pure-u-7-11 {width: 63.636%;}.pure-u-8-11 {width: 72.727%;}.pure-u-9-11 {width: 81.818%;}.pure-u-10-11 {width: 90.909%;} \ No newline at end of file diff --git a/netbox/project-static/select2-4.0.12/docs/themes/learn2/fonts/fontawesome-webfont.eot b/netbox/project-static/select2-4.0.12/docs/themes/learn2/fonts/fontawesome-webfont.eot new file mode 100644 index 000000000..e9f60ca95 Binary files /dev/null and b/netbox/project-static/select2-4.0.12/docs/themes/learn2/fonts/fontawesome-webfont.eot differ diff --git a/netbox/project-static/select2-4.0.12/docs/themes/learn2/fonts/fontawesome-webfont.svg b/netbox/project-static/select2-4.0.12/docs/themes/learn2/fonts/fontawesome-webfont.svg new file mode 100644 index 000000000..855c845e5 --- /dev/null +++ b/netbox/project-static/select2-4.0.12/docs/themes/learn2/fonts/fontawesome-webfont.svg @@ -0,0 +1,2671 @@ + + + + +Created by FontForge 20120731 at Mon Oct 24 17:37:40 2016 + By ,,, +Copyright Dave Gandy 2016. All rights reserved. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/netbox/project-static/select2-4.0.12/docs/themes/learn2/fonts/fontawesome-webfont.ttf b/netbox/project-static/select2-4.0.12/docs/themes/learn2/fonts/fontawesome-webfont.ttf new file mode 100644 index 000000000..35acda2fa Binary files /dev/null and b/netbox/project-static/select2-4.0.12/docs/themes/learn2/fonts/fontawesome-webfont.ttf differ diff --git a/netbox/project-static/select2-4.0.12/docs/themes/learn2/fonts/fontawesome-webfont.woff b/netbox/project-static/select2-4.0.12/docs/themes/learn2/fonts/fontawesome-webfont.woff new file mode 100644 index 000000000..400014a4b Binary files /dev/null and b/netbox/project-static/select2-4.0.12/docs/themes/learn2/fonts/fontawesome-webfont.woff differ diff --git a/netbox/project-static/select2-4.0.12/docs/themes/learn2/fonts/fontawesome-webfont.woff2 b/netbox/project-static/select2-4.0.12/docs/themes/learn2/fonts/fontawesome-webfont.woff2 new file mode 100644 index 000000000..4d13fc604 Binary files /dev/null and b/netbox/project-static/select2-4.0.12/docs/themes/learn2/fonts/fontawesome-webfont.woff2 differ diff --git a/netbox/project-static/select2-4.0.12/docs/themes/learn2/images/clippy.svg b/netbox/project-static/select2-4.0.12/docs/themes/learn2/images/clippy.svg new file mode 100644 index 000000000..e1b170359 --- /dev/null +++ b/netbox/project-static/select2-4.0.12/docs/themes/learn2/images/clippy.svg @@ -0,0 +1,3 @@ + + + diff --git a/netbox/project-static/select2-4.0.12/docs/themes/learn2/images/favicon.png b/netbox/project-static/select2-4.0.12/docs/themes/learn2/images/favicon.png new file mode 100644 index 000000000..ec645f192 Binary files /dev/null and b/netbox/project-static/select2-4.0.12/docs/themes/learn2/images/favicon.png differ diff --git a/netbox/project-static/select2-4.0.12/docs/themes/learn2/images/logo.png b/netbox/project-static/select2-4.0.12/docs/themes/learn2/images/logo.png new file mode 100644 index 000000000..287a4e756 Binary files /dev/null and b/netbox/project-static/select2-4.0.12/docs/themes/learn2/images/logo.png differ diff --git a/netbox/project-static/select2-4.0.12/docs/themes/learn2/js/clipboard.min.js b/netbox/project-static/select2-4.0.12/docs/themes/learn2/js/clipboard.min.js new file mode 100644 index 000000000..000e4b48e --- /dev/null +++ b/netbox/project-static/select2-4.0.12/docs/themes/learn2/js/clipboard.min.js @@ -0,0 +1,7 @@ +/*! + * clipboard.js v1.5.5 + * https://zenorocha.github.io/clipboard.js + * + * Licensed MIT © Zeno Rocha + */ +!function(t){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=t();else if("function"==typeof define&&define.amd)define([],t);else{var e;e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,e.Clipboard=t()}}(function(){var t,e,n;return function t(e,n,r){function o(a,c){if(!n[a]){if(!e[a]){var s="function"==typeof require&&require;if(!c&&s)return s(a,!0);if(i)return i(a,!0);var u=new Error("Cannot find module '"+a+"'");throw u.code="MODULE_NOT_FOUND",u}var l=n[a]={exports:{}};e[a][0].call(l.exports,function(t){var n=e[a][1][t];return o(n?n:t)},l,l.exports,t,e,n,r)}return n[a].exports}for(var i="function"==typeof require&&require,a=0;ar;r++)n[r].fn.apply(n[r].ctx,e);return this},off:function(t,e){var n=this.e||(this.e={}),r=n[t],o=[];if(r&&e)for(var i=0,a=r.length;a>i;i++)r[i].fn!==e&&r[i].fn._!==e&&o.push(r[i]);return o.length?n[t]=o:delete n[t],this}},e.exports=r},{}],8:[function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function o(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}n.__esModule=!0;var i=function(){function t(t,e){for(var n=0;n0})},e=function(a,b){var c={},d=new RegExp("^"+b+"([A-Z])(.*)");for(var e in a){var f=e.match(d);if(f){var g=(f[1]+f[2].replace(/([A-Z])/g,"-$1")).toLowerCase();c[g]=a[e]}}return c},f={keyup:"onKeyUp",resize:"onResize"},g=function(c){a.each(b.opened().reverse(),function(){return c.isDefaultPrevented()||!1!==this[f[c.type]](c)?void 0:(c.preventDefault(),c.stopPropagation(),!1)})},h=function(c){if(c!==b._globalHandlerInstalled){b._globalHandlerInstalled=c;var d=a.map(f,function(a,c){return c+"."+b.prototype.namespace}).join(" ");a(window)[c?"on":"off"](d,g)}};b.prototype={constructor:b,namespace:"featherlight",targetAttr:"data-featherlight",variant:null,resetCss:!1,background:null,openTrigger:"click",closeTrigger:"click",filter:null,root:"body",openSpeed:250,closeSpeed:250,closeOnClick:"background",closeOnEsc:!0,closeIcon:"✕",loading:"",otherClose:null,beforeOpen:a.noop,beforeContent:a.noop,beforeClose:a.noop,afterOpen:a.noop,afterContent:a.noop,afterClose:a.noop,onKeyUp:a.noop,onResize:a.noop,type:null,contentFilters:["jquery","image","html","ajax","iframe","text"],setup:function(b,c){"object"!=typeof b||b instanceof a!=!1||c||(c=b,b=void 0);var d=a.extend(this,c,{target:b}),e=d.resetCss?d.namespace+"-reset":d.namespace,f=a(d.background||['
','
','',d.closeIcon,"",'
'+d.loading+"
","
","
"].join("")),g="."+d.namespace+"-close"+(d.otherClose?","+d.otherClose:"");return d.$instance=f.clone().addClass(d.variant),d.$instance.on(d.closeTrigger+"."+d.namespace,function(b){var c=a(b.target);("background"===d.closeOnClick&&c.is("."+d.namespace)||"anywhere"===d.closeOnClick||c.closest(g).length)&&(b.preventDefault(),d.close())}),this},getContent:function(){var b=this,c=this.constructor.contentFilters,d=function(a){return b.$currentTarget&&b.$currentTarget.attr(a)},e=d(b.targetAttr),f=b.target||e||"",g=c[b.type];if(!g&&f in c&&(g=c[f],f=b.target&&e),f=f||d("href")||"",!g)for(var h in c)b[h]&&(g=c[h],f=b[h]);if(!g){var i=f;if(f=null,a.each(b.contentFilters,function(){return g=c[this],g.test&&(f=g.test(i)),!f&&g.regex&&i.match&&i.match(g.regex)&&(f=i),!f}),!f)return"console"in window&&window.console.error("Featherlight: no content filter found "+(i?' for "'+i+'"':" (no target specified)")),!1}return g.process.call(b,f)},setContent:function(b){var c=this;return(b.is("iframe")||a("iframe",b).length>0)&&c.$instance.addClass(c.namespace+"-iframe"),c.$instance.removeClass(c.namespace+"-loading"),c.$instance.find("."+c.namespace+"-inner").slice(1).remove().end().replaceWith(a.contains(c.$instance[0],b[0])?"":b),c.$content=b.addClass(c.namespace+"-inner"),c},open:function(b){var d=this;if(d.$instance.hide().appendTo(d.root),!(b&&b.isDefaultPrevented()||d.beforeOpen(b)===!1)){b&&b.preventDefault();var e=d.getContent();if(e)return c.push(d),h(!0),d.$instance.fadeIn(d.openSpeed),d.beforeContent(b),a.when(e).always(function(a){d.setContent(a),d.afterContent(b)}).then(d.$instance.promise()).done(function(){d.afterOpen(b)})}return d.$instance.detach(),a.Deferred().reject().promise()},close:function(b){var c=this,e=a.Deferred();return c.beforeClose(b)===!1?e.reject():(0===d(c).length&&h(!1),c.$instance.fadeOut(c.closeSpeed,function(){c.$instance.detach(),c.afterClose(b),e.resolve()})),e.promise()},chainCallbacks:function(b){for(var c in b)this[c]=a.proxy(b[c],this,a.proxy(this[c],this))}},a.extend(b,{id:0,autoBind:"[data-featherlight]",defaults:b.prototype,contentFilters:{jquery:{regex:/^[#.]\w/,test:function(b){return b instanceof a&&b},process:function(b){return a(b).clone(!0)}},image:{regex:/\.(png|jpg|jpeg|gif|tiff|bmp)(\?\S*)?$/i,process:function(b){var c=this,d=a.Deferred(),e=new Image,f=a('');return e.onload=function(){f.naturalWidth=e.width,f.naturalHeight=e.height,d.resolve(f)},e.onerror=function(){d.reject(f)},e.src=b,d.promise()}},html:{regex:/^\s*<[\w!][^<]*>/,process:function(b){return a(b)}},ajax:{regex:/./,process:function(b){var c=a.Deferred(),d=a("
").load(b,function(a,b){"error"!==b&&c.resolve(d.contents()),c.fail()});return c.promise()}},iframe:{process:function(b){var c=new a.Deferred,d=a("