Explorar el Código

Merge pull request #3936 from netbox-community/develop-2.7

Merge v2.7 changes
Jeremy Stretch hace 6 años
padre
commit
3eb2d45e8d
Se han modificado 100 ficheros con 10478 adiciones y 4266 borrados
  1. 4 0
      .gitignore
  2. 1 0
      .travis.yml
  3. 12 8
      base_requirements.txt
  4. 16 0
      contrib/gunicorn.py
  5. 22 0
      contrib/netbox-rq.service
  6. 22 0
      contrib/netbox.service
  7. 5 0
      docs/additional-features/graphs.md
  8. 0 17
      docs/additional-features/topology-maps.md
  9. 18 6
      docs/configuration/optional-settings.md
  10. 26 18
      docs/configuration/required-settings.md
  11. 1 1
      docs/installation/1-postgresql.md
  12. 35 30
      docs/installation/2-netbox.md
  13. 33 27
      docs/installation/3-http-daemon.md
  14. 2 0
      docs/installation/index.md
  15. 100 0
      docs/installation/migrating-to-systemd.md
  16. 5 7
      docs/installation/upgrading.md
  17. BIN
      docs/media/installation/netbox_ui_admin.png
  18. BIN
      docs/media/installation/netbox_ui_guest.png
  19. 1 1
      docs/release-notes/index.md
  20. 0 15
      docs/release-notes/version-2.6.md
  21. 276 0
      docs/release-notes/version-2.7.md
  22. 3 3
      netbox/circuits/api/serializers.py
  23. 8 8
      netbox/circuits/api/views.py
  24. 48 0
      netbox/circuits/choices.py
  25. 0 23
      netbox/circuits/constants.py
  26. 11 11
      netbox/circuits/filters.py
  27. 0 26
      netbox/circuits/fixtures/initial_data.json
  28. 5 5
      netbox/circuits/forms.py
  29. 74 34
      netbox/circuits/migrations/0001_initial_squashed_0006_terminations.py
  30. 254 0
      netbox/circuits/migrations/0007_circuit_add_description_squashed_0017_circuittype_description.py
  31. 1 1
      netbox/circuits/migrations/0013_cables.py
  32. 39 0
      netbox/circuits/migrations/0016_3569_circuit_fields.py
  33. 18 0
      netbox/circuits/migrations/0017_circuittype_description.py
  34. 40 9
      netbox/circuits/models.py
  35. 4 2
      netbox/circuits/tables.py
  36. 60 18
      netbox/circuits/tests/test_api.py
  37. 12 12
      netbox/circuits/tests/test_filters.py
  38. 60 3
      netbox/circuits/tests/test_views.py
  39. 19 15
      netbox/circuits/views.py
  40. 106 50
      netbox/dcim/api/serializers.py
  41. 95 56
      netbox/dcim/api/views.py
  42. 1076 0
      netbox/dcim/choices.py
  43. 35 481
      netbox/dcim/constants.py
  44. 6 3
      netbox/dcim/fields.py
  45. 140 82
      netbox/dcim/filters.py
  46. 11 11
      netbox/dcim/fixtures/dcim.json
  47. 0 195
      netbox/dcim/fixtures/initial_data.json
  48. 442 77
      netbox/dcim/forms.py
  49. 0 259
      netbox/dcim/migrations/0002_auto_20160622_1821_squashed_0022_color_names_to_rgb.py
  50. 101 0
      netbox/dcim/migrations/0003_auto_20160628_1721_squashed_0010_devicebay_installed_device_set_null.py
  51. 154 0
      netbox/dcim/migrations/0011_devicetype_part_number_squashed_0022_color_names_to_rgb.py
  52. 54 10
      netbox/dcim/migrations/0023_devicetype_comments_squashed_0043_device_component_name_lengths.py
  53. 0 144
      netbox/dcim/migrations/0044_virtualization_squashed_0055_virtualchassis_ordering.py
  54. 354 0
      netbox/dcim/migrations/0044_virtualization_squashed_0061_platform_napalm_args.py
  55. 124 0
      netbox/dcim/migrations/0062_interface_mtu_squashed_0065_front_rear_ports.py
  56. 146 0
      netbox/dcim/migrations/0067_device_type_remove_qualifiers_squashed_0070_custom_tag_models.py
  57. 839 0
      netbox/dcim/migrations/0071_device_components_add_description_squashed_0088_powerfeed_available_power.py
  58. 33 0
      netbox/dcim/migrations/0076_console_port_types.py
  59. 33 0
      netbox/dcim/migrations/0077_power_types.py
  60. 35 0
      netbox/dcim/migrations/0078_3569_site_fields.py
  61. 92 0
      netbox/dcim/migrations/0079_3569_rack_fields.py
  62. 39 0
      netbox/dcim/migrations/0080_3569_devicetype_fields.py
  63. 65 0
      netbox/dcim/migrations/0081_3569_device_fields.py
  64. 147 0
      netbox/dcim/migrations/0082_3569_interface_fields.py
  65. 93 0
      netbox/dcim/migrations/0082_3569_port_fields.py
  66. 106 0
      netbox/dcim/migrations/0083_3569_cable_fields.py
  67. 100 0
      netbox/dcim/migrations/0084_3569_powerfeed_fields.py
  68. 62 0
      netbox/dcim/migrations/0085_3569_poweroutlet_fields.py
  69. 23 0
      netbox/dcim/migrations/0086_device_name_nonunique.py
  70. 23 0
      netbox/dcim/migrations/0087_role_descriptions.py
  71. 18 0
      netbox/dcim/migrations/0088_powerfeed_available_power.py
  72. 21 0
      netbox/dcim/migrations/0089_deterministic_ordering.py
  73. 24 0
      netbox/dcim/migrations/0090_cable_termination_models.py
  74. 752 1786
      netbox/dcim/models/__init__.py
  75. 400 0
      netbox/dcim/models/device_component_templates.py
  76. 1019 0
      netbox/dcim/models/device_components.py
  77. 159 21
      netbox/dcim/tables.py
  78. 146 53
      netbox/dcim/tests/test_api.py
  79. 146 144
      netbox/dcim/tests/test_filters.py
  80. 8 8
      netbox/dcim/tests/test_forms.py
  81. 65 21
      netbox/dcim/tests/test_models.py
  82. 841 26
      netbox/dcim/tests/test_views.py
  83. 17 1
      netbox/dcim/urls.py
  84. 222 73
      netbox/dcim/views.py
  85. 0 14
      netbox/extras/__init__.py
  86. 3 17
      netbox/extras/admin.py
  87. 8 8
      netbox/extras/api/customfields.py
  88. 65 21
      netbox/extras/api/serializers.py
  89. 3 3
      netbox/extras/api/urls.py
  90. 62 40
      netbox/extras/api/views.py
  91. 16 23
      netbox/extras/apps.py
  92. 140 0
      netbox/extras/choices.py
  93. 159 205
      netbox/extras/constants.py
  94. 29 37
      netbox/extras/filters.py
  95. 26 10
      netbox/extras/forms.py
  96. 9 8
      netbox/extras/middleware.py
  97. 147 72
      netbox/extras/migrations/0001_initial_squashed_0013_objectchange.py
  98. 5 7
      netbox/extras/migrations/0010_customfield_filter_logic.py
  99. 106 0
      netbox/extras/migrations/0014_configcontexts_squashed_0019_tag_taggeditem.py
  100. 93 0
      netbox/extras/migrations/0020_tag_data_squashed_0021_add_color_comments_changelog_to_tag.py

+ 4 - 0
.gitignore

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

+ 1 - 0
.travis.yml

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

+ 12 - 8
base_requirements.txt

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

+ 16 - 0
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

+ 22 - 0
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

+ 22 - 0
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

+ 5 - 0
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`.
 * **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`.
 * **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
 ## 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:
 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:

+ 0 - 17
docs/additional-features/topology-maps.md

@@ -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.

+ 18 - 6
docs/configuration/optional-settings.md

@@ -293,19 +293,31 @@ Session data is used to track authenticated users when they access NetBox. By de
 
 
 ---
 ---
 
 
-## TIME_ZONE
+## STORAGE_BACKEND
 
 
-Default: UTC
+Default: None (local storage)
 
 
-The time zone NetBox will use when dealing with dates and times. It is recommended to use UTC time unless you have a specific need to use a local time zone. [List of available time zones](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones).
+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.
 
 
 ---
 ---
 
 
-## WEBHOOKS_ENABLED
+## STORAGE_CONFIG
 
 
-Default: False
+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.
 
 
-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.
+---
+
+## TIME_ZONE
+
+Default: UTC
+
+The time zone NetBox will use when dealing with dates and times. It is recommended to use UTC time unless you have a specific need to use a local time zone. [List of available time zones](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones).
 
 
 ---
 ---
 
 

+ 26 - 18
docs/configuration/required-settings.md

@@ -25,7 +25,7 @@ NetBox requires access to a PostgreSQL database service to store data. This serv
 
 
 Example:
 Example:
 
 
-```
+```python
 DATABASE = {
 DATABASE = {
     'NAME': 'netbox',               # Database name
     'NAME': 'netbox',               # Database name
     'USER': 'netbox',               # PostgreSQL username
     'USER': 'netbox',               # PostgreSQL username
@@ -42,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
 [Redis](https://redis.io/) is an in-memory data store similar to memcached. While Redis has been an optional component of
 NetBox since the introduction of webhooks in version 2.4, it is required starting in 2.6 to support NetBox's caching
 NetBox since the introduction of webhooks in version 2.4, it is required starting in 2.6 to support NetBox's caching
-functionality (as well as other planned features).
+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)
 * `HOST` - Name or IP address of the Redis server (use `localhost` if running locally)
 * `PORT` - TCP port of the Redis service; leave blank for default port (6379)
 * `PORT` - TCP port of the Redis service; leave blank for default port (6379)
 * `PASSWORD` - Redis password (if set)
 * `PASSWORD` - Redis password (if set)
-* `DATABASE` - Numeric database ID for webhooks
-* `CACHE_DATABASE` - Numeric database ID for caching
+* `DATABASE` - Numeric database ID
 * `DEFAULT_TIMEOUT` - Connection timeout in seconds
 * `DEFAULT_TIMEOUT` - Connection timeout in seconds
 * `SSL` - Use SSL connection to Redis
 * `SSL` - Use SSL connection to Redis
 
 
 Example:
 Example:
 
 
-```
+```python
 REDIS = {
 REDIS = {
-    '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:
 !!! 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:
 !!! 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.
 
 
 ---
 ---
 
 

+ 1 - 1
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.
     The installation instructions provided here have been tested to work on Ubuntu 18.04 and CentOS 7.5. The particular commands needed to install dependencies on other distributions may vary significantly. Unfortunately, this is outside the control of the NetBox maintainers. Please consult your distribution's documentation for assistance with any errors.
 
 
 !!! warning
 !!! warning
-    NetBox v2.2 and later requires PostgreSQL 9.4 or higher.
+    NetBox requires PostgreSQL 9.4 or higher.
 
 
 # Installation
 # Installation
 
 

+ 35 - 30
docs/installation/2-netbox.md

@@ -5,14 +5,14 @@ This section of the documentation discusses installing and configuring the NetBo
 **Ubuntu**
 **Ubuntu**
 
 
 ```no-highlight
 ```no-highlight
-# apt-get install -y python3 python3-pip python3-dev build-essential libxml2-dev libxslt1-dev libffi-dev 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**
 **CentOS**
 
 
 ```no-highlight
 ```no-highlight
 # yum install -y epel-release
 # yum install -y epel-release
-# yum install -y gcc python36 python36-devel python36-setuptools libxml2-devel libxslt-devel libffi-devel 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
 # easy_install-3.6 pip
 # ln -s /usr/bin/python3.6 /usr/bin/python3
 # 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
 # pip3 install napalm
 ```
 ```
 
 
+## Remote File Storage (Optional)
+
+By default, NetBox will use the local filesystem to storage uploaded files. To use a remote filesystem, install the [`django-storages`](https://django-storages.readthedocs.io/en/stable/) library and configure your [desired backend](../../configuration/optional-settings/#storage_backend) in `configuration.py`.
+
+```no-highlight
+# pip3 install django-storages
+```
+
 # Configuration
 # Configuration
 
 
 Move into the NetBox configuration directory and make a copy of `configuration.example.py` named `configuration.py`.
 Move into the NetBox configuration directory and make a copy of `configuration.example.py` named `configuration.py`.
@@ -139,13 +147,22 @@ Redis is a in-memory key-value store required as part of the NetBox installation
 
 
 ```python
 ```python
 REDIS = {
 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
 ```no-highlight
 # python3 manage.py collectstatic --no-input
 # 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
 # Test the Application
@@ -237,3 +234,11 @@ Next, connect to the name or IP of the server (as defined in `ALLOWED_HOSTS`) on
 
 
 !!! warning
 !!! warning
     If the test service does not run, or you cannot reach the NetBox home page, something has gone wrong. Do not proceed with the rest of this guide until the installation has been corrected.
     If the test service does not run, or you cannot reach the NetBox home page, something has gone wrong. Do not proceed with the rest of this guide until the installation has been corrected.
+
+Note that the initial UI will be locked down for non-authenticated users.
+
+![NetBox UI as seen by a non-authenticated user](../media/installation/netbox_ui_guest.png)
+
+After logging in as the superuser you created earlier, all areas of the UI will be available.
+
+![NetBox UI as seen by an administrator](../media/installation/netbox_ui_admin.png)

+ 33 - 27
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
 !!! info
     For the sake of brevity, only Ubuntu 18.04 instructions are provided here, but this sort of web server and WSGI configuration is not unique to NetBox. Please consult your distribution's documentation for assistance if needed.
     For the sake of brevity, only Ubuntu 18.04 instructions are provided here, but this sort of web server and WSGI configuration is not unique to NetBox. Please consult your distribution's documentation for assistance if needed.
@@ -107,47 +107,53 @@ Install gunicorn:
 # pip3 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
 ```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
 ```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
 ```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
 !!! 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.

+ 2 - 0
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).
 If you are upgrading from an existing installation, please consult the [upgrading guide](upgrading.md).
 
 
 NetBox v2.5 and later requires Python 3.5 or higher. Please see the instructions for [migrating to Python 3](migrating-to-python3.md) if you are still using Python 2.
 NetBox v2.5 and later requires Python 3.5 or higher. Please see the instructions for [migrating to Python 3](migrating-to-python3.md) if you are still using Python 2.
+
+Netbox v2.5.9 and later moved to using systemd instead of supervisord.  Please see the instructions for [migrating to systemd](migrating-to-systemd.md) if you are still using supervisord.

+ 100 - 0
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
+```

+ 5 - 7
docs/installation/upgrading.md

@@ -84,14 +84,12 @@ This script:
 
 
 # Restart the WSGI Service
 # 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
 ```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.

BIN
docs/media/installation/netbox_ui_admin.png


BIN
docs/media/installation/netbox_ui_guest.png


+ 1 - 1
docs/release-notes/index.md

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

+ 0 - 15
docs/release-notes/version-2.6.md

@@ -1,18 +1,3 @@
-# v2.6.13 (FUTURE)
-
-## Enhancements
-
-* [#3525](https://github.com/netbox-community/netbox/issues/3525) - Enable IP address filtering with multiple address terms
-
-## Bug Fixes
-
-* [#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
-
----
-
 # v2.6.12 (2020-01-13)
 # v2.6.12 (2020-01-13)
 
 
 ## Enhancements
 ## Enhancements

+ 276 - 0
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': '<Key>',
+    'AWS_SECRET_ACCESS_KEY': '<Secret>',
+    'AWS_STORAGE_BUCKET_NAME': 'netbox',
+    'AWS_S3_REGION_NAME': 'eu-west-1',
+}
+```
+
+Thanks to [@steffann](https://github.com/steffann) for contributing this work!
+
+## Changes
+
+### Rack Elevations Rendered via SVG ([#2248](https://github.com/netbox-community/netbox/issues/2248))
+
+NetBox v2.7 introduces a new method of rendering rack elevations as an
+[SVG](https://en.wikipedia.org/wiki/Scalable_Vector_Graphics) via a REST API endpoint. This replaces the prior method of
+rendering elevations using pure HTML which was cumbersome and had several shortcomings. Allowing elevations to be
+rendered as an SVG image in the API allows users to retrieve and make use of the drawings in their own tooling. This
+also opens the door to other feature requests related to rack elevations in the NetBox backlog.
+
+This feature implements a new REST API endpoint:
+
+```
+/api/dcim/racks/<id>/elevation/
+```
+
+By default, this endpoint returns a paginated JSON response representing each rack unit in the given elevation. This is
+the same response returned by the rack units detail endpoint and for this reason the rack units endpoint has been
+deprecated and will be removed in v2.8 (see [#3753](https://github.com/netbox-community/netbox/issues/3753)):
+
+```
+/api/dcim/racks/<id>/units/
+```
+
+In order to render the elevation as an SVG, include the `render=svg` query parameter in the request. You may also
+control the width of the elevation drawing in pixels with `unit_width=<width in pixels>` and the height of each rack
+unit with `unit_height=<height in pixels>`. The `unit_width` defaults to `230` and the `unit_height` default to `20`
+which produces elevations the same size as those that appear in the NetBox Web UI. The query parameter `face` is used to
+request either the `front` or `rear` of the elevation and defaults to `front`.
+
+Here is an example of the request url for an SVG rendering using the default parameters to render the front of the
+elevation:
+
+```
+/api/dcim/racks/<id>/elevation/?render=svg
+```
+
+Here is an example of the request url for an SVG rendering of the rear of the elevation having a width of 300 pixels and
+per unit height of 35 pixels:
+
+```
+/api/dcim/racks/<id>/elevation/?render=svg&face=rear&unit_width=300&unit_height=35
+```
+
+Thanks to [@hellerve](https://github.com/hellerve) for doing the heavy lifting on this!
+
+### Topology Maps Removed ([#2745](https://github.com/netbox-community/netbox/issues/2745))
+
+The topology maps feature has been removed to help focus NetBox development efforts.
+
+### Redis Configuration ([#3282](https://github.com/netbox-community/netbox/issues/3282))
+
+v2.6.0 introduced caching and added the `CACHE_DATABASE` option to the existing `REDIS` database configuration section.
+This did not however, allow for using two different Redis connections for the seperate caching and webhooks features.
+This change separates the Redis connection configurations in the `REDIS` section into distinct `webhooks` and `caching`
+subsections. This requires modification of the `REDIS` section of the `configuration.py` file as follows:
+
+Old Redis configuration:
+```python
+REDIS = {
+    'HOST': 'localhost',
+    'PORT': 6379,
+    'PASSWORD': '',
+    'DATABASE': 0,
+    'CACHE_DATABASE': 1,
+    'DEFAULT_TIMEOUT': 300,
+    'SSL': False,
+}
+```
+
+New Redis configuration:
+```python
+REDIS = {
+    'webhooks': {
+        'HOST': 'redis.example.com',
+        'PORT': 1234,
+        'PASSWORD': 'foobar',
+        'DATABASE': 0,
+        'DEFAULT_TIMEOUT': 300,
+        'SSL': False,
+    },
+    'caching': {
+        'HOST': 'localhost',
+        'PORT': 6379,
+        'PASSWORD': '',
+        'DATABASE': 1,
+        'DEFAULT_TIMEOUT': 300,
+        'SSL': False,
+    }
+}
+```
+
+Note that `CACHE_DATABASE` has been removed and the connection settings have been duplicated for both `webhooks` and
+`caching`. This allows the user to make use of separate Redis instances and/or databases if desired. Full connection
+details are required in both sections, even if they are the same.
+
+### WEBHOOKS_ENABLED Configuration Setting Removed ([#3408](https://github.com/netbox-community/netbox/issues/3408))
+
+As `django-rq` is now a required library, NetBox assumes that the RQ worker process is running. The installation and
+upgrade documentation has been updated to reflect this, and the `WEBHOOKS_ENABLED` configuration parameter is no longer
+used. Please ensure that both the NetBox WSGI service and the RQ worker process are running on all production
+installations.
+
+### API Choice Fields Now Use String Values ([#3569](https://github.com/netbox-community/netbox/issues/3569))
+
+NetBox's REST API presents fields which reference a particular choice as a dictionary with two keys: `value` and
+`label`. In previous versions, `value` was an integer which represented the particular choice in the database. This has
+been changed to a more human-friendly "slug" string, which is essentially a simplified version of the choice's `label`.
+
+For example, The site status field was previously represented as:
+
+```json
+"status": {
+    "value": 1,
+    "label": "Active"
+},
+```
+
+Beginning with v2.7.0, it now looks like this:
+
+```json
+"status": {
+    "value": "active",
+    "label": "Active"
+},
+```
+
+This change allows for much more intuitive representation of values, and obviates the need for API consumers to maintain
+a mapping of static integer values.
+
+Note that that all v2.7 releases will continue to accept the legacy integer values in write requests (POST, PUT, and
+PATCH) to maintain backward compatibility. This behavior will be discontinued beginning in v2.8.0.
+
+## Enhancements
+
+* [#33](https://github.com/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
+  `<app>.<model>`; e.g. `dcim.site`.
+* ipam.Role: Added field `description`
+* secrets.SecretRole: Added field `description`
+* virtualization.Cluster: Added field `tenant`

+ 3 - 3
netbox/circuits/api/serializers.py

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

+ 8 - 8
netbox/circuits/api/views.py

@@ -7,7 +7,7 @@ from circuits import filters
 from circuits.models import Provider, CircuitTermination, CircuitType, Circuit
 from circuits.models import Provider, CircuitTermination, CircuitType, Circuit
 from extras.api.serializers import RenderedGraphSerializer
 from extras.api.serializers import RenderedGraphSerializer
 from extras.api.views import CustomFieldModelViewSet
 from extras.api.views import CustomFieldModelViewSet
-from extras.models import Graph, GRAPH_TYPE_PROVIDER
+from extras.models import Graph
 from utilities.api import FieldChoicesViewSet, ModelViewSet
 from utilities.api import FieldChoicesViewSet, ModelViewSet
 from . import serializers
 from . import serializers
 
 
@@ -18,8 +18,8 @@ from . import serializers
 
 
 class CircuitsFieldChoicesViewSet(FieldChoicesViewSet):
 class CircuitsFieldChoicesViewSet(FieldChoicesViewSet):
     fields = (
     fields = (
-        (Circuit, ['status']),
-        (CircuitTermination, ['term_side']),
+        (serializers.CircuitSerializer, ['status']),
+        (serializers.CircuitTerminationSerializer, ['term_side']),
     )
     )
 
 
 
 
@@ -32,7 +32,7 @@ class ProviderViewSet(CustomFieldModelViewSet):
         circuit_count=Count('circuits')
         circuit_count=Count('circuits')
     )
     )
     serializer_class = serializers.ProviderSerializer
     serializer_class = serializers.ProviderSerializer
-    filterset_class = filters.ProviderFilter
+    filterset_class = filters.ProviderFilterSet
 
 
     @action(detail=True)
     @action(detail=True)
     def graphs(self, request, pk):
     def graphs(self, request, pk):
@@ -40,7 +40,7 @@ class ProviderViewSet(CustomFieldModelViewSet):
         A convenience method for rendering graphs for a particular provider.
         A convenience method for rendering graphs for a particular provider.
         """
         """
         provider = get_object_or_404(Provider, pk=pk)
         provider = get_object_or_404(Provider, pk=pk)
-        queryset = Graph.objects.filter(type=GRAPH_TYPE_PROVIDER)
+        queryset = Graph.objects.filter(type__model='provider')
         serializer = RenderedGraphSerializer(queryset, many=True, context={'graphed_object': provider})
         serializer = RenderedGraphSerializer(queryset, many=True, context={'graphed_object': provider})
         return Response(serializer.data)
         return Response(serializer.data)
 
 
@@ -54,7 +54,7 @@ class CircuitTypeViewSet(ModelViewSet):
         circuit_count=Count('circuits')
         circuit_count=Count('circuits')
     )
     )
     serializer_class = serializers.CircuitTypeSerializer
     serializer_class = serializers.CircuitTypeSerializer
-    filterset_class = filters.CircuitTypeFilter
+    filterset_class = filters.CircuitTypeFilterSet
 
 
 
 
 #
 #
@@ -64,7 +64,7 @@ class CircuitTypeViewSet(ModelViewSet):
 class CircuitViewSet(CustomFieldModelViewSet):
 class CircuitViewSet(CustomFieldModelViewSet):
     queryset = Circuit.objects.prefetch_related('type', 'tenant', 'provider').prefetch_related('tags')
     queryset = Circuit.objects.prefetch_related('type', 'tenant', 'provider').prefetch_related('tags')
     serializer_class = serializers.CircuitSerializer
     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'
         'circuit', 'site', 'connected_endpoint__device', 'cable'
     )
     )
     serializer_class = serializers.CircuitTerminationSerializer
     serializer_class = serializers.CircuitTerminationSerializer
-    filterset_class = filters.CircuitTerminationFilter
+    filterset_class = filters.CircuitTerminationFilterSet

+ 48 - 0
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')
+    )

+ 0 - 23
netbox/circuits/constants.py

@@ -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'),
-)

+ 11 - 11
netbox/circuits/filters.py

@@ -3,20 +3,20 @@ from django.db.models import Q
 
 
 from dcim.models import Region, Site
 from dcim.models import Region, Site
 from extras.filters import CustomFieldFilterSet, CreatedUpdatedFilterSet
 from extras.filters import CustomFieldFilterSet, CreatedUpdatedFilterSet
-from tenancy.filtersets import TenancyFilterSet
+from tenancy.filters import TenancyFilterSet
 from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter
 from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter
-from .constants import *
+from .choices import *
 from .models import Circuit, CircuitTermination, CircuitType, Provider
 from .models import Circuit, CircuitTermination, CircuitType, Provider
 
 
 __all__ = (
 __all__ = (
-    'CircuitFilter',
-    'CircuitTerminationFilter',
-    'CircuitTypeFilter',
-    'ProviderFilter',
+    'CircuitFilterSet',
+    'CircuitTerminationFilterSet',
+    'CircuitTypeFilterSet',
+    'ProviderFilterSet',
 )
 )
 
 
 
 
-class ProviderFilter(CustomFieldFilterSet, CreatedUpdatedFilterSet):
+class ProviderFilterSet(CustomFieldFilterSet, CreatedUpdatedFilterSet):
     id__in = NumericInFilter(
     id__in = NumericInFilter(
         field_name='id',
         field_name='id',
         lookup_expr='in'
         lookup_expr='in'
@@ -65,14 +65,14 @@ class ProviderFilter(CustomFieldFilterSet, CreatedUpdatedFilterSet):
         )
         )
 
 
 
 
-class CircuitTypeFilter(NameSlugSearchFilterSet):
+class CircuitTypeFilterSet(NameSlugSearchFilterSet):
 
 
     class Meta:
     class Meta:
         model = CircuitType
         model = CircuitType
         fields = ['id', 'name', 'slug']
         fields = ['id', 'name', 'slug']
 
 
 
 
-class CircuitFilter(CustomFieldFilterSet, TenancyFilterSet, CreatedUpdatedFilterSet):
+class CircuitFilterSet(CustomFieldFilterSet, TenancyFilterSet, CreatedUpdatedFilterSet):
     id__in = NumericInFilter(
     id__in = NumericInFilter(
         field_name='id',
         field_name='id',
         lookup_expr='in'
         lookup_expr='in'
@@ -102,7 +102,7 @@ class CircuitFilter(CustomFieldFilterSet, TenancyFilterSet, CreatedUpdatedFilter
         label='Circuit type (slug)',
         label='Circuit type (slug)',
     )
     )
     status = django_filters.MultipleChoiceFilter(
     status = django_filters.MultipleChoiceFilter(
-        choices=CIRCUIT_STATUS_CHOICES,
+        choices=CircuitStatusChoices,
         null_value=None
         null_value=None
     )
     )
     site_id = django_filters.ModelMultipleChoiceFilter(
     site_id = django_filters.ModelMultipleChoiceFilter(
@@ -146,7 +146,7 @@ class CircuitFilter(CustomFieldFilterSet, TenancyFilterSet, CreatedUpdatedFilter
         ).distinct()
         ).distinct()
 
 
 
 
-class CircuitTerminationFilter(django_filters.FilterSet):
+class CircuitTerminationFilterSet(django_filters.FilterSet):
     q = django_filters.CharFilter(
     q = django_filters.CharFilter(
         method='search',
         method='search',
         label='Search',
         label='Search',

+ 0 - 26
netbox/circuits/fixtures/initial_data.json

@@ -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"
-    }
-}
-]

+ 5 - 5
netbox/circuits/forms.py

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

+ 74 - 34
netbox/circuits/migrations/0001_initial_squashed_0010_circuit_status.py → netbox/circuits/migrations/0001_initial_squashed_0006_terminations.py

@@ -1,46 +1,61 @@
-# -*- 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
 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):
 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 = [
     dependencies = [
+        ('tenancy', '0001_initial'),
         ('dcim', '0001_initial'),
         ('dcim', '0001_initial'),
         ('dcim', '0022_color_names_to_rgb'),
         ('dcim', '0022_color_names_to_rgb'),
-        ('tenancy', '0001_initial'),
     ]
     ]
 
 
     operations = [
     operations = [
         migrations.CreateModel(
         migrations.CreateModel(
-            name='Provider',
+            name='CircuitType',
             fields=[
             fields=[
                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                 ('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)),
                 ('name', models.CharField(max_length=50, unique=True)),
                 ('slug', models.SlugField(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={
             options={
                 'ordering': ['name'],
                 'ordering': ['name'],
             },
             },
         ),
         ),
         migrations.CreateModel(
         migrations.CreateModel(
-            name='CircuitType',
+            name='Provider',
             fields=[
             fields=[
                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                 ('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)),
                 ('name', models.CharField(max_length=50, unique=True)),
                 ('slug', models.SlugField(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={
             options={
                 'ordering': ['name'],
                 'ordering': ['name'],
@@ -52,43 +67,68 @@ class Migration(migrations.Migration):
                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                 ('created', models.DateField(auto_now_add=True)),
                 ('created', models.DateField(auto_now_add=True)),
                 ('last_updated', models.DateTimeField(auto_now=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)),
                 ('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')),
                 ('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')),
                 ('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')),
                 ('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={
             options={
                 'ordering': ['provider', 'cid'],
                 'ordering': ['provider', 'cid'],
+                'unique_together': {('provider', 'cid')},
             },
             },
         ),
         ),
-        migrations.AlterUniqueTogether(
-            name='circuit',
-            unique_together=set([('provider', 'cid')]),
-        ),
         migrations.CreateModel(
         migrations.CreateModel(
             name='CircuitTermination',
             name='CircuitTermination',
             fields=[
             fields=[
                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                 ('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')),
                 ('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')),
                 ('site', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='circuit_terminations', to='dcim.Site')),
             ],
             ],
             options={
             options={
                 'ordering': ['circuit', 'term_side'],
                 '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',
         ),
         ),
     ]
     ]

+ 254 - 0
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),
+        ),
+    ]

+ 1 - 1
netbox/circuits/migrations/0013_cables.py

@@ -3,7 +3,7 @@ import sys
 from django.db import migrations, models
 from django.db import migrations, models
 import django.db.models.deletion
 import django.db.models.deletion
 
 
-from dcim.constants import CONNECTION_STATUS_CONNECTED
+CONNECTION_STATUS_CONNECTED = True
 
 
 
 
 def circuit_terminations_to_cables(apps, schema_editor):
 def circuit_terminations_to_cables(apps, schema_editor):

+ 39 - 0
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
+        ),
+
+    ]

+ 18 - 0
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),
+        ),
+    ]

+ 40 - 9
netbox/circuits/models.py

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

+ 4 - 2
netbox/circuits/tables.py

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

+ 60 - 18
netbox/circuits/tests/test_api.py

@@ -1,12 +1,35 @@
+from django.contrib.contenttypes.models import ContentType
 from django.urls import reverse
 from django.urls import reverse
 from rest_framework import status
 from rest_framework import status
 
 
-from circuits.constants import CIRCUIT_STATUS_ACTIVE, TERM_SIDE_A, TERM_SIDE_Z
+from circuits.choices import *
 from circuits.models import Circuit, CircuitTermination, CircuitType, Provider
 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 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):
 class ProviderTest(APITestCase):
@@ -28,16 +51,20 @@ class ProviderTest(APITestCase):
 
 
     def test_get_provider_graphs(self):
     def test_get_provider_graphs(self):
 
 
+        provider_ct = ContentType.objects.get(app_label='circuits', model='provider')
         self.graph1 = Graph.objects.create(
         self.graph1 = Graph.objects.create(
-            type=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'
             source='http://example.com/graphs.py?provider={{ obj.slug }}&foo=1'
         )
         )
         self.graph2 = Graph.objects.create(
         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'
             source='http://example.com/graphs.py?provider={{ obj.slug }}&foo=2'
         )
         )
         self.graph3 = Graph.objects.create(
         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'
             source='http://example.com/graphs.py?provider={{ obj.slug }}&foo=3'
         )
         )
 
 
@@ -250,7 +277,7 @@ class CircuitTest(APITestCase):
             'cid': 'TEST0004',
             'cid': 'TEST0004',
             'provider': self.provider1.pk,
             'provider': self.provider1.pk,
             'type': self.circuittype1.pk,
             'type': self.circuittype1.pk,
-            'status': CIRCUIT_STATUS_ACTIVE,
+            'status': CircuitStatusChoices.STATUS_ACTIVE,
         }
         }
 
 
         url = reverse('circuits-api:circuit-list')
         url = reverse('circuits-api:circuit-list')
@@ -270,19 +297,19 @@ class CircuitTest(APITestCase):
                 'cid': 'TEST0004',
                 'cid': 'TEST0004',
                 'provider': self.provider1.pk,
                 'provider': self.provider1.pk,
                 'type': self.circuittype1.pk,
                 'type': self.circuittype1.pk,
-                'status': CIRCUIT_STATUS_ACTIVE,
+                'status': CircuitStatusChoices.STATUS_ACTIVE,
             },
             },
             {
             {
                 'cid': 'TEST0005',
                 'cid': 'TEST0005',
                 'provider': self.provider1.pk,
                 'provider': self.provider1.pk,
                 'type': self.circuittype1.pk,
                 'type': self.circuittype1.pk,
-                'status': CIRCUIT_STATUS_ACTIVE,
+                'status': CircuitStatusChoices.STATUS_ACTIVE,
             },
             },
             {
             {
                 'cid': 'TEST0006',
                 'cid': 'TEST0006',
                 'provider': self.provider1.pk,
                 'provider': self.provider1.pk,
                 'type': self.circuittype1.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.circuit2 = Circuit.objects.create(cid='TEST0002', provider=provider, type=circuittype)
         self.circuit3 = Circuit.objects.create(cid='TEST0003', provider=provider, type=circuittype)
         self.circuit3 = Circuit.objects.create(cid='TEST0003', provider=provider, type=circuittype)
         self.circuittermination1 = CircuitTermination.objects.create(
         self.circuittermination1 = CircuitTermination.objects.create(
-            circuit=self.circuit1, 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(
         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(
         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(
         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):
     def test_get_circuittermination(self):
@@ -366,7 +405,7 @@ class CircuitTerminationTest(APITestCase):
 
 
         data = {
         data = {
             'circuit': self.circuit3.pk,
             'circuit': self.circuit3.pk,
-            'term_side': TERM_SIDE_A,
+            'term_side': CircuitTerminationSideChoices.SIDE_A,
             'site': self.site1.pk,
             'site': self.site1.pk,
             'port_speed': 1000000,
             'port_speed': 1000000,
         }
         }
@@ -385,12 +424,15 @@ class CircuitTerminationTest(APITestCase):
     def test_update_circuittermination(self):
     def test_update_circuittermination(self):
 
 
         circuittermination5 = CircuitTermination.objects.create(
         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 = {
         data = {
             'circuit': self.circuit3.pk,
             'circuit': self.circuit3.pk,
-            'term_side': TERM_SIDE_Z,
+            'term_side': CircuitTerminationSideChoices.SIDE_Z,
             'site': self.site2.pk,
             'site': self.site2.pk,
             'port_speed': 1000000,
             'port_speed': 1000000,
         }
         }

+ 12 - 12
netbox/circuits/tests/test_filters.py

@@ -1,6 +1,6 @@
 from django.test import TestCase
 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.filters import *
 from circuits.models import Circuit, CircuitTermination, CircuitType, Provider
 from circuits.models import Circuit, CircuitTermination, CircuitType, Provider
 from dcim.models import Region, Site
 from dcim.models import Region, Site
@@ -8,7 +8,7 @@ from dcim.models import Region, Site
 
 
 class ProviderTestCase(TestCase):
 class ProviderTestCase(TestCase):
     queryset = Provider.objects.all()
     queryset = Provider.objects.all()
-    filterset = ProviderFilter
+    filterset = ProviderFilterSet
 
 
     @classmethod
     @classmethod
     def setUpTestData(cls):
     def setUpTestData(cls):
@@ -91,7 +91,7 @@ class ProviderTestCase(TestCase):
 
 
 class CircuitTypeTestCase(TestCase):
 class CircuitTypeTestCase(TestCase):
     queryset = CircuitType.objects.all()
     queryset = CircuitType.objects.all()
-    filterset = CircuitTypeFilter
+    filterset = CircuitTypeFilterSet
 
 
     @classmethod
     @classmethod
     def setUpTestData(cls):
     def setUpTestData(cls):
@@ -117,7 +117,7 @@ class CircuitTypeTestCase(TestCase):
 
 
 class CircuitTestCase(TestCase):
 class CircuitTestCase(TestCase):
     queryset = Circuit.objects.all()
     queryset = Circuit.objects.all()
-    filterset = CircuitFilter
+    filterset = CircuitFilterSet
 
 
     @classmethod
     @classmethod
     def setUpTestData(cls):
     def setUpTestData(cls):
@@ -151,12 +151,12 @@ class CircuitTestCase(TestCase):
         Provider.objects.bulk_create(providers)
         Provider.objects.bulk_create(providers)
 
 
         circuits = (
         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)
         Circuit.objects.bulk_create(circuits)
 
 
@@ -199,7 +199,7 @@ class CircuitTestCase(TestCase):
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
 
 
     def test_status(self):
     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)
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
 
 
     def test_region(self):
     def test_region(self):
@@ -219,7 +219,7 @@ class CircuitTestCase(TestCase):
 
 
 class CircuitTerminationTestCase(TestCase):
 class CircuitTerminationTestCase(TestCase):
     queryset = CircuitTermination.objects.all()
     queryset = CircuitTermination.objects.all()
-    filterset = CircuitTerminationFilter
+    filterset = CircuitTerminationFilterSet
 
 
     @classmethod
     @classmethod
     def setUpTestData(cls):
     def setUpTestData(cls):

+ 60 - 3
netbox/circuits/tests/test_views.py

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

+ 19 - 15
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.views.generic import View
 from django_tables2 import RequestConfig
 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.forms import ConfirmationForm
 from utilities.paginator import EnhancedPaginator
 from utilities.paginator import EnhancedPaginator
 from utilities.views import (
 from utilities.views import (
     BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
     BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
 )
 )
 from . import filters, forms, tables
 from . import filters, forms, tables
-from .constants import TERM_SIDE_A, TERM_SIDE_Z
+from .choices import CircuitTerminationSideChoices
 from .models import Circuit, CircuitTermination, CircuitType, Provider
 from .models import Circuit, CircuitTermination, CircuitType, Provider
 
 
 
 
@@ -26,8 +26,8 @@ from .models import Circuit, CircuitTermination, CircuitType, Provider
 class ProviderListView(PermissionRequiredMixin, ObjectListView):
 class ProviderListView(PermissionRequiredMixin, ObjectListView):
     permission_required = 'circuits.view_provider'
     permission_required = 'circuits.view_provider'
     queryset = Provider.objects.annotate(count_circuits=Count('circuits'))
     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
     table = tables.ProviderDetailTable
     template_name = 'circuits/provider_list.html'
     template_name = 'circuits/provider_list.html'
 
 
@@ -39,7 +39,7 @@ class ProviderView(PermissionRequiredMixin, View):
 
 
         provider = get_object_or_404(Provider, slug=slug)
         provider = get_object_or_404(Provider, slug=slug)
         circuits = Circuit.objects.filter(provider=provider).prefetch_related('type', 'tenant', 'terminations__site')
         circuits = Circuit.objects.filter(provider=provider).prefetch_related('type', 'tenant', 'terminations__site')
-        show_graphs = Graph.objects.filter(type=GRAPH_TYPE_PROVIDER).exists()
+        show_graphs = Graph.objects.filter(type__model='provider').exists()
 
 
         circuits_table = tables.CircuitTable(circuits, orderable=False)
         circuits_table = tables.CircuitTable(circuits, orderable=False)
         circuits_table.columns.hide('provider')
         circuits_table.columns.hide('provider')
@@ -85,7 +85,7 @@ class ProviderBulkImportView(PermissionRequiredMixin, BulkImportView):
 class ProviderBulkEditView(PermissionRequiredMixin, BulkEditView):
 class ProviderBulkEditView(PermissionRequiredMixin, BulkEditView):
     permission_required = 'circuits.change_provider'
     permission_required = 'circuits.change_provider'
     queryset = Provider.objects.all()
     queryset = Provider.objects.all()
-    filter = filters.ProviderFilter
+    filterset = filters.ProviderFilterSet
     table = tables.ProviderTable
     table = tables.ProviderTable
     form = forms.ProviderBulkEditForm
     form = forms.ProviderBulkEditForm
     default_return_url = 'circuits:provider_list'
     default_return_url = 'circuits:provider_list'
@@ -94,7 +94,7 @@ class ProviderBulkEditView(PermissionRequiredMixin, BulkEditView):
 class ProviderBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 class ProviderBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
     permission_required = 'circuits.delete_provider'
     permission_required = 'circuits.delete_provider'
     queryset = Provider.objects.all()
     queryset = Provider.objects.all()
-    filter = filters.ProviderFilter
+    filterset = filters.ProviderFilterSet
     table = tables.ProviderTable
     table = tables.ProviderTable
     default_return_url = 'circuits:provider_list'
     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]),
         a_side=Subquery(_terminations.filter(term_side='A').values('site__name')[:1]),
         z_side=Subquery(_terminations.filter(term_side='Z').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
     table = tables.CircuitTable
     template_name = 'circuits/circuit_list.html'
     template_name = 'circuits/circuit_list.html'
 
 
@@ -163,12 +163,12 @@ class CircuitView(PermissionRequiredMixin, View):
         termination_a = CircuitTermination.objects.prefetch_related(
         termination_a = CircuitTermination.objects.prefetch_related(
             'site__region', 'connected_endpoint__device'
             'site__region', 'connected_endpoint__device'
         ).filter(
         ).filter(
-            circuit=circuit, term_side=TERM_SIDE_A
+            circuit=circuit, term_side=CircuitTerminationSideChoices.SIDE_A
         ).first()
         ).first()
         termination_z = CircuitTermination.objects.prefetch_related(
         termination_z = CircuitTermination.objects.prefetch_related(
             'site__region', 'connected_endpoint__device'
             'site__region', 'connected_endpoint__device'
         ).filter(
         ).filter(
-            circuit=circuit, term_side=TERM_SIDE_Z
+            circuit=circuit, term_side=CircuitTerminationSideChoices.SIDE_Z
         ).first()
         ).first()
 
 
         return render(request, 'circuits/circuit.html', {
         return render(request, 'circuits/circuit.html', {
@@ -206,7 +206,7 @@ class CircuitBulkImportView(PermissionRequiredMixin, BulkImportView):
 class CircuitBulkEditView(PermissionRequiredMixin, BulkEditView):
 class CircuitBulkEditView(PermissionRequiredMixin, BulkEditView):
     permission_required = 'circuits.change_circuit'
     permission_required = 'circuits.change_circuit'
     queryset = Circuit.objects.prefetch_related('provider', 'type', 'tenant').prefetch_related('terminations__site')
     queryset = Circuit.objects.prefetch_related('provider', 'type', 'tenant').prefetch_related('terminations__site')
-    filter = filters.CircuitFilter
+    filterset = filters.CircuitFilterSet
     table = tables.CircuitTable
     table = tables.CircuitTable
     form = forms.CircuitBulkEditForm
     form = forms.CircuitBulkEditForm
     default_return_url = 'circuits:circuit_list'
     default_return_url = 'circuits:circuit_list'
@@ -215,7 +215,7 @@ class CircuitBulkEditView(PermissionRequiredMixin, BulkEditView):
 class CircuitBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 class CircuitBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
     permission_required = 'circuits.delete_circuit'
     permission_required = 'circuits.delete_circuit'
     queryset = Circuit.objects.prefetch_related('provider', 'type', 'tenant').prefetch_related('terminations__site')
     queryset = Circuit.objects.prefetch_related('provider', 'type', 'tenant').prefetch_related('terminations__site')
-    filter = filters.CircuitFilter
+    filterset = filters.CircuitFilterSet
     table = tables.CircuitTable
     table = tables.CircuitTable
     default_return_url = 'circuits:circuit_list'
     default_return_url = 'circuits:circuit_list'
 
 
@@ -224,8 +224,12 @@ class CircuitBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 def circuit_terminations_swap(request, pk):
 def circuit_terminations_swap(request, pk):
 
 
     circuit = get_object_or_404(Circuit, pk=pk)
     circuit = get_object_or_404(Circuit, pk=pk)
-    termination_a = CircuitTermination.objects.filter(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:
     if not termination_a and not termination_z:
         messages.error(request, "No terminations have been defined for circuit {}.".format(circuit))
         messages.error(request, "No terminations have been defined for circuit {}.".format(circuit))
         return redirect('circuits:circuit', pk=circuit.pk)
         return redirect('circuits:circuit', pk=circuit.pk)

+ 106 - 50
netbox/dcim/api/serializers.py

@@ -4,6 +4,7 @@ from rest_framework import serializers
 from rest_framework.validators import UniqueTogetherValidator
 from rest_framework.validators import UniqueTogetherValidator
 from taggit_serializer.serializers import TaggitSerializer, TagListSerializerField
 from taggit_serializer.serializers import TaggitSerializer, TagListSerializerField
 
 
+from dcim.choices import *
 from dcim.constants import *
 from dcim.constants import *
 from dcim.models import (
 from dcim.models import (
     Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
     Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
@@ -67,7 +68,7 @@ class RegionSerializer(serializers.ModelSerializer):
 
 
 
 
 class SiteSerializer(TaggitSerializer, CustomFieldModelSerializer):
 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)
     region = NestedRegionSerializer(required=False, allow_null=True)
     tenant = NestedTenantSerializer(required=False, allow_null=True)
     tenant = NestedTenantSerializer(required=False, allow_null=True)
     time_zone = TimeZoneField(required=False)
     time_zone = TimeZoneField(required=False)
@@ -107,18 +108,18 @@ class RackRoleSerializer(ValidatedModelSerializer):
 
 
     class Meta:
     class Meta:
         model = RackRole
         model = RackRole
-        fields = ['id', 'name', 'slug', 'color', 'rack_count']
+        fields = ['id', 'name', 'slug', 'color', 'description', 'rack_count']
 
 
 
 
 class RackSerializer(TaggitSerializer, CustomFieldModelSerializer):
 class RackSerializer(TaggitSerializer, CustomFieldModelSerializer):
     site = NestedSiteSerializer()
     site = NestedSiteSerializer()
     group = NestedRackGroupSerializer(required=False, allow_null=True, default=None)
     group = NestedRackGroupSerializer(required=False, allow_null=True, default=None)
     tenant = NestedTenantSerializer(required=False, allow_null=True)
     tenant = NestedTenantSerializer(required=False, allow_null=True)
-    status = ChoiceField(choices=RACK_STATUS_CHOICES, required=False)
+    status = ChoiceField(choices=RackStatusChoices, required=False)
     role = NestedRackRoleSerializer(required=False, allow_null=True)
     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)
     tags = TagListSerializerField(required=False)
     device_count = serializers.IntegerField(read_only=True)
     device_count = serializers.IntegerField(read_only=True)
     powerfeed_count = serializers.IntegerField(read_only=True)
     powerfeed_count = serializers.IntegerField(read_only=True)
@@ -156,7 +157,7 @@ class RackUnitSerializer(serializers.Serializer):
     """
     """
     id = serializers.IntegerField(read_only=True)
     id = serializers.IntegerField(read_only=True)
     name = serializers.CharField(read_only=True)
     name = serializers.CharField(read_only=True)
-    face = serializers.IntegerField(read_only=True)
+    face = ChoiceField(choices=DeviceFaceChoices, read_only=True)
     device = NestedDeviceSerializer(read_only=True)
     device = NestedDeviceSerializer(read_only=True)
 
 
 
 
@@ -170,6 +171,31 @@ class RackReservationSerializer(ValidatedModelSerializer):
         fields = ['id', 'rack', 'units', 'created', 'user', 'tenant', 'description']
         fields = ['id', 'rack', 'units', 'created', 'user', 'tenant', 'description']
 
 
 
 
+class RackElevationDetailFilterSerializer(serializers.Serializer):
+    face = serializers.ChoiceField(
+        choices=DeviceFaceChoices,
+        default=DeviceFaceChoices.FACE_FRONT
+    )
+    render = serializers.ChoiceField(
+        choices=RackElevationDetailRenderChoices,
+        default=RackElevationDetailRenderChoices.RENDER_JSON
+    )
+    unit_width = serializers.IntegerField(
+        default=RACK_ELEVATION_UNIT_WIDTH_DEFAULT
+    )
+    unit_height = serializers.IntegerField(
+        default=RACK_ELEVATION_UNIT_HEIGHT_DEFAULT
+    )
+    exclude = serializers.IntegerField(
+        required=False,
+        default=None
+    )
+    expand_devices = serializers.BooleanField(
+        required=False,
+        default=True
+    )
+
+
 #
 #
 # Device types
 # Device types
 #
 #
@@ -186,7 +212,7 @@ class ManufacturerSerializer(ValidatedModelSerializer):
 
 
 class DeviceTypeSerializer(TaggitSerializer, CustomFieldModelSerializer):
 class DeviceTypeSerializer(TaggitSerializer, CustomFieldModelSerializer):
     manufacturer = NestedManufacturerSerializer()
     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)
     tags = TagListSerializerField(required=False)
     device_count = serializers.IntegerField(read_only=True)
     device_count = serializers.IntegerField(read_only=True)
 
 
@@ -200,58 +226,72 @@ class DeviceTypeSerializer(TaggitSerializer, CustomFieldModelSerializer):
 
 
 class ConsolePortTemplateSerializer(ValidatedModelSerializer):
 class ConsolePortTemplateSerializer(ValidatedModelSerializer):
     device_type = NestedDeviceTypeSerializer()
     device_type = NestedDeviceTypeSerializer()
+    type = ChoiceField(
+        choices=ConsolePortTypeChoices,
+        required=False
+    )
 
 
     class Meta:
     class Meta:
         model = ConsolePortTemplate
         model = ConsolePortTemplate
-        fields = ['id', 'device_type', 'name']
+        fields = ['id', 'device_type', 'name', 'type']
 
 
 
 
 class ConsoleServerPortTemplateSerializer(ValidatedModelSerializer):
 class ConsoleServerPortTemplateSerializer(ValidatedModelSerializer):
     device_type = NestedDeviceTypeSerializer()
     device_type = NestedDeviceTypeSerializer()
+    type = ChoiceField(
+        choices=ConsolePortTypeChoices,
+        required=False
+    )
 
 
     class Meta:
     class Meta:
         model = ConsoleServerPortTemplate
         model = ConsoleServerPortTemplate
-        fields = ['id', 'device_type', 'name']
+        fields = ['id', 'device_type', 'name', 'type']
 
 
 
 
 class PowerPortTemplateSerializer(ValidatedModelSerializer):
 class PowerPortTemplateSerializer(ValidatedModelSerializer):
     device_type = NestedDeviceTypeSerializer()
     device_type = NestedDeviceTypeSerializer()
+    type = ChoiceField(
+        choices=PowerPortTypeChoices,
+        required=False
+    )
 
 
     class Meta:
     class Meta:
         model = PowerPortTemplate
         model = PowerPortTemplate
-        fields = ['id', 'device_type', 'name', 'maximum_draw', 'allocated_draw']
+        fields = ['id', 'device_type', 'name', 'type', 'maximum_draw', 'allocated_draw']
 
 
 
 
 class PowerOutletTemplateSerializer(ValidatedModelSerializer):
 class PowerOutletTemplateSerializer(ValidatedModelSerializer):
     device_type = NestedDeviceTypeSerializer()
     device_type = NestedDeviceTypeSerializer()
+    type = ChoiceField(
+        choices=PowerOutletTypeChoices,
+        required=False
+    )
     power_port = PowerPortTemplateSerializer(
     power_port = PowerPortTemplateSerializer(
         required=False
         required=False
     )
     )
     feed_leg = ChoiceField(
     feed_leg = ChoiceField(
-        choices=POWERFEED_LEG_CHOICES,
+        choices=PowerOutletFeedLegChoices,
         required=False,
         required=False,
         allow_null=True
         allow_null=True
     )
     )
 
 
     class Meta:
     class Meta:
         model = PowerOutletTemplate
         model = PowerOutletTemplate
-        fields = ['id', 'device_type', 'name', 'power_port', 'feed_leg']
+        fields = ['id', 'device_type', 'name', 'type', 'power_port', 'feed_leg']
 
 
 
 
 class InterfaceTemplateSerializer(ValidatedModelSerializer):
 class InterfaceTemplateSerializer(ValidatedModelSerializer):
     device_type = NestedDeviceTypeSerializer()
     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:
     class Meta:
         model = InterfaceTemplate
         model = InterfaceTemplate
-        fields = ['id', 'device_type', 'name', 'type', 'form_factor', 'mgmt_only']
+        fields = ['id', 'device_type', 'name', 'type', 'mgmt_only']
 
 
 
 
 class RearPortTemplateSerializer(ValidatedModelSerializer):
 class RearPortTemplateSerializer(ValidatedModelSerializer):
     device_type = NestedDeviceTypeSerializer()
     device_type = NestedDeviceTypeSerializer()
-    type = ChoiceField(choices=PORT_TYPE_CHOICES)
+    type = ChoiceField(choices=PortTypeChoices)
 
 
     class Meta:
     class Meta:
         model = RearPortTemplate
         model = RearPortTemplate
@@ -260,7 +300,7 @@ class RearPortTemplateSerializer(ValidatedModelSerializer):
 
 
 class FrontPortTemplateSerializer(ValidatedModelSerializer):
 class FrontPortTemplateSerializer(ValidatedModelSerializer):
     device_type = NestedDeviceTypeSerializer()
     device_type = NestedDeviceTypeSerializer()
-    type = ChoiceField(choices=PORT_TYPE_CHOICES)
+    type = ChoiceField(choices=PortTypeChoices)
     rear_port = NestedRearPortTemplateSerializer()
     rear_port = NestedRearPortTemplateSerializer()
 
 
     class Meta:
     class Meta:
@@ -286,7 +326,9 @@ class DeviceRoleSerializer(ValidatedModelSerializer):
 
 
     class Meta:
     class Meta:
         model = DeviceRole
         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):
 class PlatformSerializer(ValidatedModelSerializer):
@@ -309,8 +351,8 @@ class DeviceSerializer(TaggitSerializer, CustomFieldModelSerializer):
     platform = NestedPlatformSerializer(required=False, allow_null=True)
     platform = NestedPlatformSerializer(required=False, allow_null=True)
     site = NestedSiteSerializer()
     site = NestedSiteSerializer()
     rack = NestedRackSerializer(required=False, allow_null=True)
     rack = NestedRackSerializer(required=False, allow_null=True)
-    face = ChoiceField(choices=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_ip = NestedIPAddressSerializer(read_only=True)
     primary_ip4 = NestedIPAddressSerializer(required=False, allow_null=True)
     primary_ip4 = NestedIPAddressSerializer(required=False, allow_null=True)
     primary_ip6 = NestedIPAddressSerializer(required=False, allow_null=True)
     primary_ip6 = NestedIPAddressSerializer(required=False, allow_null=True)
@@ -376,37 +418,49 @@ class DeviceNAPALMSerializer(serializers.Serializer):
 
 
 class ConsoleServerPortSerializer(TaggitSerializer, ConnectedEndpointSerializer):
 class ConsoleServerPortSerializer(TaggitSerializer, ConnectedEndpointSerializer):
     device = NestedDeviceSerializer()
     device = NestedDeviceSerializer()
+    type = ChoiceField(
+        choices=ConsolePortTypeChoices,
+        required=False
+    )
     cable = NestedCableSerializer(read_only=True)
     cable = NestedCableSerializer(read_only=True)
     tags = TagListSerializerField(required=False)
     tags = TagListSerializerField(required=False)
 
 
     class Meta:
     class Meta:
         model = ConsoleServerPort
         model = ConsoleServerPort
         fields = [
         fields = [
-            'id', 'device', 'name', '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):
 class ConsolePortSerializer(TaggitSerializer, ConnectedEndpointSerializer):
     device = NestedDeviceSerializer()
     device = NestedDeviceSerializer()
+    type = ChoiceField(
+        choices=ConsolePortTypeChoices,
+        required=False
+    )
     cable = NestedCableSerializer(read_only=True)
     cable = NestedCableSerializer(read_only=True)
     tags = TagListSerializerField(required=False)
     tags = TagListSerializerField(required=False)
 
 
     class Meta:
     class Meta:
         model = ConsolePort
         model = ConsolePort
         fields = [
         fields = [
-            'id', 'device', 'name', '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):
 class PowerOutletSerializer(TaggitSerializer, ConnectedEndpointSerializer):
     device = NestedDeviceSerializer()
     device = NestedDeviceSerializer()
+    type = ChoiceField(
+        choices=PowerOutletTypeChoices,
+        required=False
+    )
     power_port = NestedPowerPortSerializer(
     power_port = NestedPowerPortSerializer(
         required=False
         required=False
     )
     )
     feed_leg = ChoiceField(
     feed_leg = ChoiceField(
-        choices=POWERFEED_LEG_CHOICES,
+        choices=PowerOutletFeedLegChoices,
         required=False,
         required=False,
         allow_null=True
         allow_null=True
     )
     )
@@ -420,31 +474,33 @@ class PowerOutletSerializer(TaggitSerializer, ConnectedEndpointSerializer):
     class Meta:
     class Meta:
         model = PowerOutlet
         model = PowerOutlet
         fields = [
         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',
             'connected_endpoint', 'connection_status', 'cable', 'tags',
         ]
         ]
 
 
 
 
 class PowerPortSerializer(TaggitSerializer, ConnectedEndpointSerializer):
 class PowerPortSerializer(TaggitSerializer, ConnectedEndpointSerializer):
     device = NestedDeviceSerializer()
     device = NestedDeviceSerializer()
+    type = ChoiceField(
+        choices=PowerPortTypeChoices,
+        required=False
+    )
     cable = NestedCableSerializer(read_only=True)
     cable = NestedCableSerializer(read_only=True)
     tags = TagListSerializerField(required=False)
     tags = TagListSerializerField(required=False)
 
 
     class Meta:
     class Meta:
         model = PowerPort
         model = PowerPort
         fields = [
         fields = [
-            'id', 'device', 'name', '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',
             'connected_endpoint', 'connection_status', 'cable', 'tags',
         ]
         ]
 
 
 
 
 class InterfaceSerializer(TaggitSerializer, ConnectedEndpointSerializer):
 class InterfaceSerializer(TaggitSerializer, ConnectedEndpointSerializer):
     device = NestedDeviceSerializer()
     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)
     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)
     untagged_vlan = NestedVLANSerializer(required=False, allow_null=True)
     tagged_vlans = SerializedPKRelatedField(
     tagged_vlans = SerializedPKRelatedField(
         queryset=VLAN.objects.all(),
         queryset=VLAN.objects.all(),
@@ -458,9 +514,9 @@ class InterfaceSerializer(TaggitSerializer, ConnectedEndpointSerializer):
     class Meta:
     class Meta:
         model = Interface
         model = Interface
         fields = [
         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()
     # TODO: This validation should be handled by Interface.clean()
@@ -486,7 +542,7 @@ class InterfaceSerializer(TaggitSerializer, ConnectedEndpointSerializer):
 
 
 class RearPortSerializer(TaggitSerializer, ValidatedModelSerializer):
 class RearPortSerializer(TaggitSerializer, ValidatedModelSerializer):
     device = NestedDeviceSerializer()
     device = NestedDeviceSerializer()
-    type = ChoiceField(choices=PORT_TYPE_CHOICES)
+    type = ChoiceField(choices=PortTypeChoices)
     cable = NestedCableSerializer(read_only=True)
     cable = NestedCableSerializer(read_only=True)
     tags = TagListSerializerField(required=False)
     tags = TagListSerializerField(required=False)
 
 
@@ -508,7 +564,7 @@ class FrontPortRearPortSerializer(WritableNestedSerializer):
 
 
 class FrontPortSerializer(TaggitSerializer, ValidatedModelSerializer):
 class FrontPortSerializer(TaggitSerializer, ValidatedModelSerializer):
     device = NestedDeviceSerializer()
     device = NestedDeviceSerializer()
-    type = ChoiceField(choices=PORT_TYPE_CHOICES)
+    type = ChoiceField(choices=PortTypeChoices)
     rear_port = FrontPortRearPortSerializer()
     rear_port = FrontPortRearPortSerializer()
     cable = NestedCableSerializer(read_only=True)
     cable = NestedCableSerializer(read_only=True)
     tags = TagListSerializerField(required=False)
     tags = TagListSerializerField(required=False)
@@ -553,15 +609,15 @@ class InventoryItemSerializer(TaggitSerializer, ValidatedModelSerializer):
 
 
 class CableSerializer(ValidatedModelSerializer):
 class CableSerializer(ValidatedModelSerializer):
     termination_a_type = ContentTypeField(
     termination_a_type = ContentTypeField(
-        queryset=ContentType.objects.filter(model__in=CABLE_TERMINATION_TYPES)
+        queryset=ContentType.objects.filter(CABLE_TERMINATION_MODELS)
     )
     )
     termination_b_type = ContentTypeField(
     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_a = serializers.SerializerMethodField(read_only=True)
     termination_b = 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:
     class Meta:
         model = Cable
         model = Cable
@@ -666,20 +722,20 @@ class PowerFeedSerializer(TaggitSerializer, CustomFieldModelSerializer):
         default=None
         default=None
     )
     )
     type = ChoiceField(
     type = ChoiceField(
-        choices=POWERFEED_TYPE_CHOICES,
-        default=POWERFEED_TYPE_PRIMARY
+        choices=PowerFeedTypeChoices,
+        default=PowerFeedTypeChoices.TYPE_PRIMARY
     )
     )
     status = ChoiceField(
     status = ChoiceField(
-        choices=POWERFEED_STATUS_CHOICES,
-        default=POWERFEED_STATUS_ACTIVE
+        choices=PowerFeedStatusChoices,
+        default=PowerFeedStatusChoices.STATUS_ACTIVE
     )
     )
     supply = ChoiceField(
     supply = ChoiceField(
-        choices=POWERFEED_SUPPLY_CHOICES,
-        default=POWERFEED_SUPPLY_AC
+        choices=PowerFeedSupplyChoices,
+        default=PowerFeedSupplyChoices.SUPPLY_AC
     )
     )
     phase = ChoiceField(
     phase = ChoiceField(
-        choices=POWERFEED_PHASE_CHOICES,
-        default=POWERFEED_PHASE_SINGLE
+        choices=PowerFeedPhaseChoices,
+        default=PowerFeedPhaseChoices.PHASE_SINGLE
     )
     )
     tags = TagListSerializerField(
     tags = TagListSerializerField(
         required=False
         required=False

+ 95 - 56
netbox/dcim/api/views.py

@@ -2,7 +2,7 @@ from collections import OrderedDict
 
 
 from django.conf import settings
 from django.conf import settings
 from django.db.models import Count, F
 from django.db.models import Count, F
-from django.http import HttpResponseForbidden
+from django.http import HttpResponseForbidden, HttpResponse
 from django.shortcuts import get_object_or_404
 from django.shortcuts import get_object_or_404
 from drf_yasg import openapi
 from drf_yasg import openapi
 from drf_yasg.openapi import Parameter
 from drf_yasg.openapi import Parameter
@@ -23,7 +23,6 @@ from dcim.models import (
 )
 )
 from extras.api.serializers import RenderedGraphSerializer
 from extras.api.serializers import RenderedGraphSerializer
 from extras.api.views import CustomFieldModelViewSet
 from extras.api.views import CustomFieldModelViewSet
-from extras.constants import GRAPH_TYPE_DEVICE, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE
 from extras.models import Graph
 from extras.models import Graph
 from ipam.models import Prefix, VLAN
 from ipam.models import Prefix, VLAN
 from utilities.api import (
 from utilities.api import (
@@ -41,21 +40,26 @@ from .exceptions import MissingFilterException
 
 
 class DCIMFieldChoicesViewSet(FieldChoicesViewSet):
 class DCIMFieldChoicesViewSet(FieldChoicesViewSet):
     fields = (
     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')
         site_count=Count('sites')
     )
     )
     serializer_class = serializers.RegionSerializer
     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'),
         virtualmachine_count=get_subquery(VirtualMachine, 'cluster__site'),
     )
     )
     serializer_class = serializers.SiteSerializer
     serializer_class = serializers.SiteSerializer
-    filterset_class = filters.SiteFilter
+    filterset_class = filters.SiteFilterSet
 
 
     @action(detail=True)
     @action(detail=True)
     def graphs(self, request, pk):
     def graphs(self, request, pk):
@@ -129,7 +133,7 @@ class SiteViewSet(CustomFieldModelViewSet):
         A convenience method for rendering graphs for a particular site.
         A convenience method for rendering graphs for a particular site.
         """
         """
         site = get_object_or_404(Site, pk=pk)
         site = get_object_or_404(Site, pk=pk)
-        queryset = Graph.objects.filter(type=GRAPH_TYPE_SITE)
+        queryset = Graph.objects.filter(type__model='site')
         serializer = RenderedGraphSerializer(queryset, many=True, context={'graphed_object': site})
         serializer = RenderedGraphSerializer(queryset, many=True, context={'graphed_object': site})
         return Response(serializer.data)
         return Response(serializer.data)
 
 
@@ -143,7 +147,7 @@ class RackGroupViewSet(ModelViewSet):
         rack_count=Count('racks')
         rack_count=Count('racks')
     )
     )
     serializer_class = serializers.RackGroupSerializer
     serializer_class = serializers.RackGroupSerializer
-    filterset_class = filters.RackGroupFilter
+    filterset_class = filters.RackGroupFilterSet
 
 
 
 
 #
 #
@@ -155,7 +159,7 @@ class RackRoleViewSet(ModelViewSet):
         rack_count=Count('racks')
         rack_count=Count('racks')
     )
     )
     serializer_class = serializers.RackRoleSerializer
     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')
         powerfeed_count=get_subquery(PowerFeed, 'rack')
     )
     )
     serializer_class = serializers.RackSerializer
     serializer_class = serializers.RackSerializer
-    filterset_class = filters.RackFilter
+    filterset_class = filters.RackFilterSet
 
 
+    @swagger_auto_schema(deprecated=True)
     @action(detail=True)
     @action(detail=True)
     def units(self, request, pk=None):
     def units(self, request, pk=None):
         """
         """
         List rack units (by rack)
         List rack units (by rack)
         """
         """
+        # TODO: Remove this action detail route in v2.8
         rack = get_object_or_404(Rack, pk=pk)
         rack = get_object_or_404(Rack, pk=pk)
-        face = request.GET.get('face', 0)
+        face = request.GET.get('face', 'front')
         exclude_pk = request.GET.get('exclude', None)
         exclude_pk = request.GET.get('exclude', None)
         if exclude_pk is not None:
         if exclude_pk is not None:
             try:
             try:
@@ -197,6 +203,39 @@ class RackViewSet(CustomFieldModelViewSet):
             rack_units = serializers.RackUnitSerializer(page, many=True, context={'request': request})
             rack_units = serializers.RackUnitSerializer(page, many=True, context={'request': request})
             return self.get_paginated_response(rack_units.data)
             return self.get_paginated_response(rack_units.data)
 
 
+    @swagger_auto_schema(
+        responses={200: serializers.RackUnitSerializer(many=True)},
+        query_serializer=serializers.RackElevationDetailFilterSerializer
+    )
+    @action(detail=True)
+    def elevation(self, request, pk=None):
+        """
+        Rack elevation representing the list of rack units. Also supports rendering the elevation as an SVG.
+        """
+        rack = get_object_or_404(Rack, pk=pk)
+        serializer = serializers.RackElevationDetailFilterSerializer(data=request.GET)
+        if not serializer.is_valid():
+            return Response(serializer.errors, 400)
+        data = serializer.validated_data
+
+        if data['render'] == 'svg':
+            # Render and return the elevation as an SVG drawing with the correct content type
+            drawing = rack.get_elevation_svg(data['face'], data['unit_width'], data['unit_height'])
+            return HttpResponse(drawing.tostring(), content_type='image/svg+xml')
+
+        else:
+            # Return a JSON representation of the rack units in the elevation
+            elevation = rack.get_rack_units(
+                face=data['face'],
+                exclude=data['exclude'],
+                expand_devices=data['expand_devices']
+            )
+
+            page = self.paginate_queryset(elevation)
+            if page is not None:
+                rack_units = serializers.RackUnitSerializer(page, many=True, context={'request': request})
+                return self.get_paginated_response(rack_units.data)
+
 
 
 #
 #
 # Rack reservations
 # Rack reservations
@@ -205,7 +244,7 @@ class RackViewSet(CustomFieldModelViewSet):
 class RackReservationViewSet(ModelViewSet):
 class RackReservationViewSet(ModelViewSet):
     queryset = RackReservation.objects.prefetch_related('rack', 'user', 'tenant')
     queryset = RackReservation.objects.prefetch_related('rack', 'user', 'tenant')
     serializer_class = serializers.RackReservationSerializer
     serializer_class = serializers.RackReservationSerializer
-    filterset_class = filters.RackReservationFilter
+    filterset_class = filters.RackReservationFilterSet
 
 
     # Assign user from request
     # Assign user from request
     def perform_create(self, serializer):
     def perform_create(self, serializer):
@@ -223,7 +262,7 @@ class ManufacturerViewSet(ModelViewSet):
         platform_count=get_subquery(Platform, 'manufacturer')
         platform_count=get_subquery(Platform, 'manufacturer')
     )
     )
     serializer_class = serializers.ManufacturerSerializer
     serializer_class = serializers.ManufacturerSerializer
-    filterset_class = filters.ManufacturerFilter
+    filterset_class = filters.ManufacturerFilterSet
 
 
 
 
 #
 #
@@ -235,7 +274,7 @@ class DeviceTypeViewSet(CustomFieldModelViewSet):
         device_count=Count('instances')
         device_count=Count('instances')
     )
     )
     serializer_class = serializers.DeviceTypeSerializer
     serializer_class = serializers.DeviceTypeSerializer
-    filterset_class = filters.DeviceTypeFilter
+    filterset_class = filters.DeviceTypeFilterSet
 
 
 
 
 #
 #
@@ -245,49 +284,49 @@ class DeviceTypeViewSet(CustomFieldModelViewSet):
 class ConsolePortTemplateViewSet(ModelViewSet):
 class ConsolePortTemplateViewSet(ModelViewSet):
     queryset = ConsolePortTemplate.objects.prefetch_related('device_type__manufacturer')
     queryset = ConsolePortTemplate.objects.prefetch_related('device_type__manufacturer')
     serializer_class = serializers.ConsolePortTemplateSerializer
     serializer_class = serializers.ConsolePortTemplateSerializer
-    filterset_class = filters.ConsolePortTemplateFilter
+    filterset_class = filters.ConsolePortTemplateFilterSet
 
 
 
 
 class ConsoleServerPortTemplateViewSet(ModelViewSet):
 class ConsoleServerPortTemplateViewSet(ModelViewSet):
     queryset = ConsoleServerPortTemplate.objects.prefetch_related('device_type__manufacturer')
     queryset = ConsoleServerPortTemplate.objects.prefetch_related('device_type__manufacturer')
     serializer_class = serializers.ConsoleServerPortTemplateSerializer
     serializer_class = serializers.ConsoleServerPortTemplateSerializer
-    filterset_class = filters.ConsoleServerPortTemplateFilter
+    filterset_class = filters.ConsoleServerPortTemplateFilterSet
 
 
 
 
 class PowerPortTemplateViewSet(ModelViewSet):
 class PowerPortTemplateViewSet(ModelViewSet):
     queryset = PowerPortTemplate.objects.prefetch_related('device_type__manufacturer')
     queryset = PowerPortTemplate.objects.prefetch_related('device_type__manufacturer')
     serializer_class = serializers.PowerPortTemplateSerializer
     serializer_class = serializers.PowerPortTemplateSerializer
-    filterset_class = filters.PowerPortTemplateFilter
+    filterset_class = filters.PowerPortTemplateFilterSet
 
 
 
 
 class PowerOutletTemplateViewSet(ModelViewSet):
 class PowerOutletTemplateViewSet(ModelViewSet):
     queryset = PowerOutletTemplate.objects.prefetch_related('device_type__manufacturer')
     queryset = PowerOutletTemplate.objects.prefetch_related('device_type__manufacturer')
     serializer_class = serializers.PowerOutletTemplateSerializer
     serializer_class = serializers.PowerOutletTemplateSerializer
-    filterset_class = filters.PowerOutletTemplateFilter
+    filterset_class = filters.PowerOutletTemplateFilterSet
 
 
 
 
 class InterfaceTemplateViewSet(ModelViewSet):
 class InterfaceTemplateViewSet(ModelViewSet):
     queryset = InterfaceTemplate.objects.prefetch_related('device_type__manufacturer')
     queryset = InterfaceTemplate.objects.prefetch_related('device_type__manufacturer')
     serializer_class = serializers.InterfaceTemplateSerializer
     serializer_class = serializers.InterfaceTemplateSerializer
-    filterset_class = filters.InterfaceTemplateFilter
+    filterset_class = filters.InterfaceTemplateFilterSet
 
 
 
 
 class FrontPortTemplateViewSet(ModelViewSet):
 class FrontPortTemplateViewSet(ModelViewSet):
     queryset = FrontPortTemplate.objects.prefetch_related('device_type__manufacturer')
     queryset = FrontPortTemplate.objects.prefetch_related('device_type__manufacturer')
     serializer_class = serializers.FrontPortTemplateSerializer
     serializer_class = serializers.FrontPortTemplateSerializer
-    filterset_class = filters.FrontPortTemplateFilter
+    filterset_class = filters.FrontPortTemplateFilterSet
 
 
 
 
 class RearPortTemplateViewSet(ModelViewSet):
 class RearPortTemplateViewSet(ModelViewSet):
     queryset = RearPortTemplate.objects.prefetch_related('device_type__manufacturer')
     queryset = RearPortTemplate.objects.prefetch_related('device_type__manufacturer')
     serializer_class = serializers.RearPortTemplateSerializer
     serializer_class = serializers.RearPortTemplateSerializer
-    filterset_class = filters.RearPortTemplateFilter
+    filterset_class = filters.RearPortTemplateFilterSet
 
 
 
 
 class DeviceBayTemplateViewSet(ModelViewSet):
 class DeviceBayTemplateViewSet(ModelViewSet):
     queryset = DeviceBayTemplate.objects.prefetch_related('device_type__manufacturer')
     queryset = DeviceBayTemplate.objects.prefetch_related('device_type__manufacturer')
     serializer_class = serializers.DeviceBayTemplateSerializer
     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')
         virtualmachine_count=get_subquery(VirtualMachine, 'role')
     )
     )
     serializer_class = serializers.DeviceRoleSerializer
     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')
         virtualmachine_count=get_subquery(VirtualMachine, 'platform')
     )
     )
     serializer_class = serializers.PlatformSerializer
     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',
         'device_type__manufacturer', 'device_role', 'tenant', 'platform', 'site', 'rack', 'parent_bay',
         'virtual_chassis__master', 'primary_ip4__nat_outside', 'primary_ip6__nat_outside', 'tags',
         '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):
     def get_serializer_class(self):
         """
         """
@@ -353,7 +392,7 @@ class DeviceViewSet(CustomFieldModelViewSet):
         A convenience method for rendering graphs for a particular Device.
         A convenience method for rendering graphs for a particular Device.
         """
         """
         device = get_object_or_404(Device, pk=pk)
         device = get_object_or_404(Device, pk=pk)
-        queryset = Graph.objects.filter(type=GRAPH_TYPE_DEVICE)
+        queryset = Graph.objects.filter(type__model='device')
         serializer = RenderedGraphSerializer(queryset, many=True, context={'graphed_object': device})
         serializer = RenderedGraphSerializer(queryset, many=True, context={'graphed_object': device})
 
 
         return Response(serializer.data)
         return Response(serializer.data)
@@ -464,13 +503,13 @@ class DeviceViewSet(CustomFieldModelViewSet):
 class ConsolePortViewSet(CableTraceMixin, ModelViewSet):
 class ConsolePortViewSet(CableTraceMixin, ModelViewSet):
     queryset = ConsolePort.objects.prefetch_related('device', 'connected_endpoint__device', 'cable', 'tags')
     queryset = ConsolePort.objects.prefetch_related('device', 'connected_endpoint__device', 'cable', 'tags')
     serializer_class = serializers.ConsolePortSerializer
     serializer_class = serializers.ConsolePortSerializer
-    filterset_class = filters.ConsolePortFilter
+    filterset_class = filters.ConsolePortFilterSet
 
 
 
 
 class ConsoleServerPortViewSet(CableTraceMixin, ModelViewSet):
 class ConsoleServerPortViewSet(CableTraceMixin, ModelViewSet):
     queryset = ConsoleServerPort.objects.prefetch_related('device', 'connected_endpoint__device', 'cable', 'tags')
     queryset = ConsoleServerPort.objects.prefetch_related('device', 'connected_endpoint__device', 'cable', 'tags')
     serializer_class = serializers.ConsoleServerPortSerializer
     serializer_class = serializers.ConsoleServerPortSerializer
-    filterset_class = filters.ConsoleServerPortFilter
+    filterset_class = filters.ConsoleServerPortFilterSet
 
 
 
 
 class PowerPortViewSet(CableTraceMixin, ModelViewSet):
 class PowerPortViewSet(CableTraceMixin, ModelViewSet):
@@ -478,13 +517,13 @@ class PowerPortViewSet(CableTraceMixin, ModelViewSet):
         'device', '_connected_poweroutlet__device', '_connected_powerfeed', 'cable', 'tags'
         'device', '_connected_poweroutlet__device', '_connected_powerfeed', 'cable', 'tags'
     )
     )
     serializer_class = serializers.PowerPortSerializer
     serializer_class = serializers.PowerPortSerializer
-    filterset_class = filters.PowerPortFilter
+    filterset_class = filters.PowerPortFilterSet
 
 
 
 
 class PowerOutletViewSet(CableTraceMixin, ModelViewSet):
 class PowerOutletViewSet(CableTraceMixin, ModelViewSet):
     queryset = PowerOutlet.objects.prefetch_related('device', 'connected_endpoint__device', 'cable', 'tags')
     queryset = PowerOutlet.objects.prefetch_related('device', 'connected_endpoint__device', 'cable', 'tags')
     serializer_class = serializers.PowerOutletSerializer
     serializer_class = serializers.PowerOutletSerializer
-    filterset_class = filters.PowerOutletFilter
+    filterset_class = filters.PowerOutletFilterSet
 
 
 
 
 class InterfaceViewSet(CableTraceMixin, ModelViewSet):
 class InterfaceViewSet(CableTraceMixin, ModelViewSet):
@@ -494,7 +533,7 @@ class InterfaceViewSet(CableTraceMixin, ModelViewSet):
         device__isnull=False
         device__isnull=False
     )
     )
     serializer_class = serializers.InterfaceSerializer
     serializer_class = serializers.InterfaceSerializer
-    filterset_class = filters.InterfaceFilter
+    filterset_class = filters.InterfaceFilterSet
 
 
     @action(detail=True)
     @action(detail=True)
     def graphs(self, request, pk):
     def graphs(self, request, pk):
@@ -502,7 +541,7 @@ class InterfaceViewSet(CableTraceMixin, ModelViewSet):
         A convenience method for rendering graphs for a particular interface.
         A convenience method for rendering graphs for a particular interface.
         """
         """
         interface = get_object_or_404(Interface, pk=pk)
         interface = get_object_or_404(Interface, pk=pk)
-        queryset = Graph.objects.filter(type=GRAPH_TYPE_INTERFACE)
+        queryset = Graph.objects.filter(type__model='interface')
         serializer = RenderedGraphSerializer(queryset, many=True, context={'graphed_object': interface})
         serializer = RenderedGraphSerializer(queryset, many=True, context={'graphed_object': interface})
         return Response(serializer.data)
         return Response(serializer.data)
 
 
@@ -510,25 +549,25 @@ class InterfaceViewSet(CableTraceMixin, ModelViewSet):
 class FrontPortViewSet(ModelViewSet):
 class FrontPortViewSet(ModelViewSet):
     queryset = FrontPort.objects.prefetch_related('device__device_type__manufacturer', 'rear_port', 'cable', 'tags')
     queryset = FrontPort.objects.prefetch_related('device__device_type__manufacturer', 'rear_port', 'cable', 'tags')
     serializer_class = serializers.FrontPortSerializer
     serializer_class = serializers.FrontPortSerializer
-    filterset_class = filters.FrontPortFilter
+    filterset_class = filters.FrontPortFilterSet
 
 
 
 
 class RearPortViewSet(ModelViewSet):
 class RearPortViewSet(ModelViewSet):
     queryset = RearPort.objects.prefetch_related('device__device_type__manufacturer', 'cable', 'tags')
     queryset = RearPort.objects.prefetch_related('device__device_type__manufacturer', 'cable', 'tags')
     serializer_class = serializers.RearPortSerializer
     serializer_class = serializers.RearPortSerializer
-    filterset_class = filters.RearPortFilter
+    filterset_class = filters.RearPortFilterSet
 
 
 
 
 class DeviceBayViewSet(ModelViewSet):
 class DeviceBayViewSet(ModelViewSet):
     queryset = DeviceBay.objects.prefetch_related('installed_device').prefetch_related('tags')
     queryset = DeviceBay.objects.prefetch_related('installed_device').prefetch_related('tags')
     serializer_class = serializers.DeviceBaySerializer
     serializer_class = serializers.DeviceBaySerializer
-    filterset_class = filters.DeviceBayFilter
+    filterset_class = filters.DeviceBayFilterSet
 
 
 
 
 class InventoryItemViewSet(ModelViewSet):
 class InventoryItemViewSet(ModelViewSet):
     queryset = InventoryItem.objects.prefetch_related('device', 'manufacturer').prefetch_related('tags')
     queryset = InventoryItem.objects.prefetch_related('device', 'manufacturer').prefetch_related('tags')
     serializer_class = serializers.InventoryItemSerializer
     serializer_class = serializers.InventoryItemSerializer
-    filterset_class = filters.InventoryItemFilter
+    filterset_class = filters.InventoryItemFilterSet
 
 
 
 
 #
 #
@@ -542,7 +581,7 @@ class ConsoleConnectionViewSet(ListModelMixin, GenericViewSet):
         connected_endpoint__isnull=False
         connected_endpoint__isnull=False
     )
     )
     serializer_class = serializers.ConsolePortSerializer
     serializer_class = serializers.ConsolePortSerializer
-    filterset_class = filters.ConsoleConnectionFilter
+    filterset_class = filters.ConsoleConnectionFilterSet
 
 
 
 
 class PowerConnectionViewSet(ListModelMixin, GenericViewSet):
 class PowerConnectionViewSet(ListModelMixin, GenericViewSet):
@@ -552,7 +591,7 @@ class PowerConnectionViewSet(ListModelMixin, GenericViewSet):
         _connected_poweroutlet__isnull=False
         _connected_poweroutlet__isnull=False
     )
     )
     serializer_class = serializers.PowerPortSerializer
     serializer_class = serializers.PowerPortSerializer
-    filterset_class = filters.PowerConnectionFilter
+    filterset_class = filters.PowerConnectionFilterSet
 
 
 
 
 class InterfaceConnectionViewSet(ListModelMixin, GenericViewSet):
 class InterfaceConnectionViewSet(ListModelMixin, GenericViewSet):
@@ -564,7 +603,7 @@ class InterfaceConnectionViewSet(ListModelMixin, GenericViewSet):
         pk__lt=F('_connected_interface')
         pk__lt=F('_connected_interface')
     )
     )
     serializer_class = serializers.InterfaceConnectionSerializer
     serializer_class = serializers.InterfaceConnectionSerializer
-    filterset_class = filters.InterfaceConnectionFilter
+    filterset_class = filters.InterfaceConnectionFilterSet
 
 
 
 
 #
 #
@@ -576,7 +615,7 @@ class CableViewSet(ModelViewSet):
         'termination_a', 'termination_b'
         'termination_a', 'termination_b'
     )
     )
     serializer_class = serializers.CableSerializer
     serializer_class = serializers.CableSerializer
-    filterset_class = filters.CableFilter
+    filterset_class = filters.CableFilterSet
 
 
 
 
 #
 #
@@ -588,7 +627,7 @@ class VirtualChassisViewSet(ModelViewSet):
         member_count=Count('members')
         member_count=Count('members')
     )
     )
     serializer_class = serializers.VirtualChassisSerializer
     serializer_class = serializers.VirtualChassisSerializer
-    filterset_class = filters.VirtualChassisFilter
+    filterset_class = filters.VirtualChassisFilterSet
 
 
 
 
 #
 #
@@ -602,7 +641,7 @@ class PowerPanelViewSet(ModelViewSet):
         powerfeed_count=Count('powerfeeds')
         powerfeed_count=Count('powerfeeds')
     )
     )
     serializer_class = serializers.PowerPanelSerializer
     serializer_class = serializers.PowerPanelSerializer
-    filterset_class = filters.PowerPanelFilter
+    filterset_class = filters.PowerPanelFilterSet
 
 
 
 
 #
 #
@@ -612,7 +651,7 @@ class PowerPanelViewSet(ModelViewSet):
 class PowerFeedViewSet(CustomFieldModelViewSet):
 class PowerFeedViewSet(CustomFieldModelViewSet):
     queryset = PowerFeed.objects.prefetch_related('power_panel', 'rack', 'tags')
     queryset = PowerFeed.objects.prefetch_related('power_panel', 'rack', 'tags')
     serializer_class = serializers.PowerFeedSerializer
     serializer_class = serializers.PowerFeedSerializer
-    filterset_class = filters.PowerFeedFilter
+    filterset_class = filters.PowerFeedFilterSet
 
 
 
 
 #
 #

+ 1076 - 0
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,
+    }

+ 35 - 481
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 faces
-RACK_FACE_FRONT = 0
-RACK_FACE_REAR = 1
-RACK_FACE_CHOICES = [
-    [RACK_FACE_FRONT, 'Front'],
-    [RACK_FACE_REAR, 'Rear'],
-]
-
-# Rack statuses
-RACK_STATUS_RESERVED = 0
-RACK_STATUS_AVAILABLE = 1
-RACK_STATUS_PLANNED = 2
-RACK_STATUS_ACTIVE = 3
-RACK_STATUS_DEPRECATED = 4
-RACK_STATUS_CHOICES = [
-    [RACK_STATUS_ACTIVE, 'Active'],
-    [RACK_STATUS_PLANNED, 'Planned'],
-    [RACK_STATUS_RESERVED, 'Reserved'],
-    [RACK_STATUS_AVAILABLE, 'Available'],
-    [RACK_STATUS_DEPRECATED, 'Deprecated'],
-]
-
-# Device rack position
-DEVICE_POSITION_CHOICES = [
-    # Rack.u_height is limited to 100
-    (i, 'Unit {}'.format(i)) for i in range(1, 101)
-]
 
 
-# Parent/child device roles
-SUBDEVICE_ROLE_PARENT = True
-SUBDEVICE_ROLE_CHILD = False
-SUBDEVICE_ROLE_CHOICES = (
-    (None, 'None'),
-    (SUBDEVICE_ROLE_PARENT, 'Parent'),
-    (SUBDEVICE_ROLE_CHILD, 'Child'),
-)
+#
+# Rack elevation rendering
+#
 
 
-# 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
+RACK_ELEVATION_UNIT_WIDTH_DEFAULT = 230
+RACK_ELEVATION_UNIT_HEIGHT_DEFAULT = 20
 
 
-# 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 = [
 VIRTUAL_IFACE_TYPES = [
-    IFACE_TYPE_VIRTUAL,
-    IFACE_TYPE_LAG,
+    InterfaceTypeChoices.TYPE_VIRTUAL,
+    InterfaceTypeChoices.TYPE_LAG,
 ]
 ]
 
 
 WIRELESS_IFACE_TYPES = [
 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
 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
 # Console/power/interface connection statuses
 CONNECTION_STATUS_PLANNED = False
 CONNECTION_STATUS_PLANNED = False
 CONNECTION_STATUS_CONNECTED = True
 CONNECTION_STATUS_CONNECTED = True
@@ -387,72 +45,22 @@ CONNECTION_STATUS_CHOICES = [
 ]
 ]
 
 
 # Cable endpoint types
 # 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 = {
 COMPATIBLE_TERMINATION_TYPES = {
     'consoleport': ['consoleserverport', 'frontport', 'rearport'],
     'consoleport': ['consoleserverport', 'frontport', 'rearport'],
     'consoleserverport': ['consoleport', 'frontport', 'rearport'],
     'consoleserverport': ['consoleport', 'frontport', 'rearport'],
@@ -463,57 +71,3 @@ COMPATIBLE_TERMINATION_TYPES = {
     'rearport': ['consoleport', 'consoleserverport', 'interface', 'frontport', 'rearport', 'circuittermination'],
     'rearport': ['consoleport', 'consoleserverport', 'interface', 'frontport', 'rearport', 'circuittermination'],
     'circuittermination': ['interface', 'frontport', 'rearport'],
     'circuittermination': ['interface', 'frontport', 'rearport'],
 }
 }
-
-LENGTH_UNIT_METER = 1200
-LENGTH_UNIT_CENTIMETER = 1100
-LENGTH_UNIT_MILLIMETER = 1000
-LENGTH_UNIT_FOOT = 2100
-LENGTH_UNIT_INCH = 2000
-CABLE_LENGTH_UNIT_CHOICES = (
-    (LENGTH_UNIT_METER, 'Meters'),
-    (LENGTH_UNIT_CENTIMETER, 'Centimeters'),
-    (LENGTH_UNIT_FOOT, 'Feet'),
-    (LENGTH_UNIT_INCH, 'Inches'),
-)
-RACK_DIMENSION_UNIT_CHOICES = (
-    (LENGTH_UNIT_MILLIMETER, 'Millimeters'),
-    (LENGTH_UNIT_INCH, 'Inches'),
-)
-
-# 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'),
-)

+ 6 - 3
netbox/dcim/fields.py

@@ -3,7 +3,7 @@ from django.core.validators import MinValueValidator, MaxValueValidator
 from django.db import models
 from django.db import models
 from netaddr import AddrFormatError, EUI, mac_unix_expanded
 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):
 class ASNField(models.BigIntegerField):
@@ -14,7 +14,10 @@ class ASNField(models.BigIntegerField):
     ]
     ]
 
 
     def formfield(self, **kwargs):
     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)
         defaults.update(**kwargs)
         return super().formfield(**defaults)
         return super().formfield(**defaults)
 
 
@@ -29,7 +32,7 @@ class MACAddressField(models.Field):
     def python_type(self):
     def python_type(self):
         return EUI
         return EUI
 
 
-    def from_db_value(self, value, expression, connection, context):
+    def from_db_value(self, value, expression, connection):
         return self.to_python(value)
         return self.to_python(value)
 
 
     def to_python(self, value):
     def to_python(self, value):

+ 140 - 82
netbox/dcim/filters.py

@@ -2,8 +2,8 @@ import django_filters
 from django.contrib.auth.models import User
 from django.contrib.auth.models import User
 from django.db.models import Q
 from django.db.models import Q
 
 
-from extras.filters import CustomFieldFilterSet, LocalConfigContextFilter, CreatedUpdatedFilterSet
-from tenancy.filtersets import TenancyFilterSet
+from extras.filters import CustomFieldFilterSet, LocalConfigContextFilterSet, CreatedUpdatedFilterSet
+from tenancy.filters import TenancyFilterSet
 from tenancy.models import Tenant
 from tenancy.models import Tenant
 from utilities.constants import COLOR_CHOICES
 from utilities.constants import COLOR_CHOICES
 from utilities.filters import (
 from utilities.filters import (
@@ -11,6 +11,7 @@ from utilities.filters import (
     TagFilter, TreeNodeMultipleChoiceFilter,
     TagFilter, TreeNodeMultipleChoiceFilter,
 )
 )
 from virtualization.models import Cluster
 from virtualization.models import Cluster
+from .choices import *
 from .constants import *
 from .constants import *
 from .models import (
 from .models import (
     Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
     Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
@@ -22,45 +23,45 @@ from .models import (
 
 
 
 
 __all__ = (
 __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(
     parent_id = django_filters.ModelMultipleChoiceFilter(
         queryset=Region.objects.all(),
         queryset=Region.objects.all(),
         label='Parent region (ID)',
         label='Parent region (ID)',
@@ -77,7 +78,7 @@ class RegionFilter(NameSlugSearchFilterSet):
         fields = ['id', 'name', 'slug']
         fields = ['id', 'name', 'slug']
 
 
 
 
-class SiteFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
+class SiteFilterSet(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
     id__in = NumericInFilter(
     id__in = NumericInFilter(
         field_name='id',
         field_name='id',
         lookup_expr='in'
         lookup_expr='in'
@@ -87,7 +88,7 @@ class SiteFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet
         label='Search',
         label='Search',
     )
     )
     status = django_filters.MultipleChoiceFilter(
     status = django_filters.MultipleChoiceFilter(
-        choices=SITE_STATUS_CHOICES,
+        choices=SiteStatusChoices,
         null_value=None
         null_value=None
     )
     )
     region_id = TreeNodeMultipleChoiceFilter(
     region_id = TreeNodeMultipleChoiceFilter(
@@ -131,7 +132,7 @@ class SiteFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet
         return queryset.filter(qs_filter)
         return queryset.filter(qs_filter)
 
 
 
 
-class RackGroupFilter(NameSlugSearchFilterSet):
+class RackGroupFilterSet(NameSlugSearchFilterSet):
     region_id = TreeNodeMultipleChoiceFilter(
     region_id = TreeNodeMultipleChoiceFilter(
         queryset=Region.objects.all(),
         queryset=Region.objects.all(),
         field_name='site__region__in',
         field_name='site__region__in',
@@ -159,14 +160,14 @@ class RackGroupFilter(NameSlugSearchFilterSet):
         fields = ['id', 'name', 'slug']
         fields = ['id', 'name', 'slug']
 
 
 
 
-class RackRoleFilter(NameSlugSearchFilterSet):
+class RackRoleFilterSet(NameSlugSearchFilterSet):
 
 
     class Meta:
     class Meta:
         model = RackRole
         model = RackRole
         fields = ['id', 'name', 'slug', 'color']
         fields = ['id', 'name', 'slug', 'color']
 
 
 
 
-class RackFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
+class RackFilterSet(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
     id__in = NumericInFilter(
     id__in = NumericInFilter(
         field_name='id',
         field_name='id',
         lookup_expr='in'
         lookup_expr='in'
@@ -207,7 +208,7 @@ class RackFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet
         label='Group',
         label='Group',
     )
     )
     status = django_filters.MultipleChoiceFilter(
     status = django_filters.MultipleChoiceFilter(
-        choices=RACK_STATUS_CHOICES,
+        choices=RackStatusChoices,
         null_value=None
         null_value=None
     )
     )
     role_id = django_filters.ModelMultipleChoiceFilter(
     role_id = django_filters.ModelMultipleChoiceFilter(
@@ -244,7 +245,7 @@ class RackFilter(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet
         )
         )
 
 
 
 
-class RackReservationFilter(TenancyFilterSet):
+class RackReservationFilterSet(TenancyFilterSet):
     id__in = NumericInFilter(
     id__in = NumericInFilter(
         field_name='id',
         field_name='id',
         lookup_expr='in'
         lookup_expr='in'
@@ -305,14 +306,14 @@ class RackReservationFilter(TenancyFilterSet):
         )
         )
 
 
 
 
-class ManufacturerFilter(NameSlugSearchFilterSet):
+class ManufacturerFilterSet(NameSlugSearchFilterSet):
 
 
     class Meta:
     class Meta:
         model = Manufacturer
         model = Manufacturer
         fields = ['id', 'name', 'slug']
         fields = ['id', 'name', 'slug']
 
 
 
 
-class DeviceTypeFilter(CustomFieldFilterSet, CreatedUpdatedFilterSet):
+class DeviceTypeFilterSet(CustomFieldFilterSet, CreatedUpdatedFilterSet):
     id__in = NumericInFilter(
     id__in = NumericInFilter(
         field_name='id',
         field_name='id',
         lookup_expr='in'
         lookup_expr='in'
@@ -403,70 +404,70 @@ class DeviceTypeComponentFilterSet(NameSlugSearchFilterSet):
     )
     )
 
 
 
 
-class ConsolePortTemplateFilter(DeviceTypeComponentFilterSet):
+class ConsolePortTemplateFilterSet(DeviceTypeComponentFilterSet):
 
 
     class Meta:
     class Meta:
         model = ConsolePortTemplate
         model = ConsolePortTemplate
-        fields = ['id', 'name']
+        fields = ['id', 'name', 'type']
 
 
 
 
-class ConsoleServerPortTemplateFilter(DeviceTypeComponentFilterSet):
+class ConsoleServerPortTemplateFilterSet(DeviceTypeComponentFilterSet):
 
 
     class Meta:
     class Meta:
         model = ConsoleServerPortTemplate
         model = ConsoleServerPortTemplate
-        fields = ['id', 'name']
+        fields = ['id', 'name', 'type']
 
 
 
 
-class PowerPortTemplateFilter(DeviceTypeComponentFilterSet):
+class PowerPortTemplateFilterSet(DeviceTypeComponentFilterSet):
 
 
     class Meta:
     class Meta:
         model = PowerPortTemplate
         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:
     class Meta:
         model = PowerOutletTemplate
         model = PowerOutletTemplate
-        fields = ['id', 'name', 'feed_leg']
+        fields = ['id', 'name', 'type', 'feed_leg']
 
 
 
 
-class InterfaceTemplateFilter(DeviceTypeComponentFilterSet):
+class InterfaceTemplateFilterSet(DeviceTypeComponentFilterSet):
 
 
     class Meta:
     class Meta:
         model = InterfaceTemplate
         model = InterfaceTemplate
         fields = ['id', 'name', 'type', 'mgmt_only']
         fields = ['id', 'name', 'type', 'mgmt_only']
 
 
 
 
-class FrontPortTemplateFilter(DeviceTypeComponentFilterSet):
+class FrontPortTemplateFilterSet(DeviceTypeComponentFilterSet):
 
 
     class Meta:
     class Meta:
         model = FrontPortTemplate
         model = FrontPortTemplate
         fields = ['id', 'name', 'type']
         fields = ['id', 'name', 'type']
 
 
 
 
-class RearPortTemplateFilter(DeviceTypeComponentFilterSet):
+class RearPortTemplateFilterSet(DeviceTypeComponentFilterSet):
 
 
     class Meta:
     class Meta:
         model = RearPortTemplate
         model = RearPortTemplate
         fields = ['id', 'name', 'type', 'positions']
         fields = ['id', 'name', 'type', 'positions']
 
 
 
 
-class DeviceBayTemplateFilter(DeviceTypeComponentFilterSet):
+class DeviceBayTemplateFilterSet(DeviceTypeComponentFilterSet):
 
 
     class Meta:
     class Meta:
         model = DeviceBayTemplate
         model = DeviceBayTemplate
         fields = ['id', 'name']
         fields = ['id', 'name']
 
 
 
 
-class DeviceRoleFilter(NameSlugSearchFilterSet):
+class DeviceRoleFilterSet(NameSlugSearchFilterSet):
 
 
     class Meta:
     class Meta:
         model = DeviceRole
         model = DeviceRole
         fields = ['id', 'name', 'slug', 'color', 'vm_role']
         fields = ['id', 'name', 'slug', 'color', 'vm_role']
 
 
 
 
-class PlatformFilter(NameSlugSearchFilterSet):
+class PlatformFilterSet(NameSlugSearchFilterSet):
     manufacturer_id = django_filters.ModelMultipleChoiceFilter(
     manufacturer_id = django_filters.ModelMultipleChoiceFilter(
         field_name='manufacturer',
         field_name='manufacturer',
         queryset=Manufacturer.objects.all(),
         queryset=Manufacturer.objects.all(),
@@ -484,7 +485,7 @@ class PlatformFilter(NameSlugSearchFilterSet):
         fields = ['id', 'name', 'slug', 'napalm_driver']
         fields = ['id', 'name', 'slug', 'napalm_driver']
 
 
 
 
-class DeviceFilter(LocalConfigContextFilter, TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
+class DeviceFilterSet(LocalConfigContextFilterSet, TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
     id__in = NumericInFilter(
     id__in = NumericInFilter(
         field_name='id',
         field_name='id',
         lookup_expr='in'
         lookup_expr='in'
@@ -571,7 +572,7 @@ class DeviceFilter(LocalConfigContextFilter, TenancyFilterSet, CustomFieldFilter
         label='Device model (slug)',
         label='Device model (slug)',
     )
     )
     status = django_filters.MultipleChoiceFilter(
     status = django_filters.MultipleChoiceFilter(
-        choices=DEVICE_STATUS_CHOICES,
+        choices=DeviceStatusChoices,
         null_value=None
         null_value=None
     )
     )
     is_full_depth = django_filters.BooleanFilter(
     is_full_depth = django_filters.BooleanFilter(
@@ -681,6 +682,26 @@ class DeviceComponentFilterSet(django_filters.FilterSet):
         method='search',
         method='search',
         label='Search',
         label='Search',
     )
     )
+    region_id = django_filters.ModelMultipleChoiceFilter(
+        field_name='device__site__region',
+        queryset=Region.objects.all(),
+        label='Region (ID)',
+    )
+    region = django_filters.ModelMultipleChoiceFilter(
+        field_name='device__site__region__in',
+        queryset=Region.objects.all(),
+        label='Region name (slug)',
+    )
+    site_id = django_filters.ModelMultipleChoiceFilter(
+        field_name='device__site',
+        queryset=Site.objects.all(),
+        label='Site (ID)',
+    )
+    site = django_filters.ModelMultipleChoiceFilter(
+        field_name='device__site__slug',
+        queryset=Site.objects.all(),
+        label='Site name (slug)',
+    )
     device_id = django_filters.ModelMultipleChoiceFilter(
     device_id = django_filters.ModelMultipleChoiceFilter(
         queryset=Device.objects.all(),
         queryset=Device.objects.all(),
         label='Device (ID)',
         label='Device (ID)',
@@ -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(
     cabled = django_filters.BooleanFilter(
         field_name='cable',
         field_name='cable',
         lookup_expr='isnull',
         lookup_expr='isnull',
@@ -714,7 +739,11 @@ class ConsolePortFilter(DeviceComponentFilterSet):
         fields = ['id', 'name', 'description', 'connection_status']
         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(
     cabled = django_filters.BooleanFilter(
         field_name='cable',
         field_name='cable',
         lookup_expr='isnull',
         lookup_expr='isnull',
@@ -726,7 +755,11 @@ class ConsoleServerPortFilter(DeviceComponentFilterSet):
         fields = ['id', 'name', 'description', 'connection_status']
         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(
     cabled = django_filters.BooleanFilter(
         field_name='cable',
         field_name='cable',
         lookup_expr='isnull',
         lookup_expr='isnull',
@@ -738,7 +771,11 @@ class PowerPortFilter(DeviceComponentFilterSet):
         fields = ['id', 'name', 'maximum_draw', 'allocated_draw', 'description', 'connection_status']
         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(
     cabled = django_filters.BooleanFilter(
         field_name='cable',
         field_name='cable',
         lookup_expr='isnull',
         lookup_expr='isnull',
@@ -750,7 +787,7 @@ class PowerOutletFilter(DeviceComponentFilterSet):
         fields = ['id', 'name', 'feed_leg', 'description', 'connection_status']
         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.
     Not using DeviceComponentFilterSet for Interfaces because we need to check for VirtualChassis membership.
     """
     """
@@ -758,6 +795,27 @@ class InterfaceFilter(django_filters.FilterSet):
         method='search',
         method='search',
         label='Search',
         label='Search',
     )
     )
+    region_id = django_filters.ModelMultipleChoiceFilter(
+        field_name='device__site__region',
+        queryset=Region.objects.all(),
+        label='Region (ID)',
+    )
+    region = django_filters.ModelMultipleChoiceFilter(
+        field_name='device__site__region__in',
+        queryset=Region.objects.all(),
+        label='Region name (slug)',
+    )
+    site_id = django_filters.ModelMultipleChoiceFilter(
+        field_name='device__site',
+        queryset=Site.objects.all(),
+        label='Site (ID)',
+    )
+    site = django_filters.ModelMultipleChoiceFilter(
+        field_name='device__site__slug',
+        to_field_name='slug',
+        queryset=Site.objects.all(),
+        label='Site name (slug)',
+    )
     device = MultiValueCharFilter(
     device = MultiValueCharFilter(
         method='filter_device',
         method='filter_device',
         field_name='name',
         field_name='name',
@@ -793,7 +851,7 @@ class InterfaceFilter(django_filters.FilterSet):
         label='Assigned VID'
         label='Assigned VID'
     )
     )
     type = django_filters.MultipleChoiceFilter(
     type = django_filters.MultipleChoiceFilter(
-        choices=IFACE_TYPE_CHOICES,
+        choices=InterfaceTypeChoices,
         null_value=None
         null_value=None
     )
     )
 
 
@@ -857,7 +915,7 @@ class InterfaceFilter(django_filters.FilterSet):
         }.get(value, queryset.none())
         }.get(value, queryset.none())
 
 
 
 
-class FrontPortFilter(DeviceComponentFilterSet):
+class FrontPortFilterSet(DeviceComponentFilterSet):
     cabled = django_filters.BooleanFilter(
     cabled = django_filters.BooleanFilter(
         field_name='cable',
         field_name='cable',
         lookup_expr='isnull',
         lookup_expr='isnull',
@@ -869,7 +927,7 @@ class FrontPortFilter(DeviceComponentFilterSet):
         fields = ['id', 'name', 'type', 'description']
         fields = ['id', 'name', 'type', 'description']
 
 
 
 
-class RearPortFilter(DeviceComponentFilterSet):
+class RearPortFilterSet(DeviceComponentFilterSet):
     cabled = django_filters.BooleanFilter(
     cabled = django_filters.BooleanFilter(
         field_name='cable',
         field_name='cable',
         lookup_expr='isnull',
         lookup_expr='isnull',
@@ -881,14 +939,14 @@ class RearPortFilter(DeviceComponentFilterSet):
         fields = ['id', 'name', 'type', 'positions', 'description']
         fields = ['id', 'name', 'type', 'positions', 'description']
 
 
 
 
-class DeviceBayFilter(DeviceComponentFilterSet):
+class DeviceBayFilterSet(DeviceComponentFilterSet):
 
 
     class Meta:
     class Meta:
         model = DeviceBay
         model = DeviceBay
         fields = ['id', 'name', 'description']
         fields = ['id', 'name', 'description']
 
 
 
 
-class InventoryItemFilter(DeviceComponentFilterSet):
+class InventoryItemFilterSet(DeviceComponentFilterSet):
     q = django_filters.CharFilter(
     q = django_filters.CharFilter(
         method='search',
         method='search',
         label='Search',
         label='Search',
@@ -959,7 +1017,7 @@ class InventoryItemFilter(DeviceComponentFilterSet):
         return queryset.filter(qs_filter)
         return queryset.filter(qs_filter)
 
 
 
 
-class VirtualChassisFilter(django_filters.FilterSet):
+class VirtualChassisFilterSet(django_filters.FilterSet):
     q = django_filters.CharFilter(
     q = django_filters.CharFilter(
         method='search',
         method='search',
         label='Search',
         label='Search',
@@ -1013,16 +1071,16 @@ class VirtualChassisFilter(django_filters.FilterSet):
         return queryset.filter(qs_filter)
         return queryset.filter(qs_filter)
 
 
 
 
-class CableFilter(django_filters.FilterSet):
+class CableFilterSet(django_filters.FilterSet):
     q = django_filters.CharFilter(
     q = django_filters.CharFilter(
         method='search',
         method='search',
         label='Search',
         label='Search',
     )
     )
     type = django_filters.MultipleChoiceFilter(
     type = django_filters.MultipleChoiceFilter(
-        choices=CABLE_TYPE_CHOICES
+        choices=CableTypeChoices
     )
     )
     status = django_filters.MultipleChoiceFilter(
     status = django_filters.MultipleChoiceFilter(
-        choices=CONNECTION_STATUS_CHOICES
+        choices=CableStatusChoices
     )
     )
     color = django_filters.MultipleChoiceFilter(
     color = django_filters.MultipleChoiceFilter(
         choices=COLOR_CHOICES
         choices=COLOR_CHOICES
@@ -1076,7 +1134,7 @@ class CableFilter(django_filters.FilterSet):
         return queryset
         return queryset
 
 
 
 
-class ConsoleConnectionFilter(django_filters.FilterSet):
+class ConsoleConnectionFilterSet(django_filters.FilterSet):
     site = django_filters.CharFilter(
     site = django_filters.CharFilter(
         method='filter_site',
         method='filter_site',
         label='Site (slug)',
         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(
     site = django_filters.CharFilter(
         method='filter_site',
         method='filter_site',
         label='Site (slug)',
         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(
     site = django_filters.CharFilter(
         method='filter_site',
         method='filter_site',
         label='Site (slug)',
         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(
     id__in = NumericInFilter(
         field_name='id',
         field_name='id',
         lookup_expr='in'
         lookup_expr='in'
@@ -1221,7 +1279,7 @@ class PowerPanelFilter(django_filters.FilterSet):
         return queryset.filter(qs_filter)
         return queryset.filter(qs_filter)
 
 
 
 
-class PowerFeedFilter(CustomFieldFilterSet, CreatedUpdatedFilterSet):
+class PowerFeedFilterSet(CustomFieldFilterSet, CreatedUpdatedFilterSet):
     id__in = NumericInFilter(
     id__in = NumericInFilter(
         field_name='id',
         field_name='id',
         lookup_expr='in'
         lookup_expr='in'

+ 11 - 11
netbox/dcim/fixtures/dcim.json

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

+ 0 - 195
netbox/dcim/fixtures/initial_data.json

@@ -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"
-    }
-}
-]

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 442 - 77
netbox/dcim/forms.py


+ 0 - 259
netbox/dcim/migrations/0002_auto_20160622_1821_squashed_0022_color_names_to_rgb.py

@@ -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),
-        ),
-    ]

+ 101 - 0
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'),
+        ),
+    ]

+ 154 - 0
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),
+        ),
+    ]

+ 54 - 10
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.contrib.postgres.fields
 import django.core.validators
 import django.core.validators
-from django.db import migrations, models
 import django.db.models.deletion
 import django.db.models.deletion
 import mptt.fields
 import mptt.fields
+from django.conf import settings
+from django.db import migrations, models
+
+import dcim.fields
 import utilities.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')]
     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 = [
     dependencies = [
-        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
         ('dcim', '0022_color_names_to_rgb'),
         ('dcim', '0022_color_names_to_rgb'),
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
     ]
     ]
 
 
     operations = [
     operations = [
@@ -94,10 +93,15 @@ class Migration(migrations.Migration):
             name='site',
             name='site',
             field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='devices', to='dcim.Site'),
             field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='devices', to='dcim.Site'),
         ),
         ),
-        migrations.AddField(
+        migrations.AlterField(
             model_name='interface',
             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(
         migrations.CreateModel(
             name='Region',
             name='Region',
@@ -157,7 +161,17 @@ class Migration(migrations.Migration):
         migrations.AlterField(
         migrations.AlterField(
             model_name='device',
             model_name='device',
             name='status',
             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(
         migrations.AlterField(
             model_name='consoleport',
             model_name='consoleport',
@@ -199,6 +213,11 @@ class Migration(migrations.Migration):
             name='serial',
             name='serial',
             field=models.CharField(blank=True, max_length=50, verbose_name='Serial number'),
             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(
         migrations.AlterField(
             model_name='devicebay',
             model_name='devicebay',
             name='name',
             name='name',
@@ -244,6 +263,16 @@ class Migration(migrations.Migration):
             name='u_height',
             name='u_height',
             field=models.PositiveSmallIntegerField(default=1, verbose_name='Height (U)'),
             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(
         migrations.AlterField(
             model_name='interface',
             model_name='interface',
             name='mac_address',
             name='mac_address',
@@ -259,6 +288,11 @@ class Migration(migrations.Migration):
             name='connection_status',
             name='connection_status',
             field=models.BooleanField(choices=[[False, 'Planned'], [True, 'Connected']], default=True, verbose_name='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(
         migrations.AlterField(
             model_name='interfacetemplate',
             model_name='interfacetemplate',
             name='mgmt_only',
             name='mgmt_only',
@@ -329,6 +363,16 @@ class Migration(migrations.Migration):
             name='contact_email',
             name='contact_email',
             field=models.EmailField(blank=True, max_length=254, verbose_name='Contact E-mail'),
             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(
         migrations.AddField(
             model_name='interface',
             model_name='interface',
             name='enabled',
             name='enabled',

+ 0 - 144
netbox/dcim/migrations/0044_virtualization_squashed_0055_virtualchassis_ordering.py

@@ -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),
-        ),
-    ]

+ 354 - 0
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'),
+        ),
+    ]

+ 124 - 0
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'),
+        ),
+    ]

+ 146 - 0
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'),
+        ),
+    ]

+ 839 - 0
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),
+        ),
+    ]

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

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

+ 35 - 0
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
+        ),
+
+    ]

+ 92 - 0
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),
+        ),
+
+    ]

+ 39 - 0
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),
+        ),
+
+    ]

+ 65 - 0
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
+        ),
+
+    ]

+ 147 - 0
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),
+        ),
+
+    ]

+ 93 - 0
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
+        ),
+    ]

+ 106 - 0
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),
+        ),
+
+    ]

+ 100 - 0
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
+        ),
+
+    ]

+ 62 - 0
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),
+        ),
+
+    ]

+ 23 - 0
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')},
+        ),
+    ]

+ 23 - 0
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),
+        ),
+    ]

+ 18 - 0
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),
+        ),
+    ]

+ 21 - 0
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')},
+        ),
+    ]

+ 24 - 0
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'),
+        ),
+    ]

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 752 - 1786
netbox/dcim/models/__init__.py


+ 400 - 0
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
+        )

+ 1019 - 0
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,
+        )

+ 159 - 21
netbox/dcim/tables.py

@@ -156,10 +156,6 @@ DEVICE_PRIMARY_IP = """
 {{ record.primary_ip4.address.ip|default:"" }}
 {{ record.primary_ip4.address.ip|default:"" }}
 """
 """
 
 
-SUBDEVICE_ROLE_TEMPLATE = """
-{% if record.subdevice_role == True %}Parent{% elif record.subdevice_role == False %}Child{% else %}&mdash;{% endif %}
-"""
-
 DEVICETYPE_INSTANCES_TEMPLATE = """
 DEVICETYPE_INSTANCES_TEMPLATE = """
 <a href="{% url 'dcim:device_list' %}?manufacturer_id={{ record.manufacturer_id }}&device_type_id={{ record.pk }}">{{ record.instance_count }}</a>
 <a href="{% url 'dcim:device_list' %}?manufacturer_id={{ record.manufacturer_id }}&device_type_id={{ record.pk }}">{{ record.instance_count }}</a>
 """
 """
@@ -276,16 +272,17 @@ class RackGroupTable(BaseTable):
 
 
 class RackRoleTable(BaseTable):
 class RackRoleTable(BaseTable):
     pk = ToggleColumn()
     pk = ToggleColumn()
-    name = tables.LinkColumn(verbose_name='Name')
     rack_count = tables.Column(verbose_name='Racks')
     rack_count = tables.Column(verbose_name='Racks')
-    color = tables.TemplateColumn(COLOR_LABEL, 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):
     class Meta(BaseTable.Meta):
         model = RackRole
         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'
         verbose_name='Device Type'
     )
     )
     is_full_depth = BooleanColumn(verbose_name='Full Depth')
     is_full_depth = BooleanColumn(verbose_name='Full Depth')
-    subdevice_role = tables.TemplateColumn(
-        template_code=SUBDEVICE_ROLE_TEMPLATE,
-        verbose_name='Subdevice Role'
-    )
     instance_count = tables.TemplateColumn(
     instance_count = tables.TemplateColumn(
         template_code=DEVICETYPE_INSTANCES_TEMPLATE,
         template_code=DEVICETYPE_INSTANCES_TEMPLATE,
         verbose_name='Instances'
         verbose_name='Instances'
@@ -424,10 +417,19 @@ class ConsolePortTemplateTable(BaseTable):
 
 
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):
         model = ConsolePortTemplate
         model = ConsolePortTemplate
-        fields = ('pk', 'name', 'actions')
+        fields = ('pk', 'name', 'type', 'actions')
         empty_text = "None"
         empty_text = "None"
 
 
 
 
+class ConsolePortImportTable(BaseTable):
+    device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device')
+
+    class Meta(BaseTable.Meta):
+        model = ConsolePort
+        fields = ('device', 'name', 'description')
+        empty_text = False
+
+
 class ConsoleServerPortTemplateTable(BaseTable):
 class ConsoleServerPortTemplateTable(BaseTable):
     pk = ToggleColumn()
     pk = ToggleColumn()
     actions = tables.TemplateColumn(
     actions = tables.TemplateColumn(
@@ -442,6 +444,15 @@ class ConsoleServerPortTemplateTable(BaseTable):
         empty_text = "None"
         empty_text = "None"
 
 
 
 
+class ConsoleServerPortImportTable(BaseTable):
+    device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device')
+
+    class Meta(BaseTable.Meta):
+        model = ConsoleServerPort
+        fields = ('device', 'name', 'description')
+        empty_text = False
+
+
 class PowerPortTemplateTable(BaseTable):
 class PowerPortTemplateTable(BaseTable):
     pk = ToggleColumn()
     pk = ToggleColumn()
     actions = tables.TemplateColumn(
     actions = tables.TemplateColumn(
@@ -452,10 +463,19 @@ class PowerPortTemplateTable(BaseTable):
 
 
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):
         model = PowerPortTemplate
         model = PowerPortTemplate
-        fields = ('pk', 'name', 'maximum_draw', 'allocated_draw', 'actions')
+        fields = ('pk', 'name', 'type', 'maximum_draw', 'allocated_draw', 'actions')
         empty_text = "None"
         empty_text = "None"
 
 
 
 
+class PowerPortImportTable(BaseTable):
+    device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device')
+
+    class Meta(BaseTable.Meta):
+        model = PowerPort
+        fields = ('device', 'name', 'description', 'maximum_draw', 'allocated_draw')
+        empty_text = False
+
+
 class PowerOutletTemplateTable(BaseTable):
 class PowerOutletTemplateTable(BaseTable):
     pk = ToggleColumn()
     pk = ToggleColumn()
     actions = tables.TemplateColumn(
     actions = tables.TemplateColumn(
@@ -466,10 +486,19 @@ class PowerOutletTemplateTable(BaseTable):
 
 
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):
         model = PowerOutletTemplate
         model = PowerOutletTemplate
-        fields = ('pk', 'name', 'power_port', 'feed_leg', 'actions')
+        fields = ('pk', 'name', 'type', 'power_port', 'feed_leg', 'actions')
         empty_text = "None"
         empty_text = "None"
 
 
 
 
+class PowerOutletImportTable(BaseTable):
+    device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device')
+
+    class Meta(BaseTable.Meta):
+        model = PowerOutlet
+        fields = ('device', 'name', 'description', 'power_port', 'feed_leg')
+        empty_text = False
+
+
 class InterfaceTemplateTable(BaseTable):
 class InterfaceTemplateTable(BaseTable):
     pk = ToggleColumn()
     pk = ToggleColumn()
     mgmt_only = tables.TemplateColumn("{% if value %}OOB Management{% endif %}")
     mgmt_only = tables.TemplateColumn("{% if value %}OOB Management{% endif %}")
@@ -485,6 +514,16 @@ class InterfaceTemplateTable(BaseTable):
         empty_text = "None"
         empty_text = "None"
 
 
 
 
+class InterfaceImportTable(BaseTable):
+    device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device')
+    virtual_machine = tables.LinkColumn('virtualization:virtualmachine', args=[Accessor('virtual_machine.pk')], verbose_name='Virtual Machine')
+
+    class Meta(BaseTable.Meta):
+        model = Interface
+        fields = ('device', 'virtual_machine', 'name', 'description', 'lag', 'type', 'enabled', 'mac_address', 'mtu', 'mgmt_only', 'mode')
+        empty_text = False
+
+
 class FrontPortTemplateTable(BaseTable):
 class FrontPortTemplateTable(BaseTable):
     pk = ToggleColumn()
     pk = ToggleColumn()
     rear_port_position = tables.Column(
     rear_port_position = tables.Column(
@@ -502,6 +541,15 @@ class FrontPortTemplateTable(BaseTable):
         empty_text = "None"
         empty_text = "None"
 
 
 
 
+class FrontPortImportTable(BaseTable):
+    device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device')
+
+    class Meta(BaseTable.Meta):
+        model = FrontPort
+        fields = ('device', 'name', 'description', 'type', 'rear_port', 'rear_port_position')
+        empty_text = False
+
+
 class RearPortTemplateTable(BaseTable):
 class RearPortTemplateTable(BaseTable):
     pk = ToggleColumn()
     pk = ToggleColumn()
     actions = tables.TemplateColumn(
     actions = tables.TemplateColumn(
@@ -516,6 +564,15 @@ class RearPortTemplateTable(BaseTable):
         empty_text = "None"
         empty_text = "None"
 
 
 
 
+class RearPortImportTable(BaseTable):
+    device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device')
+
+    class Meta(BaseTable.Meta):
+        model = RearPort
+        fields = ('device', 'name', 'description', 'type', 'position')
+        empty_text = False
+
+
 class DeviceBayTemplateTable(BaseTable):
 class DeviceBayTemplateTable(BaseTable):
     pk = ToggleColumn()
     pk = ToggleColumn()
     actions = tables.TemplateColumn(
     actions = tables.TemplateColumn(
@@ -558,7 +615,7 @@ class DeviceRoleTable(BaseTable):
 
 
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):
         model = DeviceRole
         model = DeviceRole
-        fields = ('pk', 'name', 'device_count', 'vm_count', 'color', 'vm_role', 'slug', 'actions')
+        fields = ('pk', 'name', 'device_count', 'vm_count', 'color', 'vm_role', 'description', 'slug', 'actions')
 
 
 
 
 #
 #
@@ -645,11 +702,28 @@ class DeviceImportTable(BaseTable):
 # Device components
 # Device components
 #
 #
 
 
+class DeviceComponentDetailTable(BaseTable):
+    pk = ToggleColumn()
+    cable = tables.LinkColumn()
+
+    class Meta(BaseTable.Meta):
+        order_by = ('device', 'name')
+        fields = ('pk', 'device', 'name', 'type', 'description', 'cable')
+        sequence = ('pk', 'device', 'name', 'type', 'description', 'cable')
+
+
 class ConsolePortTable(BaseTable):
 class ConsolePortTable(BaseTable):
 
 
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):
         model = ConsolePort
         model = ConsolePort
-        fields = ('name',)
+        fields = ('name', 'type')
+
+
+class ConsolePortDetailTable(DeviceComponentDetailTable):
+    device = tables.LinkColumn()
+
+    class Meta(DeviceComponentDetailTable.Meta, ConsolePortTable.Meta):
+        pass
 
 
 
 
 class ConsoleServerPortTable(BaseTable):
 class ConsoleServerPortTable(BaseTable):
@@ -659,18 +733,39 @@ class ConsoleServerPortTable(BaseTable):
         fields = ('name', 'description')
         fields = ('name', 'description')
 
 
 
 
+class ConsoleServerPortDetailTable(DeviceComponentDetailTable):
+    device = tables.LinkColumn()
+
+    class Meta(DeviceComponentDetailTable.Meta, ConsoleServerPortTable.Meta):
+        pass
+
+
 class PowerPortTable(BaseTable):
 class PowerPortTable(BaseTable):
 
 
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):
         model = PowerPort
         model = PowerPort
-        fields = ('name',)
+        fields = ('name', 'type')
+
+
+class PowerPortDetailTable(DeviceComponentDetailTable):
+    device = tables.LinkColumn()
+
+    class Meta(DeviceComponentDetailTable.Meta, PowerPortTable.Meta):
+        pass
 
 
 
 
 class PowerOutletTable(BaseTable):
 class PowerOutletTable(BaseTable):
 
 
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):
         model = PowerOutlet
         model = PowerOutlet
-        fields = ('name', 'description')
+        fields = ('name', 'type', 'description')
+
+
+class PowerOutletDetailTable(DeviceComponentDetailTable):
+    device = tables.LinkColumn()
+
+    class Meta(DeviceComponentDetailTable.Meta, PowerOutletTable.Meta):
+        pass
 
 
 
 
 class InterfaceTable(BaseTable):
 class InterfaceTable(BaseTable):
@@ -680,6 +775,15 @@ class InterfaceTable(BaseTable):
         fields = ('name', 'type', 'lag', 'enabled', 'mgmt_only', 'description')
         fields = ('name', 'type', 'lag', 'enabled', 'mgmt_only', 'description')
 
 
 
 
+class InterfaceDetailTable(DeviceComponentDetailTable):
+    parent = tables.LinkColumn(order_by=('device', 'virtual_machine'))
+
+    class Meta(InterfaceTable.Meta):
+        order_by = ('parent', 'name')
+        fields = ('pk', 'parent', 'name', 'type', 'description', 'cable')
+        sequence = ('pk', 'parent', 'name', 'type', 'description', 'cable')
+
+
 class FrontPortTable(BaseTable):
 class FrontPortTable(BaseTable):
 
 
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):
@@ -688,6 +792,13 @@ class FrontPortTable(BaseTable):
         empty_text = "None"
         empty_text = "None"
 
 
 
 
+class FrontPortDetailTable(DeviceComponentDetailTable):
+    device = tables.LinkColumn()
+
+    class Meta(DeviceComponentDetailTable.Meta, FrontPortTable.Meta):
+        pass
+
+
 class RearPortTable(BaseTable):
 class RearPortTable(BaseTable):
 
 
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):
@@ -696,6 +807,13 @@ class RearPortTable(BaseTable):
         empty_text = "None"
         empty_text = "None"
 
 
 
 
+class RearPortDetailTable(DeviceComponentDetailTable):
+    device = tables.LinkColumn()
+
+    class Meta(DeviceComponentDetailTable.Meta, RearPortTable.Meta):
+        pass
+
+
 class DeviceBayTable(BaseTable):
 class DeviceBayTable(BaseTable):
 
 
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):
@@ -703,6 +821,26 @@ class DeviceBayTable(BaseTable):
         fields = ('name',)
         fields = ('name',)
 
 
 
 
+class DeviceBayDetailTable(DeviceComponentDetailTable):
+    device = tables.LinkColumn()
+    installed_device = tables.LinkColumn()
+
+    class Meta(DeviceBayTable.Meta):
+        fields = ('pk', 'name', 'device', 'installed_device')
+        sequence = ('pk', 'name', 'device', 'installed_device')
+        exclude = ('cable',)
+
+
+class DeviceBayImportTable(BaseTable):
+    device = tables.LinkColumn('dcim:device', args=[Accessor('device.pk')], verbose_name='Device')
+    installed_device = tables.LinkColumn('dcim:device', args=[Accessor('installed_device.pk')], verbose_name='Installed Device')
+
+    class Meta(BaseTable.Meta):
+        model = DeviceBay
+        fields = ('device', 'name', 'installed_device', 'description')
+        empty_text = False
+
+
 #
 #
 # Cables
 # Cables
 #
 #

+ 146 - 53
netbox/dcim/tests/test_api.py

@@ -1,8 +1,10 @@
+from django.contrib.contenttypes.models import ContentType
 from django.urls import reverse
 from django.urls import reverse
 from netaddr import IPNetwork
 from netaddr import IPNetwork
 from rest_framework import status
 from rest_framework import status
 
 
 from circuits.models import Circuit, CircuitTermination, CircuitType, Provider
 from circuits.models import Circuit, CircuitTermination, CircuitType, Provider
+from dcim.choices import *
 from dcim.constants import *
 from dcim.constants import *
 from dcim.models import (
 from dcim.models import (
     Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
     Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
@@ -11,11 +13,94 @@ from dcim.models import (
     Rack, RackGroup, RackReservation, RackRole, RearPort, Region, Site, VirtualChassis,
     Rack, RackGroup, RackReservation, RackRole, RearPort, Region, Site, VirtualChassis,
 )
 )
 from ipam.models import IPAddress, VLAN
 from ipam.models import IPAddress, VLAN
-from extras.models import Graph, 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
 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):
 class RegionTest(APITestCase):
 
 
     def setUp(self):
     def setUp(self):
@@ -138,16 +223,20 @@ class SiteTest(APITestCase):
 
 
     def test_get_site_graphs(self):
     def test_get_site_graphs(self):
 
 
+        site_ct = ContentType.objects.get_for_model(Site)
         self.graph1 = Graph.objects.create(
         self.graph1 = Graph.objects.create(
-            type=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'
             source='http://example.com/graphs.py?site={{ obj.slug }}&foo=1'
         )
         )
         self.graph2 = Graph.objects.create(
         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'
             source='http://example.com/graphs.py?site={{ obj.slug }}&foo=2'
         )
         )
         self.graph3 = Graph.objects.create(
         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'
             source='http://example.com/graphs.py?site={{ obj.slug }}&foo=3'
         )
         )
 
 
@@ -180,7 +269,7 @@ class SiteTest(APITestCase):
             'name': 'Test Site 4',
             'name': 'Test Site 4',
             'slug': 'test-site-4',
             'slug': 'test-site-4',
             'region': self.region1.pk,
             'region': self.region1.pk,
-            'status': SITE_STATUS_ACTIVE,
+            'status': SiteStatusChoices.STATUS_ACTIVE,
         }
         }
 
 
         url = reverse('dcim-api:site-list')
         url = reverse('dcim-api:site-list')
@@ -200,19 +289,19 @@ class SiteTest(APITestCase):
                 'name': 'Test Site 4',
                 'name': 'Test Site 4',
                 'slug': 'test-site-4',
                 'slug': 'test-site-4',
                 'region': self.region1.pk,
                 'region': self.region1.pk,
-                'status': SITE_STATUS_ACTIVE,
+                'status': SiteStatusChoices.STATUS_ACTIVE,
             },
             },
             {
             {
                 'name': 'Test Site 5',
                 'name': 'Test Site 5',
                 'slug': 'test-site-5',
                 'slug': 'test-site-5',
                 'region': self.region1.pk,
                 'region': self.region1.pk,
-                'status': SITE_STATUS_ACTIVE,
+                'status': SiteStatusChoices.STATUS_ACTIVE,
             },
             },
             {
             {
                 'name': 'Test Site 6',
                 'name': 'Test Site 6',
                 'slug': 'test-site-6',
                 'slug': 'test-site-6',
                 'region': self.region1.pk,
                 'region': self.region1.pk,
-                'status': SITE_STATUS_ACTIVE,
+                'status': SiteStatusChoices.STATUS_ACTIVE,
             },
             },
         ]
         ]
 
 
@@ -2416,16 +2505,20 @@ class InterfaceTest(APITestCase):
 
 
     def test_get_interface_graphs(self):
     def test_get_interface_graphs(self):
 
 
+        interface_ct = ContentType.objects.get_for_model(Interface)
         self.graph1 = Graph.objects.create(
         self.graph1 = Graph.objects.create(
-            type=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'
             source='http://example.com/graphs.py?interface={{ obj.name }}&foo=1'
         )
         )
         self.graph2 = Graph.objects.create(
         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'
             source='http://example.com/graphs.py?interface={{ obj.name }}&foo=2'
         )
         )
         self.graph3 = Graph.objects.create(
         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'
             source='http://example.com/graphs.py?interface={{ obj.name }}&foo=3'
         )
         )
 
 
@@ -2473,7 +2566,7 @@ class InterfaceTest(APITestCase):
         data = {
         data = {
             'device': self.device.pk,
             'device': self.device.pk,
             'name': 'Test Interface 4',
             'name': 'Test Interface 4',
-            'mode': IFACE_MODE_TAGGED,
+            'mode': InterfaceModeChoices.MODE_TAGGED,
             'untagged_vlan': self.vlan3.id,
             'untagged_vlan': self.vlan3.id,
             'tagged_vlans': [self.vlan1.id, self.vlan2.id],
             'tagged_vlans': [self.vlan1.id, self.vlan2.id],
         }
         }
@@ -2520,21 +2613,21 @@ class InterfaceTest(APITestCase):
             {
             {
                 'device': self.device.pk,
                 'device': self.device.pk,
                 'name': 'Test Interface 4',
                 'name': 'Test Interface 4',
-                'mode': IFACE_MODE_TAGGED,
+                'mode': InterfaceModeChoices.MODE_TAGGED,
                 'untagged_vlan': self.vlan2.id,
                 'untagged_vlan': self.vlan2.id,
                 'tagged_vlans': [self.vlan1.id],
                 'tagged_vlans': [self.vlan1.id],
             },
             },
             {
             {
                 'device': self.device.pk,
                 'device': self.device.pk,
                 'name': 'Test Interface 5',
                 'name': 'Test Interface 5',
-                'mode': IFACE_MODE_TAGGED,
+                'mode': InterfaceModeChoices.MODE_TAGGED,
                 'untagged_vlan': self.vlan2.id,
                 'untagged_vlan': self.vlan2.id,
                 'tagged_vlans': [self.vlan1.id],
                 'tagged_vlans': [self.vlan1.id],
             },
             },
             {
             {
                 'device': self.device.pk,
                 'device': self.device.pk,
                 'name': 'Test Interface 6',
                 'name': 'Test Interface 6',
-                'mode': IFACE_MODE_TAGGED,
+                'mode': InterfaceModeChoices.MODE_TAGGED,
                 'untagged_vlan': self.vlan2.id,
                 'untagged_vlan': self.vlan2.id,
                 'tagged_vlans': [self.vlan1.id],
                 'tagged_vlans': [self.vlan1.id],
             },
             },
@@ -2553,7 +2646,7 @@ class InterfaceTest(APITestCase):
     def test_update_interface(self):
     def test_update_interface(self):
 
 
         lag_interface = Interface.objects.create(
         lag_interface = Interface.objects.create(
-            device=self.device, name='Test LAG Interface', type=IFACE_TYPE_LAG
+            device=self.device, name='Test LAG Interface', type=InterfaceTypeChoices.TYPE_LAG
         )
         )
 
 
         data = {
         data = {
@@ -2590,11 +2683,11 @@ class DeviceBayTest(APITestCase):
         manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
         manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
         self.devicetype1 = DeviceType.objects.create(
         self.devicetype1 = DeviceType.objects.create(
             manufacturer=manufacturer, model='Parent Device Type', slug='parent-device-type',
             manufacturer=manufacturer, model='Parent Device Type', slug='parent-device-type',
-            subdevice_role=SUBDEVICE_ROLE_PARENT
+            subdevice_role=SubdeviceRoleChoices.ROLE_PARENT
         )
         )
         self.devicetype2 = DeviceType.objects.create(
         self.devicetype2 = DeviceType.objects.create(
             manufacturer=manufacturer, model='Child Device Type', slug='child-device-type',
             manufacturer=manufacturer, model='Child Device Type', slug='child-device-type',
-            subdevice_role=SUBDEVICE_ROLE_CHILD
+            subdevice_role=SubdeviceRoleChoices.ROLE_CHILD
         )
         )
         devicerole = DeviceRole.objects.create(
         devicerole = DeviceRole.objects.create(
             name='Test Device Role 1', slug='test-device-role-1', color='ff0000'
             name='Test Device Role 1', slug='test-device-role-1', color='ff0000'
@@ -2841,7 +2934,7 @@ class CableTest(APITestCase):
         )
         )
         for device in [self.device1, self.device2]:
         for device in [self.device1, self.device2]:
             for i in range(0, 10):
             for i in range(0, 10):
-                Interface(device=device, type=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(
         self.cable1 = Cable(
             termination_a=self.device1.interfaces.get(name='eth0'),
             termination_a=self.device1.interfaces.get(name='eth0'),
@@ -2885,7 +2978,7 @@ class CableTest(APITestCase):
             'termination_a_id': interface_a.pk,
             'termination_a_id': interface_a.pk,
             'termination_b_type': 'dcim.interface',
             'termination_b_type': 'dcim.interface',
             'termination_b_id': interface_b.pk,
             'termination_b_id': interface_b.pk,
-            'status': CONNECTION_STATUS_PLANNED,
+            'status': CableStatusChoices.STATUS_PLANNED,
             'label': 'Test Cable 4',
             'label': 'Test Cable 4',
         }
         }
 
 
@@ -2939,7 +3032,7 @@ class CableTest(APITestCase):
 
 
         data = {
         data = {
             'label': 'Test Cable X',
             'label': 'Test Cable X',
-            'status': CONNECTION_STATUS_CONNECTED,
+            'status': CableStatusChoices.STATUS_CONNECTED,
         }
         }
 
 
         url = reverse('dcim-api:cable-detail', kwargs={'pk': self.cable1.pk})
         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'
             device=self.device2, name='Test Console Server Port 1'
         )
         )
         rearport1 = RearPort.objects.create(
         rearport1 = RearPort.objects.create(
-            device=self.panel1, name='Test Rear Port 1', type=PORT_TYPE_8P8C
+            device=self.panel1, name='Test Rear Port 1', type=PortTypeChoices.TYPE_8P8C
         )
         )
         frontport1 = FrontPort.objects.create(
         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(
         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(
         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')
         url = reverse('dcim-api:cable-list')
@@ -3161,16 +3254,16 @@ class ConnectionTest(APITestCase):
             device=self.device2, name='Test Interface 2'
             device=self.device2, name='Test Interface 2'
         )
         )
         rearport1 = RearPort.objects.create(
         rearport1 = RearPort.objects.create(
-            device=self.panel1, name='Test Rear Port 1', type=PORT_TYPE_8P8C
+            device=self.panel1, name='Test Rear Port 1', type=PortTypeChoices.TYPE_8P8C
         )
         )
         frontport1 = FrontPort.objects.create(
         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(
         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(
         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')
         url = reverse('dcim-api:cable-list')
@@ -3272,16 +3365,16 @@ class ConnectionTest(APITestCase):
             circuit=circuit, term_side='A', site=self.site, port_speed=10000
             circuit=circuit, term_side='A', site=self.site, port_speed=10000
         )
         )
         rearport1 = RearPort.objects.create(
         rearport1 = RearPort.objects.create(
-            device=self.panel1, name='Test Rear Port 1', type=PORT_TYPE_8P8C
+            device=self.panel1, name='Test Rear Port 1', type=PortTypeChoices.TYPE_8P8C
         )
         )
         frontport1 = FrontPort.objects.create(
         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(
         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(
         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')
         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
             device_type=device_type, device_role=device_role, name='StackSwitch9', site=site
         )
         )
         for i in range(0, 13):
         for i in range(0, 13):
-            Interface.objects.create(device=self.device1, name='1/{}'.format(i), type=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):
         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):
         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):
         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):
         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):
         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):
         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):
         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):
         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
         # Create two VirtualChassis with three members each
         self.vc1 = VirtualChassis.objects.create(master=self.device1, domain='test-domain-1')
         self.vc1 = VirtualChassis.objects.create(master=self.device1, domain='test-domain-1')
@@ -3678,22 +3771,22 @@ class PowerFeedTest(APITestCase):
             site=self.site1, rack_group=self.rackgroup1, name='Test Power Panel 2'
             site=self.site1, rack_group=self.rackgroup1, name='Test Power Panel 2'
         )
         )
         self.powerfeed1 = PowerFeed.objects.create(
         self.powerfeed1 = PowerFeed.objects.create(
-            power_panel=self.powerpanel1, rack=self.rack1, name='Test Power Feed 1A', type=POWERFEED_TYPE_PRIMARY
+            power_panel=self.powerpanel1, rack=self.rack1, name='Test Power Feed 1A', type=PowerFeedTypeChoices.TYPE_PRIMARY
         )
         )
         self.powerfeed2 = PowerFeed.objects.create(
         self.powerfeed2 = PowerFeed.objects.create(
-            power_panel=self.powerpanel2, rack=self.rack1, name='Test Power Feed 1B', type=POWERFEED_TYPE_REDUNDANT
+            power_panel=self.powerpanel2, rack=self.rack1, name='Test Power Feed 1B', type=PowerFeedTypeChoices.TYPE_REDUNDANT
         )
         )
         self.powerfeed3 = PowerFeed.objects.create(
         self.powerfeed3 = PowerFeed.objects.create(
-            power_panel=self.powerpanel1, rack=self.rack2, name='Test Power Feed 2A', type=POWERFEED_TYPE_PRIMARY
+            power_panel=self.powerpanel1, rack=self.rack2, name='Test Power Feed 2A', type=PowerFeedTypeChoices.TYPE_PRIMARY
         )
         )
         self.powerfeed4 = PowerFeed.objects.create(
         self.powerfeed4 = PowerFeed.objects.create(
-            power_panel=self.powerpanel2, rack=self.rack2, name='Test Power Feed 2B', type=POWERFEED_TYPE_REDUNDANT
+            power_panel=self.powerpanel2, rack=self.rack2, name='Test Power Feed 2B', type=PowerFeedTypeChoices.TYPE_REDUNDANT
         )
         )
         self.powerfeed5 = PowerFeed.objects.create(
         self.powerfeed5 = PowerFeed.objects.create(
-            power_panel=self.powerpanel1, rack=self.rack3, name='Test Power Feed 3A', type=POWERFEED_TYPE_PRIMARY
+            power_panel=self.powerpanel1, rack=self.rack3, name='Test Power Feed 3A', type=PowerFeedTypeChoices.TYPE_PRIMARY
         )
         )
         self.powerfeed6 = PowerFeed.objects.create(
         self.powerfeed6 = PowerFeed.objects.create(
-            power_panel=self.powerpanel2, rack=self.rack3, name='Test Power Feed 3B', type=POWERFEED_TYPE_REDUNDANT
+            power_panel=self.powerpanel2, rack=self.rack3, name='Test Power Feed 3B', type=PowerFeedTypeChoices.TYPE_REDUNDANT
         )
         )
 
 
     def test_get_powerfeed(self):
     def test_get_powerfeed(self):
@@ -3726,7 +3819,7 @@ class PowerFeedTest(APITestCase):
             'name': 'Test Power Feed 4A',
             'name': 'Test Power Feed 4A',
             'power_panel': self.powerpanel1.pk,
             'power_panel': self.powerpanel1.pk,
             'rack': self.rack4.pk,
             'rack': self.rack4.pk,
-            'type': POWERFEED_TYPE_PRIMARY,
+            'type': PowerFeedTypeChoices.TYPE_PRIMARY,
         }
         }
 
 
         url = reverse('dcim-api:powerfeed-list')
         url = reverse('dcim-api:powerfeed-list')
@@ -3746,13 +3839,13 @@ class PowerFeedTest(APITestCase):
                 'name': 'Test Power Feed 4A',
                 'name': 'Test Power Feed 4A',
                 'power_panel': self.powerpanel1.pk,
                 'power_panel': self.powerpanel1.pk,
                 'rack': self.rack4.pk,
                 'rack': self.rack4.pk,
-                'type': POWERFEED_TYPE_PRIMARY,
+                'type': PowerFeedTypeChoices.TYPE_PRIMARY,
             },
             },
             {
             {
                 'name': 'Test Power Feed 4B',
                 'name': 'Test Power Feed 4B',
                 'power_panel': self.powerpanel1.pk,
                 'power_panel': self.powerpanel1.pk,
                 'rack': self.rack4.pk,
                 'rack': self.rack4.pk,
-                'type': POWERFEED_TYPE_REDUNDANT,
+                'type': PowerFeedTypeChoices.TYPE_REDUNDANT,
             },
             },
         ]
         ]
 
 
@@ -3769,7 +3862,7 @@ class PowerFeedTest(APITestCase):
         data = {
         data = {
             'name': 'Test Power Feed X',
             'name': 'Test Power Feed X',
             'rack': self.rack4.pk,
             'rack': self.rack4.pk,
-            'type': POWERFEED_TYPE_REDUNDANT,
+            'type': PowerFeedTypeChoices.TYPE_REDUNDANT,
         }
         }
 
 
         url = reverse('dcim-api:powerfeed-detail', kwargs={'pk': self.powerfeed1.pk})
         url = reverse('dcim-api:powerfeed-detail', kwargs={'pk': self.powerfeed1.pk})

+ 146 - 144
netbox/dcim/tests/test_filters.py

@@ -1,7 +1,7 @@
 from django.contrib.auth.models import User
 from django.contrib.auth.models import User
 from django.test import TestCase
 from django.test import TestCase
 
 
-from dcim.constants import *
+from dcim.choices import *
 from dcim.filters import *
 from dcim.filters import *
 from dcim.models import (
 from dcim.models import (
     Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
     Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
@@ -43,27 +43,27 @@ class RegionTestCase(TestCase):
     def test_id(self):
     def test_id(self):
         id_list = self.queryset.values_list('id', flat=True)[:2]
         id_list = self.queryset.values_list('id', flat=True)[:2]
         params = {'id': [str(id) for id in id_list]}
         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):
     def test_name(self):
         params = {'name': ['Region 1', 'Region 2']}
         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):
     def test_slug(self):
         params = {'slug': ['region-1', 'region-2']}
         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):
     def test_parent(self):
         parent_regions = Region.objects.filter(parent__isnull=True)[:2]
         parent_regions = Region.objects.filter(parent__isnull=True)[:2]
         params = {'parent_id': [parent_regions[0].pk, parent_regions[1].pk]}
         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]}
         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):
 class SiteTestCase(TestCase):
     queryset = Site.objects.all()
     queryset = Site.objects.all()
-    filterset = SiteFilter
+    filterset = SiteFilterSet
 
 
     @classmethod
     @classmethod
     def setUpTestData(cls):
     def setUpTestData(cls):
@@ -77,9 +77,9 @@ class SiteTestCase(TestCase):
             region.save()
             region.save()
 
 
         sites = (
         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)
         Site.objects.bulk_create(sites)
 
 
@@ -130,7 +130,7 @@ class SiteTestCase(TestCase):
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
 
 
     def test_status(self):
     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)
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
 
 
     def test_region(self):
     def test_region(self):
@@ -143,7 +143,7 @@ class SiteTestCase(TestCase):
 
 
 class RackGroupTestCase(TestCase):
 class RackGroupTestCase(TestCase):
     queryset = RackGroup.objects.all()
     queryset = RackGroup.objects.all()
-    filterset = RackGroupFilter
+    filterset = RackGroupFilterSet
 
 
     @classmethod
     @classmethod
     def setUpTestData(cls):
     def setUpTestData(cls):
@@ -200,7 +200,7 @@ class RackGroupTestCase(TestCase):
 
 
 class RackRoleTestCase(TestCase):
 class RackRoleTestCase(TestCase):
     queryset = RackRole.objects.all()
     queryset = RackRole.objects.all()
-    filterset = RackRoleFilter
+    filterset = RackRoleFilterSet
 
 
     @classmethod
     @classmethod
     def setUpTestData(cls):
     def setUpTestData(cls):
@@ -232,7 +232,7 @@ class RackRoleTestCase(TestCase):
 
 
 class RackTestCase(TestCase):
 class RackTestCase(TestCase):
     queryset = Rack.objects.all()
     queryset = Rack.objects.all()
-    filterset = RackFilter
+    filterset = RackFilterSet
 
 
     @classmethod
     @classmethod
     def setUpTestData(cls):
     def setUpTestData(cls):
@@ -267,9 +267,9 @@ class RackTestCase(TestCase):
         RackRole.objects.bulk_create(rack_roles)
         RackRole.objects.bulk_create(rack_roles)
 
 
         racks = (
         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)
         Rack.objects.bulk_create(racks)
 
 
@@ -292,12 +292,12 @@ class RackTestCase(TestCase):
 
 
     def test_type(self):
     def test_type(self):
         # TODO: Test for multiple values
         # 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)
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
 
 
     def test_width(self):
     def test_width(self):
         # TODO: Test for multiple values
         # 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)
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
 
 
     def test_u_height(self):
     def test_u_height(self):
@@ -320,7 +320,7 @@ class RackTestCase(TestCase):
 
 
     def test_outer_unit(self):
     def test_outer_unit(self):
         self.assertEqual(Rack.objects.filter(outer_unit__isnull=False).count(), 3)
         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)
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
 
 
     def test_id__in(self):
     def test_id__in(self):
@@ -350,7 +350,7 @@ class RackTestCase(TestCase):
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
 
 
     def test_status(self):
     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)
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
 
 
     def test_role(self):
     def test_role(self):
@@ -369,7 +369,7 @@ class RackTestCase(TestCase):
 
 
 class RackReservationTestCase(TestCase):
 class RackReservationTestCase(TestCase):
     queryset = RackReservation.objects.all()
     queryset = RackReservation.objects.all()
-    filterset = RackReservationFilter
+    filterset = RackReservationFilterSet
 
 
     @classmethod
     @classmethod
     def setUpTestData(cls):
     def setUpTestData(cls):
@@ -439,7 +439,7 @@ class RackReservationTestCase(TestCase):
 
 
 class ManufacturerTestCase(TestCase):
 class ManufacturerTestCase(TestCase):
     queryset = Manufacturer.objects.all()
     queryset = Manufacturer.objects.all()
-    filterset = ManufacturerFilter
+    filterset = ManufacturerFilterSet
 
 
     @classmethod
     @classmethod
     def setUpTestData(cls):
     def setUpTestData(cls):
@@ -467,7 +467,7 @@ class ManufacturerTestCase(TestCase):
 
 
 class DeviceTypeTestCase(TestCase):
 class DeviceTypeTestCase(TestCase):
     queryset = DeviceType.objects.all()
     queryset = DeviceType.objects.all()
-    filterset = DeviceTypeFilter
+    filterset = DeviceTypeFilterSet
 
 
     @classmethod
     @classmethod
     def setUpTestData(cls):
     def setUpTestData(cls):
@@ -480,9 +480,9 @@ class DeviceTypeTestCase(TestCase):
         Manufacturer.objects.bulk_create(manufacturers)
         Manufacturer.objects.bulk_create(manufacturers)
 
 
         device_types = (
         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)
         DeviceType.objects.bulk_create(device_types)
 
 
@@ -508,13 +508,13 @@ class DeviceTypeTestCase(TestCase):
             InterfaceTemplate(device_type=device_types[1], name='Interface 2'),
             InterfaceTemplate(device_type=device_types[1], name='Interface 2'),
         ))
         ))
         rear_ports = (
         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)
         RearPortTemplate.objects.bulk_create(rear_ports)
         FrontPortTemplate.objects.bulk_create((
         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.objects.bulk_create((
             DeviceBayTemplate(device_type=device_types[0], name='Device Bay 1'),
             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)
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
 
 
     def test_subdevice_role(self):
     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)
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
 
 
     def test_id__in(self):
     def test_id__in(self):
@@ -605,7 +605,7 @@ class DeviceTypeTestCase(TestCase):
 
 
 class ConsolePortTemplateTestCase(TestCase):
 class ConsolePortTemplateTestCase(TestCase):
     queryset = ConsolePortTemplate.objects.all()
     queryset = ConsolePortTemplate.objects.all()
-    filterset = ConsolePortTemplateFilter
+    filterset = ConsolePortTemplateFilterSet
 
 
     @classmethod
     @classmethod
     def setUpTestData(cls):
     def setUpTestData(cls):
@@ -642,7 +642,7 @@ class ConsolePortTemplateTestCase(TestCase):
 
 
 class ConsoleServerPortTemplateTestCase(TestCase):
 class ConsoleServerPortTemplateTestCase(TestCase):
     queryset = ConsoleServerPortTemplate.objects.all()
     queryset = ConsoleServerPortTemplate.objects.all()
-    filterset = ConsoleServerPortTemplateFilter
+    filterset = ConsoleServerPortTemplateFilterSet
 
 
     @classmethod
     @classmethod
     def setUpTestData(cls):
     def setUpTestData(cls):
@@ -679,7 +679,7 @@ class ConsoleServerPortTemplateTestCase(TestCase):
 
 
 class PowerPortTemplateTestCase(TestCase):
 class PowerPortTemplateTestCase(TestCase):
     queryset = PowerPortTemplate.objects.all()
     queryset = PowerPortTemplate.objects.all()
-    filterset = PowerPortTemplateFilter
+    filterset = PowerPortTemplateFilterSet
 
 
     @classmethod
     @classmethod
     def setUpTestData(cls):
     def setUpTestData(cls):
@@ -724,7 +724,7 @@ class PowerPortTemplateTestCase(TestCase):
 
 
 class PowerOutletTemplateTestCase(TestCase):
 class PowerOutletTemplateTestCase(TestCase):
     queryset = PowerOutletTemplate.objects.all()
     queryset = PowerOutletTemplate.objects.all()
-    filterset = PowerOutletTemplateFilter
+    filterset = PowerOutletTemplateFilterSet
 
 
     @classmethod
     @classmethod
     def setUpTestData(cls):
     def setUpTestData(cls):
@@ -739,9 +739,9 @@ class PowerOutletTemplateTestCase(TestCase):
         DeviceType.objects.bulk_create(device_types)
         DeviceType.objects.bulk_create(device_types)
 
 
         PowerOutletTemplate.objects.bulk_create((
         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):
     def test_id(self):
@@ -760,13 +760,13 @@ class PowerOutletTemplateTestCase(TestCase):
 
 
     def test_feed_leg(self):
     def test_feed_leg(self):
         # TODO: Support filtering for multiple values
         # 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)
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
 
 
 
 
 class InterfaceTemplateTestCase(TestCase):
 class InterfaceTemplateTestCase(TestCase):
     queryset = InterfaceTemplate.objects.all()
     queryset = InterfaceTemplate.objects.all()
-    filterset = InterfaceTemplateFilter
+    filterset = InterfaceTemplateFilterSet
 
 
     @classmethod
     @classmethod
     def setUpTestData(cls):
     def setUpTestData(cls):
@@ -781,9 +781,9 @@ class InterfaceTemplateTestCase(TestCase):
         DeviceType.objects.bulk_create(device_types)
         DeviceType.objects.bulk_create(device_types)
 
 
         InterfaceTemplate.objects.bulk_create((
         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):
     def test_id(self):
@@ -802,7 +802,7 @@ class InterfaceTemplateTestCase(TestCase):
 
 
     def test_type(self):
     def test_type(self):
         # TODO: Support filtering for multiple values
         # 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)
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
 
 
     def test_mgmt_only(self):
     def test_mgmt_only(self):
@@ -814,7 +814,7 @@ class InterfaceTemplateTestCase(TestCase):
 
 
 class FrontPortTemplateTestCase(TestCase):
 class FrontPortTemplateTestCase(TestCase):
     queryset = FrontPortTemplate.objects.all()
     queryset = FrontPortTemplate.objects.all()
-    filterset = FrontPortTemplateFilter
+    filterset = FrontPortTemplateFilterSet
 
 
     @classmethod
     @classmethod
     def setUpTestData(cls):
     def setUpTestData(cls):
@@ -829,16 +829,16 @@ class FrontPortTemplateTestCase(TestCase):
         DeviceType.objects.bulk_create(device_types)
         DeviceType.objects.bulk_create(device_types)
 
 
         rear_ports = (
         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)
         RearPortTemplate.objects.bulk_create(rear_ports)
 
 
         FrontPortTemplate.objects.bulk_create((
         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):
     def test_id(self):
@@ -857,13 +857,13 @@ class FrontPortTemplateTestCase(TestCase):
 
 
     def test_type(self):
     def test_type(self):
         # TODO: Support filtering for multiple values
         # 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)
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
 
 
 
 
 class RearPortTemplateTestCase(TestCase):
 class RearPortTemplateTestCase(TestCase):
     queryset = RearPortTemplate.objects.all()
     queryset = RearPortTemplate.objects.all()
-    filterset = RearPortTemplateFilter
+    filterset = RearPortTemplateFilterSet
 
 
     @classmethod
     @classmethod
     def setUpTestData(cls):
     def setUpTestData(cls):
@@ -878,9 +878,9 @@ class RearPortTemplateTestCase(TestCase):
         DeviceType.objects.bulk_create(device_types)
         DeviceType.objects.bulk_create(device_types)
 
 
         RearPortTemplate.objects.bulk_create((
         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):
     def test_id(self):
@@ -899,7 +899,7 @@ class RearPortTemplateTestCase(TestCase):
 
 
     def test_type(self):
     def test_type(self):
         # TODO: Support filtering for multiple values
         # 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)
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
 
 
     def test_positions(self):
     def test_positions(self):
@@ -909,7 +909,7 @@ class RearPortTemplateTestCase(TestCase):
 
 
 class DeviceBayTemplateTestCase(TestCase):
 class DeviceBayTemplateTestCase(TestCase):
     queryset = DeviceBayTemplate.objects.all()
     queryset = DeviceBayTemplate.objects.all()
-    filterset = DeviceBayTemplateFilter
+    filterset = DeviceBayTemplateFilterSet
 
 
     @classmethod
     @classmethod
     def setUpTestData(cls):
     def setUpTestData(cls):
@@ -946,7 +946,7 @@ class DeviceBayTemplateTestCase(TestCase):
 
 
 class DeviceRoleTestCase(TestCase):
 class DeviceRoleTestCase(TestCase):
     queryset = DeviceRole.objects.all()
     queryset = DeviceRole.objects.all()
-    filterset = DeviceRoleFilter
+    filterset = DeviceRoleFilterSet
 
 
     @classmethod
     @classmethod
     def setUpTestData(cls):
     def setUpTestData(cls):
@@ -984,7 +984,7 @@ class DeviceRoleTestCase(TestCase):
 
 
 class PlatformTestCase(TestCase):
 class PlatformTestCase(TestCase):
     queryset = Platform.objects.all()
     queryset = Platform.objects.all()
-    filterset = PlatformFilter
+    filterset = PlatformFilterSet
 
 
     @classmethod
     @classmethod
     def setUpTestData(cls):
     def setUpTestData(cls):
@@ -1030,7 +1030,7 @@ class PlatformTestCase(TestCase):
 
 
 class DeviceTestCase(TestCase):
 class DeviceTestCase(TestCase):
     queryset = Device.objects.all()
     queryset = Device.objects.all()
-    filterset = DeviceFilter
+    filterset = DeviceFilterSet
 
 
     @classmethod
     @classmethod
     def setUpTestData(cls):
     def setUpTestData(cls):
@@ -1101,9 +1101,9 @@ class DeviceTestCase(TestCase):
         Cluster.objects.bulk_create(clusters)
         Cluster.objects.bulk_create(clusters)
 
 
         devices = (
         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)
         Device.objects.bulk_create(devices)
 
 
@@ -1130,13 +1130,13 @@ class DeviceTestCase(TestCase):
         )
         )
         Interface.objects.bulk_create(interfaces)
         Interface.objects.bulk_create(interfaces)
         rear_ports = (
         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)
         RearPort.objects.bulk_create(rear_ports)
         FrontPort.objects.bulk_create((
         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.objects.bulk_create((
             DeviceBay(device=devices[0], name='Device Bay 1'),
             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)
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
 
 
     def test_face(self):
     def test_face(self):
-        params = {'face': RACK_FACE_FRONT}
+        params = {'face': DeviceFaceChoices.FACE_FRONT}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
 
 
     def test_position(self):
     def test_position(self):
@@ -1251,7 +1251,7 @@ class DeviceTestCase(TestCase):
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
 
 
     def test_status(self):
     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)
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
 
 
     def test_is_full_depth(self):
     def test_is_full_depth(self):
@@ -1338,7 +1338,7 @@ class DeviceTestCase(TestCase):
 
 
 class ConsolePortTestCase(TestCase):
 class ConsolePortTestCase(TestCase):
     queryset = ConsolePort.objects.all()
     queryset = ConsolePort.objects.all()
-    filterset = ConsolePortFilter
+    filterset = ConsolePortFilterSet
 
 
     @classmethod
     @classmethod
     def setUpTestData(cls):
     def setUpTestData(cls):
@@ -1408,7 +1408,7 @@ class ConsolePortTestCase(TestCase):
 
 
 class ConsoleServerPortTestCase(TestCase):
 class ConsoleServerPortTestCase(TestCase):
     queryset = ConsoleServerPort.objects.all()
     queryset = ConsoleServerPort.objects.all()
-    filterset = ConsoleServerPortFilter
+    filterset = ConsoleServerPortFilterSet
 
 
     @classmethod
     @classmethod
     def setUpTestData(cls):
     def setUpTestData(cls):
@@ -1478,7 +1478,7 @@ class ConsoleServerPortTestCase(TestCase):
 
 
 class PowerPortTestCase(TestCase):
 class PowerPortTestCase(TestCase):
     queryset = PowerPort.objects.all()
     queryset = PowerPort.objects.all()
-    filterset = PowerPortFilter
+    filterset = PowerPortFilterSet
 
 
     @classmethod
     @classmethod
     def setUpTestData(cls):
     def setUpTestData(cls):
@@ -1556,7 +1556,7 @@ class PowerPortTestCase(TestCase):
 
 
 class PowerOutletTestCase(TestCase):
 class PowerOutletTestCase(TestCase):
     queryset = PowerOutlet.objects.all()
     queryset = PowerOutlet.objects.all()
-    filterset = PowerOutletFilter
+    filterset = PowerOutletFilterSet
 
 
     @classmethod
     @classmethod
     def setUpTestData(cls):
     def setUpTestData(cls):
@@ -1581,9 +1581,9 @@ class PowerOutletTestCase(TestCase):
         PowerPort.objects.bulk_create(power_ports)
         PowerPort.objects.bulk_create(power_ports)
 
 
         power_outlets = (
         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)
         PowerOutlet.objects.bulk_create(power_outlets)
 
 
@@ -1607,7 +1607,7 @@ class PowerOutletTestCase(TestCase):
 
 
     def test_feed_leg(self):
     def test_feed_leg(self):
         # TODO: Support filtering for multiple values
         # 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)
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
 
 
     # TODO: Fix boolean value
     # TODO: Fix boolean value
@@ -1631,7 +1631,7 @@ class PowerOutletTestCase(TestCase):
 
 
 class InterfaceTestCase(TestCase):
 class InterfaceTestCase(TestCase):
     queryset = Interface.objects.all()
     queryset = Interface.objects.all()
-    filterset = InterfaceFilter
+    filterset = InterfaceFilterSet
 
 
     @classmethod
     @classmethod
     def setUpTestData(cls):
     def setUpTestData(cls):
@@ -1650,12 +1650,12 @@ class InterfaceTestCase(TestCase):
         Device.objects.bulk_create(devices)
         Device.objects.bulk_create(devices)
 
 
         interfaces = (
         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)
         Interface.objects.bulk_create(interfaces)
 
 
@@ -1695,7 +1695,7 @@ class InterfaceTestCase(TestCase):
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
 
 
     def test_mode(self):
     def test_mode(self):
-        params = {'mode': IFACE_MODE_ACCESS}
+        params = {'mode': InterfaceModeChoices.MODE_ACCESS}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
 
 
     def test_description(self):
     def test_description(self):
@@ -1726,13 +1726,13 @@ class InterfaceTestCase(TestCase):
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
 
 
     def test_type(self):
     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)
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
 
 
 
 
 class FrontPortTestCase(TestCase):
 class FrontPortTestCase(TestCase):
     queryset = FrontPort.objects.all()
     queryset = FrontPort.objects.all()
-    filterset = FrontPortFilter
+    filterset = FrontPortFilterSet
 
 
     @classmethod
     @classmethod
     def setUpTestData(cls):
     def setUpTestData(cls):
@@ -1751,22 +1751,22 @@ class FrontPortTestCase(TestCase):
         Device.objects.bulk_create(devices)
         Device.objects.bulk_create(devices)
 
 
         rear_ports = (
         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)
         RearPort.objects.bulk_create(rear_ports)
 
 
         front_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)
         FrontPort.objects.bulk_create(front_ports)
 
 
@@ -1786,7 +1786,7 @@ class FrontPortTestCase(TestCase):
 
 
     def test_type(self):
     def test_type(self):
         # TODO: Test for multiple values
         # 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)
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
 
 
     def test_description(self):
     def test_description(self):
@@ -1809,7 +1809,7 @@ class FrontPortTestCase(TestCase):
 
 
 class RearPortTestCase(TestCase):
 class RearPortTestCase(TestCase):
     queryset = RearPort.objects.all()
     queryset = RearPort.objects.all()
-    filterset = RearPortFilter
+    filterset = RearPortFilterSet
 
 
     @classmethod
     @classmethod
     def setUpTestData(cls):
     def setUpTestData(cls):
@@ -1828,12 +1828,12 @@ class RearPortTestCase(TestCase):
         Device.objects.bulk_create(devices)
         Device.objects.bulk_create(devices)
 
 
         rear_ports = (
         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)
         RearPort.objects.bulk_create(rear_ports)
 
 
@@ -1853,7 +1853,7 @@ class RearPortTestCase(TestCase):
 
 
     def test_type(self):
     def test_type(self):
         # TODO: Test for multiple values
         # 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)
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
 
 
     def test_positions(self):
     def test_positions(self):
@@ -1880,7 +1880,7 @@ class RearPortTestCase(TestCase):
 
 
 class DeviceBayTestCase(TestCase):
 class DeviceBayTestCase(TestCase):
     queryset = DeviceBay.objects.all()
     queryset = DeviceBay.objects.all()
-    filterset = DeviceBayFilter
+    filterset = DeviceBayFilterSet
 
 
     @classmethod
     @classmethod
     def setUpTestData(cls):
     def setUpTestData(cls):
@@ -1927,7 +1927,7 @@ class DeviceBayTestCase(TestCase):
 
 
 class InventoryItemTestCase(TestCase):
 class InventoryItemTestCase(TestCase):
     queryset = InventoryItem.objects.all()
     queryset = InventoryItem.objects.all()
-    filterset = InventoryItemFilter
+    filterset = InventoryItemFilterSet
 
 
     @classmethod
     @classmethod
     def setUpTestData(cls):
     def setUpTestData(cls):
@@ -2045,7 +2045,7 @@ class InventoryItemTestCase(TestCase):
 
 
 class VirtualChassisTestCase(TestCase):
 class VirtualChassisTestCase(TestCase):
     queryset = VirtualChassis.objects.all()
     queryset = VirtualChassis.objects.all()
-    filterset = VirtualChassisFilter
+    filterset = VirtualChassisFilterSet
 
 
     @classmethod
     @classmethod
     def setUpTestData(cls):
     def setUpTestData(cls):
@@ -2116,7 +2116,7 @@ class VirtualChassisTestCase(TestCase):
 
 
 class CableTestCase(TestCase):
 class CableTestCase(TestCase):
     queryset = Cable.objects.all()
     queryset = Cable.objects.all()
-    filterset = CableFilter
+    filterset = CableFilterSet
 
 
     @classmethod
     @classmethod
     def setUpTestData(cls):
     def setUpTestData(cls):
@@ -2156,28 +2156,28 @@ class CableTestCase(TestCase):
         Device.objects.bulk_create(devices)
         Device.objects.bulk_create(devices)
 
 
         interfaces = (
         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)
         Interface.objects.bulk_create(interfaces)
 
 
         # Cables
         # 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):
     def test_id(self):
         id_list = self.queryset.values_list('id', flat=True)[:2]
         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)
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
 
 
     def test_length_unit(self):
     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)
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
 
 
     def test_type(self):
     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)
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
 
 
     def test_status(self):
     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)
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
 
 
     def test_color(self):
     def test_color(self):
@@ -2239,7 +2241,7 @@ class CableTestCase(TestCase):
 
 
 class PowerPanelTestCase(TestCase):
 class PowerPanelTestCase(TestCase):
     queryset = PowerPanel.objects.all()
     queryset = PowerPanel.objects.all()
-    filterset = PowerPanelFilter
+    filterset = PowerPanelFilterSet
 
 
     @classmethod
     @classmethod
     def setUpTestData(cls):
     def setUpTestData(cls):
@@ -2299,7 +2301,7 @@ class PowerPanelTestCase(TestCase):
 
 
 class PowerFeedTestCase(TestCase):
 class PowerFeedTestCase(TestCase):
     queryset = PowerFeed.objects.all()
     queryset = PowerFeed.objects.all()
-    filterset = PowerFeedFilter
+    filterset = PowerFeedFilterSet
 
 
     @classmethod
     @classmethod
     def setUpTestData(cls):
     def setUpTestData(cls):
@@ -2334,9 +2336,9 @@ class PowerFeedTestCase(TestCase):
         PowerPanel.objects.bulk_create(power_panels)
         PowerPanel.objects.bulk_create(power_panels)
 
 
         power_feeds = (
         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)
         PowerFeed.objects.bulk_create(power_feeds)
 
 
@@ -2346,19 +2348,19 @@ class PowerFeedTestCase(TestCase):
 
 
     def test_status(self):
     def test_status(self):
         # TODO: Test for multiple values
         # 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)
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
 
 
     def test_type(self):
     def test_type(self):
-        params = {'type': POWERFEED_TYPE_PRIMARY}
+        params = {'type': PowerFeedTypeChoices.TYPE_PRIMARY}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
 
 
     def test_supply(self):
     def test_supply(self):
-        params = {'supply': POWERFEED_SUPPLY_AC}
+        params = {'supply': PowerFeedSupplyChoices.SUPPLY_AC}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
 
 
     def test_phase(self):
     def test_phase(self):
-        params = {'phase': POWERFEED_PHASE_3PHASE}
+        params = {'phase': PowerFeedPhaseChoices.PHASE_3PHASE}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
 
 
     def test_voltage(self):
     def test_voltage(self):

+ 8 - 8
netbox/dcim/tests/test_forms.py

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

+ 65 - 21
netbox/dcim/tests/test_models.py

@@ -1,6 +1,10 @@
+from django.core.exceptions import ValidationError
 from django.test import TestCase
 from django.test import TestCase
 
 
+from dcim.choices import *
+from dcim.constants import CONNECTION_STATUS_CONNECTED, CONNECTION_STATUS_PLANNED
 from dcim.models import *
 from dcim.models import *
+from tenancy.models import Tenant
 
 
 
 
 class RackTestCase(TestCase):
 class RackTestCase(TestCase):
@@ -87,7 +91,7 @@ class RackTestCase(TestCase):
             site=self.site1,
             site=self.site1,
             rack=rack1,
             rack=rack1,
             position=43,
             position=43,
-            face=RACK_FACE_FRONT,
+            face=DeviceFaceChoices.FACE_FRONT,
         )
         )
         device1.save()
         device1.save()
 
 
@@ -117,7 +121,7 @@ class RackTestCase(TestCase):
             site=self.site1,
             site=self.site1,
             rack=self.rack,
             rack=self.rack,
             position=10,
             position=10,
-            face=RACK_FACE_REAR,
+            face=DeviceFaceChoices.FACE_REAR,
         )
         )
         device1.save()
         device1.save()
 
 
@@ -125,14 +129,14 @@ class RackTestCase(TestCase):
         self.assertEqual(list(self.rack.units), list(reversed(range(1, 43))))
         self.assertEqual(list(self.rack.units), list(reversed(range(1, 43))))
 
 
         # Validate inventory (front face)
         # Validate inventory (front face)
-        rack1_inventory_front = self.rack.get_front_elevation()
+        rack1_inventory_front = self.rack.get_rack_units(face=DeviceFaceChoices.FACE_FRONT)
         self.assertEqual(rack1_inventory_front[-10]['device'], device1)
         self.assertEqual(rack1_inventory_front[-10]['device'], device1)
         del(rack1_inventory_front[-10])
         del(rack1_inventory_front[-10])
         for u in rack1_inventory_front:
         for u in rack1_inventory_front:
             self.assertIsNone(u['device'])
             self.assertIsNone(u['device'])
 
 
         # Validate inventory (rear face)
         # Validate inventory (rear face)
-        rack1_inventory_rear = self.rack.get_rear_elevation()
+        rack1_inventory_rear = self.rack.get_rack_units(face=DeviceFaceChoices.FACE_REAR)
         self.assertEqual(rack1_inventory_rear[-10]['device'], device1)
         self.assertEqual(rack1_inventory_rear[-10]['device'], device1)
         del(rack1_inventory_rear[-10])
         del(rack1_inventory_rear[-10])
         for u in rack1_inventory_rear:
         for u in rack1_inventory_rear:
@@ -146,7 +150,7 @@ class RackTestCase(TestCase):
             site=self.site1,
             site=self.site1,
             rack=self.rack,
             rack=self.rack,
             position=None,
             position=None,
-            face=None,
+            face='',
         )
         )
         self.assertTrue(pdu)
         self.assertTrue(pdu)
 
 
@@ -187,20 +191,20 @@ class DeviceTestCase(TestCase):
             device_type=self.device_type,
             device_type=self.device_type,
             name='Power Outlet 1',
             name='Power Outlet 1',
             power_port=ppt,
             power_port=ppt,
-            feed_leg=POWERFEED_LEG_A
+            feed_leg=PowerOutletFeedLegChoices.FEED_LEG_A
         ).save()
         ).save()
 
 
         InterfaceTemplate(
         InterfaceTemplate(
             device_type=self.device_type,
             device_type=self.device_type,
             name='Interface 1',
             name='Interface 1',
-            type=IFACE_TYPE_1GE_FIXED,
+            type=InterfaceTypeChoices.TYPE_1GE_FIXED,
             mgmt_only=True
             mgmt_only=True
         ).save()
         ).save()
 
 
         rpt = RearPortTemplate(
         rpt = RearPortTemplate(
             device_type=self.device_type,
             device_type=self.device_type,
             name='Rear Port 1',
             name='Rear Port 1',
-            type=PORT_TYPE_8P8C,
+            type=PortTypeChoices.TYPE_8P8C,
             positions=8
             positions=8
         )
         )
         rpt.save()
         rpt.save()
@@ -208,7 +212,7 @@ class DeviceTestCase(TestCase):
         FrontPortTemplate(
         FrontPortTemplate(
             device_type=self.device_type,
             device_type=self.device_type,
             name='Front Port 1',
             name='Front Port 1',
-            type=PORT_TYPE_8P8C,
+            type=PortTypeChoices.TYPE_8P8C,
             rear_port=rpt,
             rear_port=rpt,
             rear_port_position=2
             rear_port_position=2
         ).save()
         ).save()
@@ -251,27 +255,27 @@ class DeviceTestCase(TestCase):
             device=d,
             device=d,
             name='Power Outlet 1',
             name='Power Outlet 1',
             power_port=pp,
             power_port=pp,
-            feed_leg=POWERFEED_LEG_A
+            feed_leg=PowerOutletFeedLegChoices.FEED_LEG_A
         )
         )
 
 
         Interface.objects.get(
         Interface.objects.get(
             device=d,
             device=d,
             name='Interface 1',
             name='Interface 1',
-            type=IFACE_TYPE_1GE_FIXED,
+            type=InterfaceTypeChoices.TYPE_1GE_FIXED,
             mgmt_only=True
             mgmt_only=True
         )
         )
 
 
         rp = RearPort.objects.get(
         rp = RearPort.objects.get(
             device=d,
             device=d,
             name='Rear Port 1',
             name='Rear Port 1',
-            type=PORT_TYPE_8P8C,
+            type=PortTypeChoices.TYPE_8P8C,
             positions=8
             positions=8
         )
         )
 
 
         FrontPort.objects.get(
         FrontPort.objects.get(
             device=d,
             device=d,
             name='Front Port 1',
             name='Front Port 1',
-            type=PORT_TYPE_8P8C,
+            type=PortTypeChoices.TYPE_8P8C,
             rear_port=rp,
             rear_port=rp,
             rear_port_position=2
             rear_port_position=2
         )
         )
@@ -281,6 +285,42 @@ class DeviceTestCase(TestCase):
             name='Device Bay 1'
             name='Device Bay 1'
         )
         )
 
 
+    def test_device_duplicate_name_per_site(self):
+
+        device1 = Device(
+            site=self.site,
+            device_type=self.device_type,
+            device_role=self.device_role,
+            name='Test Device 1'
+        )
+        device1.save()
+
+        device2 = Device(
+            site=device1.site,
+            device_type=device1.device_type,
+            device_role=device1.device_role,
+            name=device1.name
+        )
+
+        # Two devices assigned to the same Site and no Tenant should fail validation
+        with self.assertRaises(ValidationError):
+            device2.full_clean()
+
+        tenant = Tenant.objects.create(name='Test Tenant 1', slug='test-tenant-1')
+        device1.tenant = tenant
+        device1.save()
+        device2.tenant = tenant
+
+        # Two devices assigned to the same Site and the same Tenant should fail validation
+        with self.assertRaises(ValidationError):
+            device2.full_clean()
+
+        device2.tenant = None
+
+        # Two devices assigned to the same Site and different Tenants should pass validation
+        device2.full_clean()
+        device2.save()
+
 
 
 class CableTestCase(TestCase):
 class CableTestCase(TestCase):
 
 
@@ -382,7 +422,7 @@ class CableTestCase(TestCase):
         """
         """
         A cable cannot terminate to a virtual interface
         A cable cannot terminate to a virtual interface
         """
         """
-        virtual_interface = Interface(device=self.device1, name="V1", type=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)
         cable = Cable(termination_a=self.interface2, termination_b=virtual_interface)
         with self.assertRaises(ValidationError):
         with self.assertRaises(ValidationError):
             cable.clean()
             cable.clean()
@@ -391,7 +431,7 @@ class CableTestCase(TestCase):
         """
         """
         A cable cannot terminate to a wireless interface
         A cable cannot terminate to a wireless interface
         """
         """
-        wireless_interface = Interface(device=self.device1, name="W1", type=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)
         cable = Cable(termination_a=self.interface2, termination_b=wireless_interface)
         with self.assertRaises(ValidationError):
         with self.assertRaises(ValidationError):
             cable.clean()
             cable.clean()
@@ -424,16 +464,16 @@ class CablePathTestCase(TestCase):
             device_type=devicetype, device_role=devicerole, name='Test Panel 2', site=site
             device_type=devicetype, device_role=devicerole, name='Test Panel 2', site=site
         )
         )
         self.rear_port1 = RearPort.objects.create(
         self.rear_port1 = RearPort.objects.create(
-            device=self.panel1, name='Rear Port 1', type=PORT_TYPE_8P8C
+            device=self.panel1, name='Rear Port 1', type=PortTypeChoices.TYPE_8P8C
         )
         )
         self.front_port1 = FrontPort.objects.create(
         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(
         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(
         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):
     def test_path_completion(self):
@@ -453,14 +493,18 @@ class CablePathTestCase(TestCase):
         self.assertIsNone(interface1.connection_status)
         self.assertIsNone(interface1.connection_status)
 
 
         # Third segment
         # 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()
         cable3.save()
         interface1 = Interface.objects.get(pk=self.interface1.pk)
         interface1 = Interface.objects.get(pk=self.interface1.pk)
         self.assertEqual(interface1.connected_endpoint, self.interface2)
         self.assertEqual(interface1.connected_endpoint, self.interface2)
         self.assertEqual(interface1.connection_status, CONNECTION_STATUS_PLANNED)
         self.assertEqual(interface1.connection_status, CONNECTION_STATUS_PLANNED)
 
 
         # Switch third segment from planned to connected
         # Switch third segment from planned to connected
-        cable3.status = CONNECTION_STATUS_CONNECTED
+        cable3.status = CableStatusChoices.STATUS_CONNECTED
         cable3.save()
         cable3.save()
         interface1 = Interface.objects.get(pk=self.interface1.pk)
         interface1 = Interface.objects.get(pk=self.interface1.pk)
         self.assertEqual(interface1.connected_endpoint, self.interface2)
         self.assertEqual(interface1.connected_endpoint, self.interface2)

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 841 - 26
netbox/dcim/tests/test_views.py


+ 17 - 1
netbox/dcim/urls.py

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

+ 222 - 73
netbox/dcim/views.py

@@ -1,3 +1,4 @@
+from collections import OrderedDict
 import re
 import re
 
 
 from django.conf import settings
 from django.conf import settings
@@ -16,8 +17,7 @@ from django.utils.safestring import mark_safe
 from django.views.generic import View
 from django.views.generic import View
 
 
 from circuits.models import Circuit
 from circuits.models import Circuit
-from extras.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 extras.views import ObjectConfigContextView
 from ipam.models import Prefix, VLAN
 from ipam.models import Prefix, VLAN
 from ipam.tables import InterfaceIPAddressTable, InterfaceVLANTable
 from ipam.tables import InterfaceIPAddressTable, InterfaceVLANTable
@@ -26,7 +26,7 @@ from utilities.paginator import EnhancedPaginator
 from utilities.utils import csv_format
 from utilities.utils import csv_format
 from utilities.views import (
 from utilities.views import (
     BulkComponentCreateView, BulkDeleteView, BulkEditView, BulkImportView, ComponentCreateView, GetReturnURLMixin,
     BulkComponentCreateView, BulkDeleteView, BulkEditView, BulkImportView, ComponentCreateView, GetReturnURLMixin,
-    ObjectDeleteView, ObjectEditView, ObjectListView,
+    ObjectImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
 )
 )
 from virtualization.models import VirtualMachine
 from virtualization.models import VirtualMachine
 from . import filters, forms, tables
 from . import filters, forms, tables
@@ -148,8 +148,8 @@ class RegionListView(PermissionRequiredMixin, ObjectListView):
         'site_count',
         'site_count',
         cumulative=True
         cumulative=True
     )
     )
-    filter = filters.RegionFilter
-    filter_form = forms.RegionFilterForm
+    filterset = filters.RegionFilterSet
+    filterset_form = forms.RegionFilterForm
     table = tables.RegionTable
     table = tables.RegionTable
     template_name = 'dcim/region_list.html'
     template_name = 'dcim/region_list.html'
 
 
@@ -175,7 +175,7 @@ class RegionBulkImportView(PermissionRequiredMixin, BulkImportView):
 class RegionBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 class RegionBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
     permission_required = 'dcim.delete_region'
     permission_required = 'dcim.delete_region'
     queryset = Region.objects.all()
     queryset = Region.objects.all()
-    filter = filters.RegionFilter
+    filterset = filters.RegionFilterSet
     table = tables.RegionTable
     table = tables.RegionTable
     default_return_url = 'dcim:region_list'
     default_return_url = 'dcim:region_list'
 
 
@@ -187,8 +187,8 @@ class RegionBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 class SiteListView(PermissionRequiredMixin, ObjectListView):
 class SiteListView(PermissionRequiredMixin, ObjectListView):
     permission_required = 'dcim.view_site'
     permission_required = 'dcim.view_site'
     queryset = Site.objects.prefetch_related('region', 'tenant')
     queryset = Site.objects.prefetch_related('region', 'tenant')
-    filter = filters.SiteFilter
-    filter_form = forms.SiteFilterForm
+    filterset = filters.SiteFilterSet
+    filterset_form = forms.SiteFilterForm
     table = tables.SiteTable
     table = tables.SiteTable
     template_name = 'dcim/site_list.html'
     template_name = 'dcim/site_list.html'
 
 
@@ -208,14 +208,12 @@ class SiteView(PermissionRequiredMixin, View):
             'vm_count': VirtualMachine.objects.filter(cluster__site=site).count(),
             'vm_count': VirtualMachine.objects.filter(cluster__site=site).count(),
         }
         }
         rack_groups = RackGroup.objects.filter(site=site).annotate(rack_count=Count('racks'))
         rack_groups = RackGroup.objects.filter(site=site).annotate(rack_count=Count('racks'))
-        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', {
         return render(request, 'dcim/site.html', {
             'site': site,
             'site': site,
             'stats': stats,
             'stats': stats,
             'rack_groups': rack_groups,
             'rack_groups': rack_groups,
-            'topology_maps': topology_maps,
             'show_graphs': show_graphs,
             'show_graphs': show_graphs,
         })
         })
 
 
@@ -248,7 +246,7 @@ class SiteBulkImportView(PermissionRequiredMixin, BulkImportView):
 class SiteBulkEditView(PermissionRequiredMixin, BulkEditView):
 class SiteBulkEditView(PermissionRequiredMixin, BulkEditView):
     permission_required = 'dcim.change_site'
     permission_required = 'dcim.change_site'
     queryset = Site.objects.prefetch_related('region', 'tenant')
     queryset = Site.objects.prefetch_related('region', 'tenant')
-    filter = filters.SiteFilter
+    filterset = filters.SiteFilterSet
     table = tables.SiteTable
     table = tables.SiteTable
     form = forms.SiteBulkEditForm
     form = forms.SiteBulkEditForm
     default_return_url = 'dcim:site_list'
     default_return_url = 'dcim:site_list'
@@ -257,7 +255,7 @@ class SiteBulkEditView(PermissionRequiredMixin, BulkEditView):
 class SiteBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 class SiteBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
     permission_required = 'dcim.delete_site'
     permission_required = 'dcim.delete_site'
     queryset = Site.objects.prefetch_related('region', 'tenant')
     queryset = Site.objects.prefetch_related('region', 'tenant')
-    filter = filters.SiteFilter
+    filterset = filters.SiteFilterSet
     table = tables.SiteTable
     table = tables.SiteTable
     default_return_url = 'dcim:site_list'
     default_return_url = 'dcim:site_list'
 
 
@@ -269,8 +267,8 @@ class SiteBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 class RackGroupListView(PermissionRequiredMixin, ObjectListView):
 class RackGroupListView(PermissionRequiredMixin, ObjectListView):
     permission_required = 'dcim.view_rackgroup'
     permission_required = 'dcim.view_rackgroup'
     queryset = RackGroup.objects.prefetch_related('site').annotate(rack_count=Count('racks'))
     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
     table = tables.RackGroupTable
     template_name = 'dcim/rackgroup_list.html'
     template_name = 'dcim/rackgroup_list.html'
 
 
@@ -296,7 +294,7 @@ class RackGroupBulkImportView(PermissionRequiredMixin, BulkImportView):
 class RackGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 class RackGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
     permission_required = 'dcim.delete_rackgroup'
     permission_required = 'dcim.delete_rackgroup'
     queryset = RackGroup.objects.prefetch_related('site').annotate(rack_count=Count('racks'))
     queryset = RackGroup.objects.prefetch_related('site').annotate(rack_count=Count('racks'))
-    filter = filters.RackGroupFilter
+    filterset = filters.RackGroupFilterSet
     table = tables.RackGroupTable
     table = tables.RackGroupTable
     default_return_url = 'dcim:rackgroup_list'
     default_return_url = 'dcim:rackgroup_list'
 
 
@@ -348,8 +346,8 @@ class RackListView(PermissionRequiredMixin, ObjectListView):
     ).annotate(
     ).annotate(
         device_count=Count('devices')
         device_count=Count('devices')
     )
     )
-    filter = filters.RackFilter
-    filter_form = forms.RackFilterForm
+    filterset = filters.RackFilterSet
+    filterset_form = forms.RackFilterForm
     table = tables.RackDetailTable
     table = tables.RackDetailTable
     template_name = 'dcim/rack_list.html'
     template_name = 'dcim/rack_list.html'
 
 
@@ -363,7 +361,7 @@ class RackElevationListView(PermissionRequiredMixin, View):
     def get(self, request):
     def get(self, request):
 
 
         racks = Rack.objects.prefetch_related('site', 'group', 'tenant', 'role', 'devices__device_type')
         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()
         total_count = racks.count()
 
 
         # Pagination
         # Pagination
@@ -421,8 +419,6 @@ class RackView(PermissionRequiredMixin, View):
             'nonracked_devices': nonracked_devices,
             'nonracked_devices': nonracked_devices,
             'next_rack': next_rack,
             'next_rack': next_rack,
             'prev_rack': prev_rack,
             'prev_rack': prev_rack,
-            'front_elevation': rack.get_front_elevation(),
-            'rear_elevation': rack.get_rear_elevation(),
         })
         })
 
 
 
 
@@ -454,7 +450,7 @@ class RackBulkImportView(PermissionRequiredMixin, BulkImportView):
 class RackBulkEditView(PermissionRequiredMixin, BulkEditView):
 class RackBulkEditView(PermissionRequiredMixin, BulkEditView):
     permission_required = 'dcim.change_rack'
     permission_required = 'dcim.change_rack'
     queryset = Rack.objects.prefetch_related('site', 'group', 'tenant', 'role')
     queryset = Rack.objects.prefetch_related('site', 'group', 'tenant', 'role')
-    filter = filters.RackFilter
+    filterset = filters.RackFilterSet
     table = tables.RackTable
     table = tables.RackTable
     form = forms.RackBulkEditForm
     form = forms.RackBulkEditForm
     default_return_url = 'dcim:rack_list'
     default_return_url = 'dcim:rack_list'
@@ -463,7 +459,7 @@ class RackBulkEditView(PermissionRequiredMixin, BulkEditView):
 class RackBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 class RackBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
     permission_required = 'dcim.delete_rack'
     permission_required = 'dcim.delete_rack'
     queryset = Rack.objects.prefetch_related('site', 'group', 'tenant', 'role')
     queryset = Rack.objects.prefetch_related('site', 'group', 'tenant', 'role')
-    filter = filters.RackFilter
+    filterset = filters.RackFilterSet
     table = tables.RackTable
     table = tables.RackTable
     default_return_url = 'dcim:rack_list'
     default_return_url = 'dcim:rack_list'
 
 
@@ -475,8 +471,8 @@ class RackBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 class RackReservationListView(PermissionRequiredMixin, ObjectListView):
 class RackReservationListView(PermissionRequiredMixin, ObjectListView):
     permission_required = 'dcim.view_rackreservation'
     permission_required = 'dcim.view_rackreservation'
     queryset = RackReservation.objects.prefetch_related('rack__site')
     queryset = RackReservation.objects.prefetch_related('rack__site')
-    filter = filters.RackReservationFilter
-    filter_form = forms.RackReservationFilterForm
+    filterset = filters.RackReservationFilterSet
+    filterset_form = forms.RackReservationFilterForm
     table = tables.RackReservationTable
     table = tables.RackReservationTable
     template_name = 'dcim/rackreservation_list.html'
     template_name = 'dcim/rackreservation_list.html'
 
 
@@ -511,7 +507,7 @@ class RackReservationDeleteView(PermissionRequiredMixin, ObjectDeleteView):
 class RackReservationBulkEditView(PermissionRequiredMixin, BulkEditView):
 class RackReservationBulkEditView(PermissionRequiredMixin, BulkEditView):
     permission_required = 'dcim.change_rackreservation'
     permission_required = 'dcim.change_rackreservation'
     queryset = RackReservation.objects.prefetch_related('rack', 'user')
     queryset = RackReservation.objects.prefetch_related('rack', 'user')
-    filter = filters.RackReservationFilter
+    filterset = filters.RackReservationFilterSet
     table = tables.RackReservationTable
     table = tables.RackReservationTable
     form = forms.RackReservationBulkEditForm
     form = forms.RackReservationBulkEditForm
     default_return_url = 'dcim:rackreservation_list'
     default_return_url = 'dcim:rackreservation_list'
@@ -520,7 +516,7 @@ class RackReservationBulkEditView(PermissionRequiredMixin, BulkEditView):
 class RackReservationBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 class RackReservationBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
     permission_required = 'dcim.delete_rackreservation'
     permission_required = 'dcim.delete_rackreservation'
     queryset = RackReservation.objects.prefetch_related('rack', 'user')
     queryset = RackReservation.objects.prefetch_related('rack', 'user')
-    filter = filters.RackReservationFilter
+    filterset = filters.RackReservationFilterSet
     table = tables.RackReservationTable
     table = tables.RackReservationTable
     default_return_url = 'dcim:rackreservation_list'
     default_return_url = 'dcim:rackreservation_list'
 
 
@@ -572,8 +568,8 @@ class ManufacturerBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 class DeviceTypeListView(PermissionRequiredMixin, ObjectListView):
 class DeviceTypeListView(PermissionRequiredMixin, ObjectListView):
     permission_required = 'dcim.view_devicetype'
     permission_required = 'dcim.view_devicetype'
     queryset = DeviceType.objects.prefetch_related('manufacturer').annotate(instance_count=Count('instances'))
     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
     table = tables.DeviceTypeTable
     template_name = 'dcim/devicetype_list.html'
     template_name = 'dcim/devicetype_list.html'
 
 
@@ -659,17 +655,37 @@ class DeviceTypeDeleteView(PermissionRequiredMixin, ObjectDeleteView):
     default_return_url = 'dcim:devicetype_list'
     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):
 class DeviceTypeBulkEditView(PermissionRequiredMixin, BulkEditView):
     permission_required = 'dcim.change_devicetype'
     permission_required = 'dcim.change_devicetype'
     queryset = DeviceType.objects.prefetch_related('manufacturer').annotate(instance_count=Count('instances'))
     queryset = DeviceType.objects.prefetch_related('manufacturer').annotate(instance_count=Count('instances'))
-    filter = filters.DeviceTypeFilter
+    filterset = filters.DeviceTypeFilterSet
     table = tables.DeviceTypeTable
     table = tables.DeviceTypeTable
     form = forms.DeviceTypeBulkEditForm
     form = forms.DeviceTypeBulkEditForm
     default_return_url = 'dcim:devicetype_list'
     default_return_url = 'dcim:devicetype_list'
@@ -678,7 +694,7 @@ class DeviceTypeBulkEditView(PermissionRequiredMixin, BulkEditView):
 class DeviceTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 class DeviceTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
     permission_required = 'dcim.delete_devicetype'
     permission_required = 'dcim.delete_devicetype'
     queryset = DeviceType.objects.prefetch_related('manufacturer').annotate(instance_count=Count('instances'))
     queryset = DeviceType.objects.prefetch_related('manufacturer').annotate(instance_count=Count('instances'))
-    filter = filters.DeviceTypeFilter
+    filterset = filters.DeviceTypeFilterSet
     table = tables.DeviceTypeTable
     table = tables.DeviceTypeTable
     default_return_url = 'dcim:devicetype_list'
     default_return_url = 'dcim:devicetype_list'
 
 
@@ -960,8 +976,8 @@ class DeviceListView(PermissionRequiredMixin, ObjectListView):
     queryset = Device.objects.prefetch_related(
     queryset = Device.objects.prefetch_related(
         'device_type__manufacturer', 'device_role', 'tenant', 'site', 'rack', 'primary_ip4', 'primary_ip6'
         '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
     table = tables.DeviceDetailTable
     template_name = 'dcim/device_list.html'
     template_name = 'dcim/device_list.html'
 
 
@@ -1039,8 +1055,8 @@ class DeviceView(PermissionRequiredMixin, View):
             'secrets': secrets,
             'secrets': secrets,
             'vc_members': vc_members,
             'vc_members': vc_members,
             'related_devices': related_devices,
             'related_devices': related_devices,
-            'show_graphs': Graph.objects.filter(type=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):
 class DeviceBulkEditView(PermissionRequiredMixin, BulkEditView):
     permission_required = 'dcim.change_device'
     permission_required = 'dcim.change_device'
     queryset = Device.objects.prefetch_related('tenant', 'site', 'rack', 'device_role', 'device_type__manufacturer')
     queryset = Device.objects.prefetch_related('tenant', 'site', 'rack', 'device_role', 'device_type__manufacturer')
-    filter = filters.DeviceFilter
+    filterset = filters.DeviceFilterSet
     table = tables.DeviceTable
     table = tables.DeviceTable
     form = forms.DeviceBulkEditForm
     form = forms.DeviceBulkEditForm
     default_return_url = 'dcim:device_list'
     default_return_url = 'dcim:device_list'
@@ -1169,7 +1185,7 @@ class DeviceBulkEditView(PermissionRequiredMixin, BulkEditView):
 class DeviceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 class DeviceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
     permission_required = 'dcim.delete_device'
     permission_required = 'dcim.delete_device'
     queryset = Device.objects.prefetch_related('tenant', 'site', 'rack', 'device_role', 'device_type__manufacturer')
     queryset = Device.objects.prefetch_related('tenant', 'site', 'rack', 'device_role', 'device_type__manufacturer')
-    filter = filters.DeviceFilter
+    filterset = filters.DeviceFilterSet
     table = tables.DeviceTable
     table = tables.DeviceTable
     default_return_url = 'dcim:device_list'
     default_return_url = 'dcim:device_list'
 
 
@@ -1178,6 +1194,15 @@ class DeviceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 # Console ports
 # Console ports
 #
 #
 
 
+class ConsolePortListView(PermissionRequiredMixin, ObjectListView):
+    permission_required = 'dcim.view_consoleport'
+    queryset = ConsolePort.objects.prefetch_related('device', 'device__tenant', 'device__site', 'cable')
+    filterset = filters.ConsolePortFilterSet
+    filterset_form = forms.ConsolePortFilterForm
+    table = tables.ConsolePortDetailTable
+    template_name = 'dcim/device_component_list.html'
+
+
 class ConsolePortCreateView(PermissionRequiredMixin, ComponentCreateView):
 class ConsolePortCreateView(PermissionRequiredMixin, ComponentCreateView):
     permission_required = 'dcim.add_consoleport'
     permission_required = 'dcim.add_consoleport'
     parent_model = Device
     parent_model = Device
@@ -1199,6 +1224,13 @@ class ConsolePortDeleteView(PermissionRequiredMixin, ObjectDeleteView):
     model = ConsolePort
     model = ConsolePort
 
 
 
 
+class ConsolePortBulkImportView(PermissionRequiredMixin, BulkImportView):
+    permission_required = 'dcim.add_consoleport'
+    model_form = forms.ConsolePortCSVForm
+    table = tables.ConsolePortImportTable
+    default_return_url = 'dcim:consoleport_list'
+
+
 class ConsolePortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 class ConsolePortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
     permission_required = 'dcim.delete_consoleport'
     permission_required = 'dcim.delete_consoleport'
     queryset = ConsolePort.objects.all()
     queryset = ConsolePort.objects.all()
@@ -1210,6 +1242,15 @@ class ConsolePortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 # Console server ports
 # Console server ports
 #
 #
 
 
+class ConsoleServerPortListView(PermissionRequiredMixin, ObjectListView):
+    permission_required = 'dcim.view_consoleserverport'
+    queryset = ConsoleServerPort.objects.prefetch_related('device', 'device__tenant', 'device__site', 'cable')
+    filterset = filters.ConsoleServerPortFilterSet
+    filterset_form = forms.ConsoleServerPortFilterForm
+    table = tables.ConsoleServerPortDetailTable
+    template_name = 'dcim/device_component_list.html'
+
+
 class ConsoleServerPortCreateView(PermissionRequiredMixin, ComponentCreateView):
 class ConsoleServerPortCreateView(PermissionRequiredMixin, ComponentCreateView):
     permission_required = 'dcim.add_consoleserverport'
     permission_required = 'dcim.add_consoleserverport'
     parent_model = Device
     parent_model = Device
@@ -1231,6 +1272,13 @@ class ConsoleServerPortDeleteView(PermissionRequiredMixin, ObjectDeleteView):
     model = ConsoleServerPort
     model = ConsoleServerPort
 
 
 
 
+class ConsoleServerPortBulkImportView(PermissionRequiredMixin, BulkImportView):
+    permission_required = 'dcim.add_consoleserverport'
+    model_form = forms.ConsoleServerPortCSVForm
+    table = tables.ConsoleServerPortImportTable
+    default_return_url = 'dcim:consoleserverport_list'
+
+
 class ConsoleServerPortBulkEditView(PermissionRequiredMixin, BulkEditView):
 class ConsoleServerPortBulkEditView(PermissionRequiredMixin, BulkEditView):
     permission_required = 'dcim.change_consoleserverport'
     permission_required = 'dcim.change_consoleserverport'
     queryset = ConsoleServerPort.objects.all()
     queryset = ConsoleServerPort.objects.all()
@@ -1262,6 +1310,15 @@ class ConsoleServerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 # Power ports
 # Power ports
 #
 #
 
 
+class PowerPortListView(PermissionRequiredMixin, ObjectListView):
+    permission_required = 'dcim.view_powerport'
+    queryset = PowerPort.objects.prefetch_related('device', 'device__tenant', 'device__site', 'cable')
+    filterset = filters.PowerPortFilterSet
+    filterset_form = forms.PowerPortFilterForm
+    table = tables.PowerPortDetailTable
+    template_name = 'dcim/device_component_list.html'
+
+
 class PowerPortCreateView(PermissionRequiredMixin, ComponentCreateView):
 class PowerPortCreateView(PermissionRequiredMixin, ComponentCreateView):
     permission_required = 'dcim.add_powerport'
     permission_required = 'dcim.add_powerport'
     parent_model = Device
     parent_model = Device
@@ -1283,6 +1340,13 @@ class PowerPortDeleteView(PermissionRequiredMixin, ObjectDeleteView):
     model = PowerPort
     model = PowerPort
 
 
 
 
+class PowerPortBulkImportView(PermissionRequiredMixin, BulkImportView):
+    permission_required = 'dcim.add_powerport'
+    model_form = forms.PowerPortCSVForm
+    table = tables.PowerPortImportTable
+    default_return_url = 'dcim:powerport_list'
+
+
 class PowerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 class PowerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
     permission_required = 'dcim.delete_powerport'
     permission_required = 'dcim.delete_powerport'
     queryset = PowerPort.objects.all()
     queryset = PowerPort.objects.all()
@@ -1294,6 +1358,15 @@ class PowerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 # Power outlets
 # Power outlets
 #
 #
 
 
+class PowerOutletListView(PermissionRequiredMixin, ObjectListView):
+    permission_required = 'dcim.view_poweroutlet'
+    queryset = PowerOutlet.objects.prefetch_related('device', 'device__tenant', 'device__site', 'cable')
+    filterset = filters.PowerOutletFilterSet
+    filterset_form = forms.PowerOutletFilterForm
+    table = tables.PowerOutletDetailTable
+    template_name = 'dcim/device_component_list.html'
+
+
 class PowerOutletCreateView(PermissionRequiredMixin, ComponentCreateView):
 class PowerOutletCreateView(PermissionRequiredMixin, ComponentCreateView):
     permission_required = 'dcim.add_poweroutlet'
     permission_required = 'dcim.add_poweroutlet'
     parent_model = Device
     parent_model = Device
@@ -1315,6 +1388,13 @@ class PowerOutletDeleteView(PermissionRequiredMixin, ObjectDeleteView):
     model = PowerOutlet
     model = PowerOutlet
 
 
 
 
+class PowerOutletBulkImportView(PermissionRequiredMixin, BulkImportView):
+    permission_required = 'dcim.add_poweroutlet'
+    model_form = forms.PowerOutletCSVForm
+    table = tables.PowerOutletImportTable
+    default_return_url = 'dcim:poweroutlet_list'
+
+
 class PowerOutletBulkEditView(PermissionRequiredMixin, BulkEditView):
 class PowerOutletBulkEditView(PermissionRequiredMixin, BulkEditView):
     permission_required = 'dcim.change_poweroutlet'
     permission_required = 'dcim.change_poweroutlet'
     queryset = PowerOutlet.objects.all()
     queryset = PowerOutlet.objects.all()
@@ -1346,6 +1426,15 @@ class PowerOutletBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 # Interfaces
 # Interfaces
 #
 #
 
 
+class InterfaceListView(PermissionRequiredMixin, ObjectListView):
+    permission_required = 'dcim.view_interface'
+    queryset = Interface.objects.prefetch_related('device', 'device__tenant', 'device__site', 'cable')
+    filterset = filters.InterfaceFilterSet
+    filterset_form = forms.InterfaceFilterForm
+    table = tables.InterfaceDetailTable
+    template_name = 'dcim/device_component_list.html'
+
+
 class InterfaceView(PermissionRequiredMixin, View):
 class InterfaceView(PermissionRequiredMixin, View):
     permission_required = 'dcim.view_interface'
     permission_required = 'dcim.view_interface'
 
 
@@ -1404,6 +1493,13 @@ class InterfaceDeleteView(PermissionRequiredMixin, ObjectDeleteView):
     model = Interface
     model = Interface
 
 
 
 
+class InterfaceBulkImportView(PermissionRequiredMixin, BulkImportView):
+    permission_required = 'dcim.add_interface'
+    model_form = forms.InterfaceCSVForm
+    table = tables.InterfaceImportTable
+    default_return_url = 'dcim:interface_list'
+
+
 class InterfaceBulkEditView(PermissionRequiredMixin, BulkEditView):
 class InterfaceBulkEditView(PermissionRequiredMixin, BulkEditView):
     permission_required = 'dcim.change_interface'
     permission_required = 'dcim.change_interface'
     queryset = Interface.objects.all()
     queryset = Interface.objects.all()
@@ -1435,6 +1531,15 @@ class InterfaceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 # Front ports
 # Front ports
 #
 #
 
 
+class FrontPortListView(PermissionRequiredMixin, ObjectListView):
+    permission_required = 'dcim.view_frontport'
+    queryset = FrontPort.objects.prefetch_related('device', 'device__tenant', 'device__site', 'cable')
+    filterset = filters.FrontPortFilterSet
+    filterset_form = forms.FrontPortFilterForm
+    table = tables.FrontPortDetailTable
+    template_name = 'dcim/device_component_list.html'
+
+
 class FrontPortCreateView(PermissionRequiredMixin, ComponentCreateView):
 class FrontPortCreateView(PermissionRequiredMixin, ComponentCreateView):
     permission_required = 'dcim.add_frontport'
     permission_required = 'dcim.add_frontport'
     parent_model = Device
     parent_model = Device
@@ -1456,6 +1561,13 @@ class FrontPortDeleteView(PermissionRequiredMixin, ObjectDeleteView):
     model = FrontPort
     model = FrontPort
 
 
 
 
+class FrontPortBulkImportView(PermissionRequiredMixin, BulkImportView):
+    permission_required = 'dcim.add_frontport'
+    model_form = forms.FrontPortCSVForm
+    table = tables.FrontPortImportTable
+    default_return_url = 'dcim:frontport_list'
+
+
 class FrontPortBulkEditView(PermissionRequiredMixin, BulkEditView):
 class FrontPortBulkEditView(PermissionRequiredMixin, BulkEditView):
     permission_required = 'dcim.change_frontport'
     permission_required = 'dcim.change_frontport'
     queryset = FrontPort.objects.all()
     queryset = FrontPort.objects.all()
@@ -1487,6 +1599,15 @@ class FrontPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 # Rear ports
 # Rear ports
 #
 #
 
 
+class RearPortListView(PermissionRequiredMixin, ObjectListView):
+    permission_required = 'dcim.view_rearport'
+    queryset = RearPort.objects.prefetch_related('device', 'device__tenant', 'device__site', 'cable')
+    filterset = filters.RearPortFilterSet
+    filterset_form = forms.RearPortFilterForm
+    table = tables.RearPortDetailTable
+    template_name = 'dcim/device_component_list.html'
+
+
 class RearPortCreateView(PermissionRequiredMixin, ComponentCreateView):
 class RearPortCreateView(PermissionRequiredMixin, ComponentCreateView):
     permission_required = 'dcim.add_rearport'
     permission_required = 'dcim.add_rearport'
     parent_model = Device
     parent_model = Device
@@ -1508,6 +1629,13 @@ class RearPortDeleteView(PermissionRequiredMixin, ObjectDeleteView):
     model = RearPort
     model = RearPort
 
 
 
 
+class RearPortBulkImportView(PermissionRequiredMixin, BulkImportView):
+    permission_required = 'dcim.add_rearport'
+    model_form = forms.RearPortCSVForm
+    table = tables.RearPortImportTable
+    default_return_url = 'dcim:rearport_list'
+
+
 class RearPortBulkEditView(PermissionRequiredMixin, BulkEditView):
 class RearPortBulkEditView(PermissionRequiredMixin, BulkEditView):
     permission_required = 'dcim.change_rearport'
     permission_required = 'dcim.change_rearport'
     queryset = RearPort.objects.all()
     queryset = RearPort.objects.all()
@@ -1539,6 +1667,17 @@ class RearPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 # Device bays
 # Device bays
 #
 #
 
 
+class DeviceBayListView(PermissionRequiredMixin, ObjectListView):
+    permission_required = 'dcim.view_devicebay'
+    queryset = DeviceBay.objects.prefetch_related(
+        'device', 'device__site', 'installed_device', 'installed_device__site'
+    )
+    filterset = filters.DeviceBayFilterSet
+    filterset_form = forms.DeviceBayFilterForm
+    table = tables.DeviceBayDetailTable
+    template_name = 'dcim/device_component_list.html'
+
+
 class DeviceBayCreateView(PermissionRequiredMixin, ComponentCreateView):
 class DeviceBayCreateView(PermissionRequiredMixin, ComponentCreateView):
     permission_required = 'dcim.add_devicebay'
     permission_required = 'dcim.add_devicebay'
     parent_model = Device
     parent_model = Device
@@ -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):
 class DeviceBayBulkRenameView(PermissionRequiredMixin, BulkRenameView):
     permission_required = 'dcim.change_devicebay'
     permission_required = 'dcim.change_devicebay'
     queryset = DeviceBay.objects.all()
     queryset = DeviceBay.objects.all()
@@ -1653,7 +1799,7 @@ class DeviceBulkAddConsolePortView(PermissionRequiredMixin, BulkComponentCreateV
     form = forms.DeviceBulkAddComponentForm
     form = forms.DeviceBulkAddComponentForm
     model = ConsolePort
     model = ConsolePort
     model_form = forms.ConsolePortForm
     model_form = forms.ConsolePortForm
-    filter = filters.DeviceFilter
+    filterset = filters.DeviceFilterSet
     table = tables.DeviceTable
     table = tables.DeviceTable
     default_return_url = 'dcim:device_list'
     default_return_url = 'dcim:device_list'
 
 
@@ -1665,7 +1811,7 @@ class DeviceBulkAddConsoleServerPortView(PermissionRequiredMixin, BulkComponentC
     form = forms.DeviceBulkAddComponentForm
     form = forms.DeviceBulkAddComponentForm
     model = ConsoleServerPort
     model = ConsoleServerPort
     model_form = forms.ConsoleServerPortForm
     model_form = forms.ConsoleServerPortForm
-    filter = filters.DeviceFilter
+    filterset = filters.DeviceFilterSet
     table = tables.DeviceTable
     table = tables.DeviceTable
     default_return_url = 'dcim:device_list'
     default_return_url = 'dcim:device_list'
 
 
@@ -1677,7 +1823,7 @@ class DeviceBulkAddPowerPortView(PermissionRequiredMixin, BulkComponentCreateVie
     form = forms.DeviceBulkAddComponentForm
     form = forms.DeviceBulkAddComponentForm
     model = PowerPort
     model = PowerPort
     model_form = forms.PowerPortForm
     model_form = forms.PowerPortForm
-    filter = filters.DeviceFilter
+    filterset = filters.DeviceFilterSet
     table = tables.DeviceTable
     table = tables.DeviceTable
     default_return_url = 'dcim:device_list'
     default_return_url = 'dcim:device_list'
 
 
@@ -1689,7 +1835,7 @@ class DeviceBulkAddPowerOutletView(PermissionRequiredMixin, BulkComponentCreateV
     form = forms.DeviceBulkAddComponentForm
     form = forms.DeviceBulkAddComponentForm
     model = PowerOutlet
     model = PowerOutlet
     model_form = forms.PowerOutletForm
     model_form = forms.PowerOutletForm
-    filter = filters.DeviceFilter
+    filterset = filters.DeviceFilterSet
     table = tables.DeviceTable
     table = tables.DeviceTable
     default_return_url = 'dcim:device_list'
     default_return_url = 'dcim:device_list'
 
 
@@ -1701,7 +1847,7 @@ class DeviceBulkAddInterfaceView(PermissionRequiredMixin, BulkComponentCreateVie
     form = forms.DeviceBulkAddInterfaceForm
     form = forms.DeviceBulkAddInterfaceForm
     model = Interface
     model = Interface
     model_form = forms.InterfaceForm
     model_form = forms.InterfaceForm
-    filter = filters.DeviceFilter
+    filterset = filters.DeviceFilterSet
     table = tables.DeviceTable
     table = tables.DeviceTable
     default_return_url = 'dcim:device_list'
     default_return_url = 'dcim:device_list'
 
 
@@ -1713,7 +1859,7 @@ class DeviceBulkAddDeviceBayView(PermissionRequiredMixin, BulkComponentCreateVie
     form = forms.DeviceBulkAddComponentForm
     form = forms.DeviceBulkAddComponentForm
     model = DeviceBay
     model = DeviceBay
     model_form = forms.DeviceBayForm
     model_form = forms.DeviceBayForm
-    filter = filters.DeviceFilter
+    filterset = filters.DeviceFilterSet
     table = tables.DeviceTable
     table = tables.DeviceTable
     default_return_url = 'dcim:device_list'
     default_return_url = 'dcim:device_list'
 
 
@@ -1727,8 +1873,8 @@ class CableListView(PermissionRequiredMixin, ObjectListView):
     queryset = Cable.objects.prefetch_related(
     queryset = Cable.objects.prefetch_related(
         'termination_a', 'termination_b'
         'termination_a', 'termination_b'
     )
     )
-    filter = filters.CableFilter
-    filter_form = forms.CableFilterForm
+    filterset = filters.CableFilterSet
+    filterset_form = forms.CableFilterForm
     table = tables.CableTable
     table = tables.CableTable
     template_name = 'dcim/cable_list.html'
     template_name = 'dcim/cable_list.html'
 
 
@@ -1864,7 +2010,7 @@ class CableBulkImportView(PermissionRequiredMixin, BulkImportView):
 class CableBulkEditView(PermissionRequiredMixin, BulkEditView):
 class CableBulkEditView(PermissionRequiredMixin, BulkEditView):
     permission_required = 'dcim.change_cable'
     permission_required = 'dcim.change_cable'
     queryset = Cable.objects.prefetch_related('termination_a', 'termination_b')
     queryset = Cable.objects.prefetch_related('termination_a', 'termination_b')
-    filter = filters.CableFilter
+    filterset = filters.CableFilterSet
     table = tables.CableTable
     table = tables.CableTable
     form = forms.CableBulkEditForm
     form = forms.CableBulkEditForm
     default_return_url = 'dcim:cable_list'
     default_return_url = 'dcim:cable_list'
@@ -1873,7 +2019,7 @@ class CableBulkEditView(PermissionRequiredMixin, BulkEditView):
 class CableBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 class CableBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
     permission_required = 'dcim.delete_cable'
     permission_required = 'dcim.delete_cable'
     queryset = Cable.objects.prefetch_related('termination_a', 'termination_b')
     queryset = Cable.objects.prefetch_related('termination_a', 'termination_b')
-    filter = filters.CableFilter
+    filterset = filters.CableFilterSet
     table = tables.CableTable
     table = tables.CableTable
     default_return_url = 'dcim:cable_list'
     default_return_url = 'dcim:cable_list'
 
 
@@ -1891,8 +2037,8 @@ class ConsoleConnectionsListView(PermissionRequiredMixin, ObjectListView):
     ).order_by(
     ).order_by(
         'cable', 'connected_endpoint__device__name', 'connected_endpoint__name'
         '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
     table = tables.ConsoleConnectionTable
     template_name = 'dcim/console_connections_list.html'
     template_name = 'dcim/console_connections_list.html'
 
 
@@ -1910,7 +2056,8 @@ class ConsoleConnectionsListView(PermissionRequiredMixin, ObjectListView):
                 obj.get_connection_status_display(),
                 obj.get_connection_status_display(),
             ])
             ])
             csv_data.append(csv)
             csv_data.append(csv)
-        return csv_data
+
+        return '\n'.join(csv_data)
 
 
 
 
 class PowerConnectionsListView(PermissionRequiredMixin, ObjectListView):
 class PowerConnectionsListView(PermissionRequiredMixin, ObjectListView):
@@ -1922,8 +2069,8 @@ class PowerConnectionsListView(PermissionRequiredMixin, ObjectListView):
     ).order_by(
     ).order_by(
         'cable', '_connected_poweroutlet__device__name', '_connected_poweroutlet__name'
         '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
     table = tables.PowerConnectionTable
     template_name = 'dcim/power_connections_list.html'
     template_name = 'dcim/power_connections_list.html'
 
 
@@ -1941,7 +2088,8 @@ class PowerConnectionsListView(PermissionRequiredMixin, ObjectListView):
                 obj.get_connection_status_display(),
                 obj.get_connection_status_display(),
             ])
             ])
             csv_data.append(csv)
             csv_data.append(csv)
-        return csv_data
+
+        return '\n'.join(csv_data)
 
 
 
 
 class InterfaceConnectionsListView(PermissionRequiredMixin, ObjectListView):
 class InterfaceConnectionsListView(PermissionRequiredMixin, ObjectListView):
@@ -1955,8 +2103,8 @@ class InterfaceConnectionsListView(PermissionRequiredMixin, ObjectListView):
     ).order_by(
     ).order_by(
         'device'
         'device'
     )
     )
-    filter = filters.InterfaceConnectionFilter
-    filter_form = forms.InterfaceConnectionFilterForm
+    filterset = filters.InterfaceConnectionFilterSet
+    filterset_form = forms.InterfaceConnectionFilterForm
     table = tables.InterfaceConnectionTable
     table = tables.InterfaceConnectionTable
     template_name = 'dcim/interface_connections_list.html'
     template_name = 'dcim/interface_connections_list.html'
 
 
@@ -1980,7 +2128,8 @@ class InterfaceConnectionsListView(PermissionRequiredMixin, ObjectListView):
                 obj.get_connection_status_display(),
                 obj.get_connection_status_display(),
             ])
             ])
             csv_data.append(csv)
             csv_data.append(csv)
-        return csv_data
+
+        return '\n'.join(csv_data)
 
 
 
 
 #
 #
@@ -1990,8 +2139,8 @@ class InterfaceConnectionsListView(PermissionRequiredMixin, ObjectListView):
 class InventoryItemListView(PermissionRequiredMixin, ObjectListView):
 class InventoryItemListView(PermissionRequiredMixin, ObjectListView):
     permission_required = 'dcim.view_inventoryitem'
     permission_required = 'dcim.view_inventoryitem'
     queryset = InventoryItem.objects.prefetch_related('device', 'manufacturer')
     queryset = InventoryItem.objects.prefetch_related('device', 'manufacturer')
-    filter = filters.InventoryItemFilter
-    filter_form = forms.InventoryItemFilterForm
+    filterset = filters.InventoryItemFilterSet
+    filterset_form = forms.InventoryItemFilterForm
     table = tables.InventoryItemTable
     table = tables.InventoryItemTable
     template_name = 'dcim/inventoryitem_list.html'
     template_name = 'dcim/inventoryitem_list.html'
 
 
@@ -2025,7 +2174,7 @@ class InventoryItemBulkImportView(PermissionRequiredMixin, BulkImportView):
 class InventoryItemBulkEditView(PermissionRequiredMixin, BulkEditView):
 class InventoryItemBulkEditView(PermissionRequiredMixin, BulkEditView):
     permission_required = 'dcim.change_inventoryitem'
     permission_required = 'dcim.change_inventoryitem'
     queryset = InventoryItem.objects.prefetch_related('device', 'manufacturer')
     queryset = InventoryItem.objects.prefetch_related('device', 'manufacturer')
-    filter = filters.InventoryItemFilter
+    filterset = filters.InventoryItemFilterSet
     table = tables.InventoryItemTable
     table = tables.InventoryItemTable
     form = forms.InventoryItemBulkEditForm
     form = forms.InventoryItemBulkEditForm
     default_return_url = 'dcim:inventoryitem_list'
     default_return_url = 'dcim:inventoryitem_list'
@@ -2047,8 +2196,8 @@ class VirtualChassisListView(PermissionRequiredMixin, ObjectListView):
     permission_required = 'dcim.view_virtualchassis'
     permission_required = 'dcim.view_virtualchassis'
     queryset = VirtualChassis.objects.prefetch_related('master').annotate(member_count=Count('members'))
     queryset = VirtualChassis.objects.prefetch_related('master').annotate(member_count=Count('members'))
     table = tables.VirtualChassisTable
     table = tables.VirtualChassisTable
-    filter = filters.VirtualChassisFilter
-    filter_form = forms.VirtualChassisFilterForm
+    filterset = filters.VirtualChassisFilterSet
+    filterset_form = forms.VirtualChassisFilterForm
     template_name = 'dcim/virtualchassis_list.html'
     template_name = 'dcim/virtualchassis_list.html'
 
 
 
 
@@ -2290,8 +2439,8 @@ class PowerPanelListView(PermissionRequiredMixin, ObjectListView):
     ).annotate(
     ).annotate(
         powerfeed_count=Count('powerfeeds')
         powerfeed_count=Count('powerfeeds')
     )
     )
-    filter = filters.PowerPanelFilter
-    filter_form = forms.PowerPanelFilterForm
+    filterset = filters.PowerPanelFilterSet
+    filterset_form = forms.PowerPanelFilterForm
     table = tables.PowerPanelTable
     table = tables.PowerPanelTable
     template_name = 'dcim/powerpanel_list.html'
     template_name = 'dcim/powerpanel_list.html'
 
 
@@ -2345,7 +2494,7 @@ class PowerPanelBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
     ).annotate(
     ).annotate(
         rack_count=Count('powerfeeds')
         rack_count=Count('powerfeeds')
     )
     )
-    filter = filters.PowerPanelFilter
+    filterset = filters.PowerPanelFilterSet
     table = tables.PowerPanelTable
     table = tables.PowerPanelTable
     default_return_url = 'dcim:powerpanel_list'
     default_return_url = 'dcim:powerpanel_list'
 
 
@@ -2359,8 +2508,8 @@ class PowerFeedListView(PermissionRequiredMixin, ObjectListView):
     queryset = PowerFeed.objects.prefetch_related(
     queryset = PowerFeed.objects.prefetch_related(
         'power_panel', 'rack'
         'power_panel', 'rack'
     )
     )
-    filter = filters.PowerFeedFilter
-    filter_form = forms.PowerFeedFilterForm
+    filterset = filters.PowerFeedFilterSet
+    filterset_form = forms.PowerFeedFilterForm
     table = tables.PowerFeedTable
     table = tables.PowerFeedTable
     template_name = 'dcim/powerfeed_list.html'
     template_name = 'dcim/powerfeed_list.html'
 
 
@@ -2405,7 +2554,7 @@ class PowerFeedBulkImportView(PermissionRequiredMixin, BulkImportView):
 class PowerFeedBulkEditView(PermissionRequiredMixin, BulkEditView):
 class PowerFeedBulkEditView(PermissionRequiredMixin, BulkEditView):
     permission_required = 'dcim.change_powerfeed'
     permission_required = 'dcim.change_powerfeed'
     queryset = PowerFeed.objects.prefetch_related('power_panel', 'rack')
     queryset = PowerFeed.objects.prefetch_related('power_panel', 'rack')
-    filter = filters.PowerFeedFilter
+    filterset = filters.PowerFeedFilterSet
     table = tables.PowerFeedTable
     table = tables.PowerFeedTable
     form = forms.PowerFeedBulkEditForm
     form = forms.PowerFeedBulkEditForm
     default_return_url = 'dcim:powerfeed_list'
     default_return_url = 'dcim:powerfeed_list'
@@ -2414,6 +2563,6 @@ class PowerFeedBulkEditView(PermissionRequiredMixin, BulkEditView):
 class PowerFeedBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 class PowerFeedBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
     permission_required = 'dcim.delete_powerfeed'
     permission_required = 'dcim.delete_powerfeed'
     queryset = PowerFeed.objects.prefetch_related('power_panel', 'rack')
     queryset = PowerFeed.objects.prefetch_related('power_panel', 'rack')
-    filter = filters.PowerFeedFilter
+    filterset = filters.PowerFeedFilterSet
     table = tables.PowerFeedTable
     table = tables.PowerFeedTable
     default_return_url = 'dcim:powerfeed_list'
     default_return_url = 'dcim:powerfeed_list'

+ 0 - 14
netbox/extras/__init__.py

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

+ 3 - 17
netbox/extras/admin.py

@@ -3,9 +3,7 @@ from django.contrib import admin
 
 
 from netbox.admin import admin_site
 from netbox.admin import admin_site
 from utilities.forms import LaxURLField
 from utilities.forms import LaxURLField
-from .models import (
-    CustomField, CustomFieldChoice, CustomLink, Graph, ExportTemplate, ReportResult, TopologyMap, Webhook,
-)
+from .models import CustomField, CustomFieldChoice, CustomLink, Graph, ExportTemplate, ReportResult, Webhook
 from .reports import get_report
 from .reports import get_report
 
 
 
 
@@ -133,10 +131,10 @@ class CustomLinkAdmin(admin.ModelAdmin):
 @admin.register(Graph, site=admin_site)
 @admin.register(Graph, site=admin_site)
 class GraphAdmin(admin.ModelAdmin):
 class GraphAdmin(admin.ModelAdmin):
     list_display = [
     list_display = [
-        'name', 'type', 'weight', 'source',
+        'name', 'type', 'weight', 'template_language', 'source',
     ]
     ]
     list_filter = [
     list_filter = [
-        'type',
+        'type', 'template_language',
     ]
     ]
 
 
 
 
@@ -197,15 +195,3 @@ class ReportResultAdmin(admin.ModelAdmin):
     def passing(self, obj):
     def passing(self, obj):
         return not obj.failed
         return not obj.failed
     passing.boolean = True
     passing.boolean = True
-
-
-#
-# Topology maps
-#
-
-@admin.register(TopologyMap, site=admin_site)
-class TopologyMapAdmin(admin.ModelAdmin):
-    list_display = ['name', 'slug', 'site']
-    prepopulated_fields = {
-        'slug': ['name'],
-    }

+ 8 - 8
netbox/extras/api/customfields.py

@@ -5,7 +5,7 @@ from django.db import transaction
 from rest_framework import serializers
 from rest_framework import serializers
 from rest_framework.exceptions import ValidationError
 from rest_framework.exceptions import ValidationError
 
 
-from extras.constants import *
+from extras.choices import *
 from extras.models import CustomField, CustomFieldChoice, CustomFieldValue
 from extras.models import CustomField, CustomFieldChoice, CustomFieldValue
 from utilities.api import ValidatedModelSerializer
 from utilities.api import ValidatedModelSerializer
 
 
@@ -39,7 +39,7 @@ class CustomFieldsSerializer(serializers.BaseSerializer):
             if value not in [None, '']:
             if value not in [None, '']:
 
 
                 # Validate integer
                 # Validate integer
-                if cf.type == CF_TYPE_INTEGER:
+                if cf.type == CustomFieldTypeChoices.TYPE_INTEGER:
                     try:
                     try:
                         int(value)
                         int(value)
                     except ValueError:
                     except ValueError:
@@ -48,13 +48,13 @@ class CustomFieldsSerializer(serializers.BaseSerializer):
                         )
                         )
 
 
                 # Validate boolean
                 # 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(
                     raise ValidationError(
                         "Invalid value for boolean field {}: {}".format(field_name, value)
                         "Invalid value for boolean field {}: {}".format(field_name, value)
                     )
                     )
 
 
                 # Validate date
                 # Validate date
-                if cf.type == CF_TYPE_DATE:
+                if cf.type == CustomFieldTypeChoices.TYPE_DATE:
                     try:
                     try:
                         datetime.strptime(value, '%Y-%m-%d')
                         datetime.strptime(value, '%Y-%m-%d')
                     except ValueError:
                     except ValueError:
@@ -63,7 +63,7 @@ class CustomFieldsSerializer(serializers.BaseSerializer):
                         )
                         )
 
 
                 # Validate selected choice
                 # Validate selected choice
-                if cf.type == CF_TYPE_SELECT:
+                if cf.type == CustomFieldTypeChoices.TYPE_SELECT:
                     try:
                     try:
                         value = int(value)
                         value = int(value)
                     except ValueError:
                     except ValueError:
@@ -102,7 +102,7 @@ class CustomFieldModelSerializer(ValidatedModelSerializer):
             instance.custom_fields = {}
             instance.custom_fields = {}
             for field in fields:
             for field in fields:
                 value = instance.cf.get(field.name)
                 value = instance.cf.get(field.name)
-                if field.type == 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
                     instance.custom_fields[field.name] = CustomFieldChoiceSerializer(value).data
                 else:
                 else:
                     instance.custom_fields[field.name] = value
                     instance.custom_fields[field.name] = value
@@ -134,9 +134,9 @@ class CustomFieldModelSerializer(ValidatedModelSerializer):
             # Populate initial data using custom field default values
             # Populate initial data using custom field default values
             for field in fields:
             for field in fields:
                 if field.name not in self.initial_data['custom_fields'] and field.default:
                 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
                         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)
                         field_value = bool(field.default)
                     else:
                     else:
                         field_value = field.default
                         field_value = field.default

+ 65 - 21
netbox/extras/api/serializers.py

@@ -8,10 +8,10 @@ from dcim.api.nested_serializers import (
     NestedRegionSerializer, NestedSiteSerializer,
     NestedRegionSerializer, NestedSiteSerializer,
 )
 )
 from dcim.models import Device, DeviceRole, Platform, Rack, Region, Site
 from dcim.models import Device, DeviceRole, Platform, Rack, Region, Site
+from extras.choices import *
 from extras.constants import *
 from extras.constants import *
 from extras.models import (
 from extras.models import (
-    ConfigContext, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, TopologyMap,
-    Tag
+    ConfigContext, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, Tag,
 )
 )
 from tenancy.api.nested_serializers import NestedTenantSerializer, NestedTenantGroupSerializer
 from tenancy.api.nested_serializers import NestedTenantSerializer, NestedTenantGroupSerializer
 from tenancy.models import Tenant, TenantGroup
 from tenancy.models import Tenant, TenantGroup
@@ -28,17 +28,21 @@ from .nested_serializers import *
 #
 #
 
 
 class GraphSerializer(ValidatedModelSerializer):
 class GraphSerializer(ValidatedModelSerializer):
-    type = ChoiceField(choices=GRAPH_TYPE_CHOICES)
+    type = ContentTypeField(
+        queryset=ContentType.objects.filter(GRAPH_MODELS),
+    )
 
 
     class Meta:
     class Meta:
         model = Graph
         model = Graph
-        fields = ['id', 'type', 'weight', 'name', 'source', 'link']
+        fields = ['id', 'type', 'weight', 'name', 'template_language', 'source', 'link']
 
 
 
 
 class RenderedGraphSerializer(serializers.ModelSerializer):
 class RenderedGraphSerializer(serializers.ModelSerializer):
     embed_url = serializers.SerializerMethodField()
     embed_url = serializers.SerializerMethodField()
     embed_link = serializers.SerializerMethodField()
     embed_link = serializers.SerializerMethodField()
-    type = ChoiceField(choices=GRAPH_TYPE_CHOICES)
+    type = ContentTypeField(
+        queryset=ContentType.objects.all()
+    )
 
 
     class Meta:
     class Meta:
         model = Graph
         model = Graph
@@ -57,8 +61,8 @@ class RenderedGraphSerializer(serializers.ModelSerializer):
 
 
 class ExportTemplateSerializer(ValidatedModelSerializer):
 class ExportTemplateSerializer(ValidatedModelSerializer):
     template_language = ChoiceField(
     template_language = ChoiceField(
-        choices=TEMPLATE_LANGUAGE_CHOICES,
-        default=TEMPLATE_LANGUAGE_JINJA2
+        choices=TemplateLanguageChoices,
+        default=TemplateLanguageChoices.LANGUAGE_JINJA2
     )
     )
 
 
     class Meta:
     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
 # Tags
 #
 #
@@ -181,12 +173,18 @@ class ConfigContextSerializer(ValidatedModelSerializer):
         required=False,
         required=False,
         many=True
         many=True
     )
     )
+    tags = serializers.SlugRelatedField(
+        queryset=Tag.objects.all(),
+        slug_field='slug',
+        required=False,
+        many=True
+    )
 
 
     class Meta:
     class Meta:
         model = ConfigContext
         model = ConfigContext
         fields = [
         fields = [
             'id', 'name', 'weight', 'description', 'is_active', 'regions', 'sites', 'roles', 'platforms',
             'id', 'name', 'weight', 'description', 'is_active', 'regions', 'sites', 'roles', 'platforms',
-            'tenant_groups', 'tenants', 'data',
+            'tenant_groups', 'tenants', 'tags', 'data',
         ]
         ]
 
 
 
 
@@ -213,6 +211,52 @@ class ReportDetailSerializer(ReportSerializer):
     result = ReportResultSerializer()
     result = ReportResultSerializer()
 
 
 
 
+#
+# Scripts
+#
+
+class ScriptSerializer(serializers.Serializer):
+    id = serializers.SerializerMethodField(read_only=True)
+    name = serializers.SerializerMethodField(read_only=True)
+    description = serializers.SerializerMethodField(read_only=True)
+    vars = serializers.SerializerMethodField(read_only=True)
+
+    def get_id(self, instance):
+        return '{}.{}'.format(instance.__module__, instance.__name__)
+
+    def get_name(self, instance):
+        return getattr(instance.Meta, 'name', instance.__name__)
+
+    def get_description(self, instance):
+        return getattr(instance.Meta, 'description', '')
+
+    def get_vars(self, instance):
+        return {
+            k: v.__class__.__name__ for k, v in instance._get_vars().items()
+        }
+
+
+class ScriptInputSerializer(serializers.Serializer):
+    data = serializers.JSONField()
+    commit = serializers.BooleanField()
+
+
+class ScriptLogMessageSerializer(serializers.Serializer):
+    status = serializers.SerializerMethodField(read_only=True)
+    message = serializers.SerializerMethodField(read_only=True)
+
+    def get_status(self, instance):
+        return LOG_LEVEL_CODES.get(instance[0])
+
+    def get_message(self, instance):
+        return instance[1]
+
+
+class ScriptOutputSerializer(serializers.Serializer):
+    log = ScriptLogMessageSerializer(many=True, read_only=True)
+    output = serializers.CharField(read_only=True)
+
+
 #
 #
 # Change logging
 # Change logging
 #
 #
@@ -222,7 +266,7 @@ class ObjectChangeSerializer(serializers.ModelSerializer):
         read_only=True
         read_only=True
     )
     )
     action = ChoiceField(
     action = ChoiceField(
-        choices=OBJECTCHANGE_ACTION_CHOICES,
+        choices=ObjectChangeActionChoices,
         read_only=True
         read_only=True
     )
     )
     changed_object_type = ContentTypeField(
     changed_object_type = ContentTypeField(

+ 3 - 3
netbox/extras/api/urls.py

@@ -26,9 +26,6 @@ router.register(r'graphs', views.GraphViewSet)
 # Export templates
 # Export templates
 router.register(r'export-templates', views.ExportTemplateViewSet)
 router.register(r'export-templates', views.ExportTemplateViewSet)
 
 
-# Topology maps
-router.register(r'topology-maps', views.TopologyMapViewSet)
-
 # Tags
 # Tags
 router.register(r'tags', views.TagViewSet)
 router.register(r'tags', views.TagViewSet)
 
 
@@ -41,6 +38,9 @@ router.register(r'config-contexts', views.ConfigContextViewSet)
 # Reports
 # Reports
 router.register(r'reports', views.ReportViewSet, basename='report')
 router.register(r'reports', views.ReportViewSet, basename='report')
 
 
+# Scripts
+router.register(r'scripts', views.ScriptViewSet, basename='script')
+
 # Change logging
 # Change logging
 router.register(r'object-changes', views.ObjectChangeViewSet)
 router.register(r'object-changes', views.ObjectChangeViewSet)
 
 

+ 62 - 40
netbox/extras/api/views.py

@@ -2,8 +2,8 @@ from collections import OrderedDict
 
 
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.contenttypes.models import ContentType
 from django.db.models import Count
 from django.db.models import Count
-from django.http import Http404, 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.decorators import action
 from rest_framework.exceptions import PermissionDenied
 from rest_framework.exceptions import PermissionDenied
 from rest_framework.response import Response
 from rest_framework.response import Response
@@ -11,10 +11,10 @@ from rest_framework.viewsets import ReadOnlyModelViewSet, ViewSet
 
 
 from extras import filters
 from extras import filters
 from extras.models import (
 from extras.models import (
-    ConfigContext, CustomFieldChoice, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, TopologyMap,
-    Tag,
+    ConfigContext, CustomFieldChoice, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, Tag,
 )
 )
 from extras.reports import get_report, get_reports
 from extras.reports import get_report, get_reports
+from extras.scripts import get_script, get_scripts
 from utilities.api import FieldChoicesViewSet, IsAuthenticatedOrLoginNotRequired, ModelViewSet
 from utilities.api import FieldChoicesViewSet, IsAuthenticatedOrLoginNotRequired, ModelViewSet
 from . import serializers
 from . import serializers
 
 
@@ -25,9 +25,9 @@ from . import serializers
 
 
 class ExtrasFieldChoicesViewSet(FieldChoicesViewSet):
 class ExtrasFieldChoicesViewSet(FieldChoicesViewSet):
     fields = (
     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):
 class GraphViewSet(ModelViewSet):
     queryset = Graph.objects.all()
     queryset = Graph.objects.all()
     serializer_class = serializers.GraphSerializer
     serializer_class = serializers.GraphSerializer
-    filterset_class = filters.GraphFilter
+    filterset_class = filters.GraphFilterSet
 
 
 
 
 #
 #
@@ -112,35 +112,7 @@ class GraphViewSet(ModelViewSet):
 class ExportTemplateViewSet(ModelViewSet):
 class ExportTemplateViewSet(ModelViewSet):
     queryset = ExportTemplate.objects.all()
     queryset = ExportTemplate.objects.all()
     serializer_class = serializers.ExportTemplateSerializer
     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)
         tagged_items=Count('extras_taggeditem_items', distinct=True)
     )
     )
     serializer_class = serializers.TagSerializer
     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',
         'regions', 'sites', 'roles', 'platforms', 'tenant_groups', 'tenants',
     )
     )
     serializer_class = serializers.ConfigContextSerializer
     serializer_class = serializers.ConfigContextSerializer
-    filterset_class = filters.ConfigContextFilter
+    filterset_class = filters.ConfigContextFilterSet
 
 
 
 
 #
 #
@@ -252,6 +224,56 @@ class ReportViewSet(ViewSet):
         return Response(serializer.data)
         return Response(serializer.data)
 
 
 
 
+#
+# Scripts
+#
+
+class ScriptViewSet(ViewSet):
+    permission_classes = [IsAuthenticatedOrLoginNotRequired]
+    _ignore_model_permissions = True
+    exclude_from_schema = True
+    lookup_value_regex = '[^/]+'  # Allow dots
+
+    def _get_script(self, pk):
+        module_name, script_name = pk.split('.')
+        script = get_script(module_name, script_name)
+        if script is None:
+            raise Http404
+        return script
+
+    def list(self, request):
+
+        flat_list = []
+        for script_list in get_scripts().values():
+            flat_list.extend(script_list.values())
+
+        serializer = serializers.ScriptSerializer(flat_list, many=True, context={'request': request})
+
+        return Response(serializer.data)
+
+    def retrieve(self, request, pk):
+        script = self._get_script(pk)
+        serializer = serializers.ScriptSerializer(script, context={'request': request})
+
+        return Response(serializer.data)
+
+    def post(self, request, pk):
+        """
+        Run a Script identified as "<module>.<script>".
+        """
+        script = self._get_script(pk)()
+        input_serializer = serializers.ScriptInputSerializer(data=request.data)
+
+        if input_serializer.is_valid():
+            output = script.run(input_serializer.data['data'])
+            script.output = output
+            output_serializer = serializers.ScriptOutputSerializer(script)
+
+            return Response(output_serializer.data)
+
+        return Response(input_serializer.errors, status=status.HTTP_400_BAD_REQUEST)
+
+
 #
 #
 # Change logging
 # Change logging
 #
 #
@@ -262,4 +284,4 @@ class ObjectChangeViewSet(ReadOnlyModelViewSet):
     """
     """
     queryset = ObjectChange.objects.prefetch_related('user')
     queryset = ObjectChange.objects.prefetch_related('user')
     serializer_class = serializers.ObjectChangeSerializer
     serializer_class = serializers.ObjectChangeSerializer
-    filterset_class = filters.ObjectChangeFilter
+    filterset_class = filters.ObjectChangeFilterSet

+ 16 - 23
netbox/extras/apps.py

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

+ 140 - 0
netbox/extras/choices.py

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

+ 159 - 205
netbox/extras/constants.py

@@ -1,177 +1,127 @@
-# Models which support custom fields
-CUSTOMFIELD_MODELS = [
-    'circuits.circuit',
-    'circuits.provider',
-    'dcim.device',
-    'dcim.devicetype',
-    'dcim.powerfeed',
-    'dcim.rack',
-    'dcim.site',
-    'ipam.aggregate',
-    'ipam.ipaddress',
-    'ipam.prefix',
-    'ipam.service',
-    'ipam.vlan',
-    'ipam.vrf',
-    'secrets.secret',
-    'tenancy.tenant',
-    'virtualization.cluster',
-    'virtualization.virtualmachine',
-]
+from django.db.models import Q
 
 
-# Custom field types
-CF_TYPE_TEXT = 100
-CF_TYPE_INTEGER = 200
-CF_TYPE_BOOLEAN = 300
-CF_TYPE_DATE = 400
-CF_TYPE_URL = 500
-CF_TYPE_SELECT = 600
-CUSTOMFIELD_TYPE_CHOICES = (
-    (CF_TYPE_TEXT, 'Text'),
-    (CF_TYPE_INTEGER, 'Integer'),
-    (CF_TYPE_BOOLEAN, 'Boolean (true/false)'),
-    (CF_TYPE_DATE, 'Date'),
-    (CF_TYPE_URL, 'URL'),
-    (CF_TYPE_SELECT, 'Selection'),
-)
 
 
-# Custom field filter logic choices
-CF_FILTER_DISABLED = 0
-CF_FILTER_LOOSE = 1
-CF_FILTER_EXACT = 2
-CF_FILTER_CHOICES = (
-    (CF_FILTER_DISABLED, 'Disabled'),
-    (CF_FILTER_LOOSE, 'Loose'),
-    (CF_FILTER_EXACT, 'Exact'),
+# Models which support custom fields
+CUSTOMFIELD_MODELS = Q(
+    Q(app_label='circuits', model__in=[
+        'circuit',
+        'provider',
+    ]) |
+    Q(app_label='dcim', model__in=[
+        'device',
+        'devicetype',
+        'powerfeed',
+        'rack',
+        'site',
+    ]) |
+    Q(app_label='ipam', model__in=[
+        'aggregate',
+        'ipaddress',
+        'prefix',
+        'service',
+        'vlan',
+        'vrf',
+    ]) |
+    Q(app_label='secrets', model__in=[
+        'secret',
+    ]) |
+    Q(app_label='tenancy', model__in=[
+        'tenant',
+    ]) |
+    Q(app_label='virtualization', model__in=[
+        'cluster',
+        'virtualmachine',
+    ])
 )
 )
 
 
 # Custom links
 # Custom links
-CUSTOMLINK_MODELS = [
-    'circuits.circuit',
-    'circuits.provider',
-    'dcim.cable',
-    'dcim.device',
-    'dcim.devicetype',
-    'dcim.powerpanel',
-    'dcim.powerfeed',
-    'dcim.rack',
-    'dcim.site',
-    'ipam.aggregate',
-    'ipam.ipaddress',
-    'ipam.prefix',
-    'ipam.service',
-    'ipam.vlan',
-    'ipam.vrf',
-    'secrets.secret',
-    'tenancy.tenant',
-    'virtualization.cluster',
-    'virtualization.virtualmachine',
-]
-
-BUTTON_CLASS_DEFAULT = 'default'
-BUTTON_CLASS_PRIMARY = 'primary'
-BUTTON_CLASS_SUCCESS = 'success'
-BUTTON_CLASS_INFO = 'info'
-BUTTON_CLASS_WARNING = 'warning'
-BUTTON_CLASS_DANGER = 'danger'
-BUTTON_CLASS_LINK = 'link'
-BUTTON_CLASS_CHOICES = (
-    (BUTTON_CLASS_DEFAULT, 'Default'),
-    (BUTTON_CLASS_PRIMARY, 'Primary (blue)'),
-    (BUTTON_CLASS_SUCCESS, 'Success (green)'),
-    (BUTTON_CLASS_INFO, 'Info (aqua)'),
-    (BUTTON_CLASS_WARNING, 'Warning (orange)'),
-    (BUTTON_CLASS_DANGER, 'Danger (red)'),
-    (BUTTON_CLASS_LINK, 'None (link)'),
+CUSTOMLINK_MODELS = Q(
+    Q(app_label='circuits', model__in=[
+        'circuit',
+        'provider',
+    ]) |
+    Q(app_label='dcim', model__in=[
+        'cable',
+        'device',
+        'devicetype',
+        'powerpanel',
+        'powerfeed',
+        'rack',
+        'site',
+    ]) |
+    Q(app_label='ipam', model__in=[
+        'aggregate',
+        'ipaddress',
+        'prefix',
+        'service',
+        'vlan',
+        'vrf',
+    ]) |
+    Q(app_label='secrets', model__in=[
+        'secret',
+    ]) |
+    Q(app_label='tenancy', model__in=[
+        'tenant',
+    ]) |
+    Q(app_label='virtualization', model__in=[
+        'cluster',
+        'virtualmachine',
+    ])
 )
 )
 
 
-# Graph types
-GRAPH_TYPE_INTERFACE = 100
-GRAPH_TYPE_DEVICE = 150
-GRAPH_TYPE_PROVIDER = 200
-GRAPH_TYPE_SITE = 300
-GRAPH_TYPE_CHOICES = (
-    (GRAPH_TYPE_INTERFACE, 'Interface'),
-    (GRAPH_TYPE_DEVICE, 'Device'),
-    (GRAPH_TYPE_PROVIDER, 'Provider'),
-    (GRAPH_TYPE_SITE, 'Site'),
+# Models which can have Graphs associated with them
+GRAPH_MODELS = Q(
+    Q(app_label='circuits', model__in=[
+        'provider',
+    ]) |
+    Q(app_label='dcim', model__in=[
+        'device',
+        'interface',
+        'site',
+    ])
 )
 )
 
 
 # Models which support export templates
 # Models which support export templates
-EXPORTTEMPLATE_MODELS = [
-    'circuits.circuit',
-    'circuits.provider',
-    'dcim.cable',
-    'dcim.consoleport',
-    'dcim.device',
-    'dcim.devicetype',
-    'dcim.interface',
-    'dcim.inventoryitem',
-    'dcim.manufacturer',
-    'dcim.powerpanel',
-    'dcim.powerport',
-    'dcim.powerfeed',
-    'dcim.rack',
-    'dcim.rackgroup',
-    'dcim.region',
-    'dcim.site',
-    'dcim.virtualchassis',
-    'ipam.aggregate',
-    'ipam.ipaddress',
-    'ipam.prefix',
-    'ipam.service',
-    'ipam.vlan',
-    'ipam.vrf',
-    'secrets.secret',
-    'tenancy.tenant',
-    'virtualization.cluster',
-    'virtualization.virtualmachine',
-]
-
-# ExportTemplate language choices
-TEMPLATE_LANGUAGE_DJANGO = 10
-TEMPLATE_LANGUAGE_JINJA2 = 20
-TEMPLATE_LANGUAGE_CHOICES = (
-    (TEMPLATE_LANGUAGE_DJANGO, 'Django'),
-    (TEMPLATE_LANGUAGE_JINJA2, 'Jinja2'),
-)
-
-# Topology map types
-TOPOLOGYMAP_TYPE_NETWORK = 1
-TOPOLOGYMAP_TYPE_CONSOLE = 2
-TOPOLOGYMAP_TYPE_POWER = 3
-TOPOLOGYMAP_TYPE_CHOICES = (
-    (TOPOLOGYMAP_TYPE_NETWORK, 'Network'),
-    (TOPOLOGYMAP_TYPE_CONSOLE, 'Console'),
-    (TOPOLOGYMAP_TYPE_POWER, 'Power'),
-)
-
-# Change log actions
-OBJECTCHANGE_ACTION_CREATE = 1
-OBJECTCHANGE_ACTION_UPDATE = 2
-OBJECTCHANGE_ACTION_DELETE = 3
-OBJECTCHANGE_ACTION_CHOICES = (
-    (OBJECTCHANGE_ACTION_CREATE, 'Created'),
-    (OBJECTCHANGE_ACTION_UPDATE, 'Updated'),
-    (OBJECTCHANGE_ACTION_DELETE, 'Deleted'),
-)
-
-# User action types
-ACTION_CREATE = 1
-ACTION_IMPORT = 2
-ACTION_EDIT = 3
-ACTION_BULK_EDIT = 4
-ACTION_DELETE = 5
-ACTION_BULK_DELETE = 6
-ACTION_BULK_CREATE = 7
-ACTION_CHOICES = (
-    (ACTION_CREATE, 'created'),
-    (ACTION_BULK_CREATE, 'bulk created'),
-    (ACTION_IMPORT, 'imported'),
-    (ACTION_EDIT, 'modified'),
-    (ACTION_BULK_EDIT, 'bulk edited'),
-    (ACTION_DELETE, 'deleted'),
-    (ACTION_BULK_DELETE, 'bulk deleted'),
+EXPORTTEMPLATE_MODELS = Q(
+    Q(app_label='circuits', model__in=[
+        'circuit',
+        'provider',
+    ]) |
+    Q(app_label='dcim', model__in=[
+        'cable',
+        'consoleport',
+        'device',
+        'devicetype',
+        'interface',
+        'inventoryitem',
+        'manufacturer',
+        'powerpanel',
+        'powerport',
+        'powerfeed',
+        'rack',
+        'rackgroup',
+        'region',
+        'site',
+        'virtualchassis',
+    ]) |
+    Q(app_label='ipam', model__in=[
+        'aggregate',
+        'ipaddress',
+        'prefix',
+        'service',
+        'vlan',
+        'vrf',
+    ]) |
+    Q(app_label='secrets', model__in=[
+        'secret',
+    ]) |
+    Q(app_label='tenancy', model__in=[
+        'tenant',
+    ]) |
+    Q(app_label='virtualization', model__in=[
+        'cluster',
+        'virtualmachine',
+    ])
 )
 )
 
 
 # Report logging levels
 # Report logging levels
@@ -188,45 +138,49 @@ LOG_LEVEL_CODES = {
     LOG_FAILURE: 'failure',
     LOG_FAILURE: 'failure',
 }
 }
 
 
-# webhook content types
-WEBHOOK_CT_JSON = 1
-WEBHOOK_CT_X_WWW_FORM_ENCODED = 2
-WEBHOOK_CT_CHOICES = (
-    (WEBHOOK_CT_JSON, 'application/json'),
-    (WEBHOOK_CT_X_WWW_FORM_ENCODED, 'application/x-www-form-urlencoded'),
-)
-
 # Models which support registered webhooks
 # Models which support registered webhooks
-WEBHOOK_MODELS = [
-    'circuits.circuit',
-    'circuits.provider',
-    'dcim.cable',
-    'dcim.consoleport',
-    'dcim.consoleserverport',
-    'dcim.device',
-    'dcim.devicebay',
-    'dcim.devicetype',
-    'dcim.interface',
-    'dcim.inventoryitem',
-    'dcim.frontport',
-    'dcim.manufacturer',
-    'dcim.poweroutlet',
-    'dcim.powerpanel',
-    'dcim.powerport',
-    'dcim.powerfeed',
-    'dcim.rack',
-    'dcim.rearport',
-    'dcim.region',
-    'dcim.site',
-    'dcim.virtualchassis',
-    'ipam.aggregate',
-    'ipam.ipaddress',
-    'ipam.prefix',
-    'ipam.service',
-    'ipam.vlan',
-    'ipam.vrf',
-    'secrets.secret',
-    'tenancy.tenant',
-    'virtualization.cluster',
-    'virtualization.virtualmachine',
-]
+WEBHOOK_MODELS = Q(
+    Q(app_label='circuits', model__in=[
+        'circuit',
+        'provider',
+    ]) |
+    Q(app_label='dcim', model__in=[
+        'cable',
+        'consoleport',
+        'consoleserverport',
+        'device',
+        'devicebay',
+        'devicetype',
+        'frontport',
+        'interface',
+        'inventoryitem',
+        'manufacturer',
+        'poweroutlet',
+        'powerpanel',
+        'powerport',
+        'powerfeed',
+        'rack',
+        'rearport',
+        'region',
+        'site',
+        'virtualchassis',
+    ]) |
+    Q(app_label='ipam', model__in=[
+        'aggregate',
+        'ipaddress',
+        'prefix',
+        'service',
+        'vlan',
+        'vrf',
+    ]) |
+    Q(app_label='secrets', model__in=[
+        'secret',
+    ]) |
+    Q(app_label='tenancy', model__in=[
+        'tenant',
+    ]) |
+    Q(app_label='virtualization', model__in=[
+        'cluster',
+        'virtualmachine',
+    ])
+)

+ 29 - 37
netbox/extras/filters.py

@@ -4,21 +4,20 @@ from django.db.models import Q
 
 
 from dcim.models import DeviceRole, Platform, Region, Site
 from dcim.models import DeviceRole, Platform, Region, Site
 from tenancy.models import Tenant, TenantGroup
 from tenancy.models import Tenant, TenantGroup
-from .constants import *
-from .models import ConfigContext, CustomField, Graph, ExportTemplate, ObjectChange, Tag, TopologyMap
+from .choices import *
+from .models import ConfigContext, CustomField, Graph, ExportTemplate, ObjectChange, Tag
 
 
 
 
 __all__ = (
 __all__ = (
-    'ConfigContextFilter',
+    'ConfigContextFilterSet',
     'CreatedUpdatedFilterSet',
     'CreatedUpdatedFilterSet',
     'CustomFieldFilter',
     'CustomFieldFilter',
     'CustomFieldFilterSet',
     'CustomFieldFilterSet',
-    'ExportTemplateFilter',
-    'GraphFilter',
-    'LocalConfigContextFilter',
-    'ObjectChangeFilter',
-    'TagFilter',
-    'TopologyMapFilter',
+    'ExportTemplateFilterSet',
+    'GraphFilterSet',
+    'LocalConfigContextFilterSet',
+    'ObjectChangeFilterSet',
+    'TagFilterSet',
 )
 )
 
 
 
 
@@ -39,7 +38,7 @@ class CustomFieldFilter(django_filters.Filter):
             return queryset
             return queryset
 
 
         # Selection fields get special treatment (values must be integers)
         # Selection fields get special treatment (values must be integers)
-        if self.cf_type == CF_TYPE_SELECT:
+        if self.cf_type == CustomFieldTypeChoices.TYPE_SELECT:
             try:
             try:
                 # Treat 0 as None
                 # Treat 0 as None
                 if int(value) == 0:
                 if int(value) == 0:
@@ -56,7 +55,8 @@ class CustomFieldFilter(django_filters.Filter):
                 return queryset.none()
                 return queryset.none()
 
 
         # Apply the assigned filter logic (exact or loose)
         # Apply the assigned filter logic (exact or loose)
-        if self.cf_type == CF_TYPE_BOOLEAN or self.filter_logic == CF_FILTER_EXACT:
+        if (self.cf_type == CustomFieldTypeChoices.TYPE_BOOLEAN or
+                self.filter_logic == CustomFieldFilterLogicChoices.FILTER_EXACT):
             queryset = queryset.filter(
             queryset = queryset.filter(
                 custom_field_values__field__name=self.field_name,
                 custom_field_values__field__name=self.field_name,
                 custom_field_values__serialized_value=value
                 custom_field_values__serialized_value=value
@@ -79,26 +79,30 @@ class CustomFieldFilterSet(django_filters.FilterSet):
         super().__init__(*args, **kwargs)
         super().__init__(*args, **kwargs)
 
 
         obj_type = ContentType.objects.get_for_model(self._meta.model)
         obj_type = ContentType.objects.get_for_model(self._meta.model)
-        custom_fields = CustomField.objects.filter(obj_type=obj_type).exclude(filter_logic=CF_FILTER_DISABLED)
+        custom_fields = CustomField.objects.filter(
+            obj_type=obj_type
+        ).exclude(
+            filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED
+        )
         for cf in custom_fields:
         for cf in custom_fields:
             self.filters['cf_{}'.format(cf.name)] = CustomFieldFilter(field_name=cf.name, custom_field=cf)
             self.filters['cf_{}'.format(cf.name)] = CustomFieldFilter(field_name=cf.name, custom_field=cf)
 
 
 
 
-class GraphFilter(django_filters.FilterSet):
+class GraphFilterSet(django_filters.FilterSet):
 
 
     class Meta:
     class Meta:
         model = Graph
         model = Graph
-        fields = ['type', 'name']
+        fields = ['type', 'name', 'template_language']
 
 
 
 
-class ExportTemplateFilter(django_filters.FilterSet):
+class ExportTemplateFilterSet(django_filters.FilterSet):
 
 
     class Meta:
     class Meta:
         model = ExportTemplate
         model = ExportTemplate
         fields = ['content_type', 'name', 'template_language']
         fields = ['content_type', 'name', 'template_language']
 
 
 
 
-class TagFilter(django_filters.FilterSet):
+class TagFilterSet(django_filters.FilterSet):
     q = django_filters.CharFilter(
     q = django_filters.CharFilter(
         method='search',
         method='search',
         label='Search',
         label='Search',
@@ -117,25 +121,7 @@ class TagFilter(django_filters.FilterSet):
         )
         )
 
 
 
 
-class TopologyMapFilter(django_filters.FilterSet):
-    site_id = django_filters.ModelMultipleChoiceFilter(
-        field_name='site',
-        queryset=Site.objects.all(),
-        label='Site',
-    )
-    site = django_filters.ModelMultipleChoiceFilter(
-        field_name='site__slug',
-        queryset=Site.objects.all(),
-        to_field_name='slug',
-        label='Site (slug)',
-    )
-
-    class Meta:
-        model = TopologyMap
-        fields = ['name', 'slug']
-
-
-class ConfigContextFilter(django_filters.FilterSet):
+class ConfigContextFilterSet(django_filters.FilterSet):
     q = django_filters.CharFilter(
     q = django_filters.CharFilter(
         method='search',
         method='search',
         label='Search',
         label='Search',
@@ -206,6 +192,12 @@ class ConfigContextFilter(django_filters.FilterSet):
         to_field_name='slug',
         to_field_name='slug',
         label='Tenant (slug)',
         label='Tenant (slug)',
     )
     )
+    tag = django_filters.ModelMultipleChoiceFilter(
+        field_name='tags__slug',
+        queryset=Tag.objects.all(),
+        to_field_name='slug',
+        label='Tag (slug)',
+    )
 
 
     class Meta:
     class Meta:
         model = ConfigContext
         model = ConfigContext
@@ -225,7 +217,7 @@ class ConfigContextFilter(django_filters.FilterSet):
 # Filter for Local Config Context Data
 # Filter for Local Config Context Data
 #
 #
 
 
-class LocalConfigContextFilter(django_filters.FilterSet):
+class LocalConfigContextFilterSet(django_filters.FilterSet):
     local_context_data = django_filters.BooleanFilter(
     local_context_data = django_filters.BooleanFilter(
         method='_local_context_data',
         method='_local_context_data',
         label='Has local config context data',
         label='Has local config context data',
@@ -235,7 +227,7 @@ class LocalConfigContextFilter(django_filters.FilterSet):
         return queryset.exclude(local_context_data__isnull=value)
         return queryset.exclude(local_context_data__isnull=value)
 
 
 
 
-class ObjectChangeFilter(django_filters.FilterSet):
+class ObjectChangeFilterSet(django_filters.FilterSet):
     q = django_filters.CharFilter(
     q = django_filters.CharFilter(
         method='search',
         method='search',
         label='Search',
         label='Search',

+ 26 - 10
netbox/extras/forms.py

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

+ 9 - 8
netbox/extras/middleware.py

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

+ 147 - 72
netbox/extras/migrations/0001_initial_squashed_0010_customfield_filter_logic.py → netbox/extras/migrations/0001_initial_squashed_0013_objectchange.py

@@ -1,14 +1,10 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.11.14 on 2018-07-31 02:19
-
-from django.conf import settings
 import django.contrib.postgres.fields.jsonb
 import django.contrib.postgres.fields.jsonb
-from django.db import connection, migrations, models
 import django.db.models.deletion
 import django.db.models.deletion
-import extras.models
+from django.conf import settings
+from django.db import connection, migrations, models
 from django.db.utils import OperationalError
 from django.db.utils import OperationalError
 
 
-from extras.constants import CF_FILTER_DISABLED, CF_FILTER_EXACT, CF_FILTER_LOOSE, CF_TYPE_SELECT
+import extras.models
 
 
 
 
 def verify_postgresql_version(apps, schema_editor):
 def verify_postgresql_version(apps, schema_editor):
@@ -29,17 +25,57 @@ def verify_postgresql_version(apps, schema_editor):
         pass
         pass
 
 
 
 
+def is_filterable_to_filter_logic(apps, schema_editor):
+    CustomField = apps.get_model('extras', 'CustomField')
+    CustomField.objects.filter(is_filterable=False).update(filter_logic=0)
+    CustomField.objects.filter(is_filterable=True).update(filter_logic=1)
+    # Select fields match on primary key only
+    CustomField.objects.filter(is_filterable=True, type=600).update(filter_logic=2)
+
+
 class Migration(migrations.Migration):
 class Migration(migrations.Migration):
 
 
-    replaces = [('extras', '0001_initial'), ('extras', '0002_custom_fields'), ('extras', '0003_exporttemplate_add_description'), ('extras', '0004_topologymap_change_comma_to_semicolon'), ('extras', '0005_useraction_add_bulk_create'), ('extras', '0006_add_imageattachments'), ('extras', '0007_unicode_literals'), ('extras', '0008_reports'), ('extras', '0009_topologymap_type'), ('extras', '0010_customfield_filter_logic')]
+    replaces = [('extras', '0001_initial'), ('extras', '0002_custom_fields'), ('extras', '0003_exporttemplate_add_description'), ('extras', '0004_topologymap_change_comma_to_semicolon'), ('extras', '0005_useraction_add_bulk_create'), ('extras', '0006_add_imageattachments'), ('extras', '0007_unicode_literals'), ('extras', '0008_reports'), ('extras', '0009_topologymap_type'), ('extras', '0010_customfield_filter_logic'), ('extras', '0011_django2'), ('extras', '0012_webhooks'), ('extras', '0013_objectchange')]
 
 
     dependencies = [
     dependencies = [
-        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
         ('dcim', '0002_auto_20160622_1821'),
         ('dcim', '0002_auto_20160622_1821'),
         ('contenttypes', '0002_remove_content_type_name'),
         ('contenttypes', '0002_remove_content_type_name'),
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
     ]
     ]
 
 
     operations = [
     operations = [
+        migrations.CreateModel(
+            name='CustomField',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('type', models.PositiveSmallIntegerField(choices=[(100, 'Text'), (200, 'Integer'), (300, 'Boolean (true/false)'), (400, 'Date'), (500, 'URL'), (600, 'Selection')], default=100)),
+                ('name', models.CharField(max_length=50, unique=True)),
+                ('label', models.CharField(blank=True, help_text="Name of the field as displayed to users (if not provided, the field's name will be used)", max_length=50)),
+                ('description', models.CharField(blank=True, max_length=100)),
+                ('required', models.BooleanField(default=False, help_text='Determines whether this field is required when creating new objects or editing an existing object.')),
+                ('is_filterable', models.BooleanField(default=True, help_text='This field can be used to filter objects.')),
+                ('default', models.CharField(blank=True, help_text='Default value for the field. Use "true" or "false" for booleans.', max_length=100)),
+                ('weight', models.PositiveSmallIntegerField(default=100, help_text='Fields with higher weights appear lower in a form')),
+                ('obj_type', models.ManyToManyField(help_text='The object(s) to which this field applies.', related_name='custom_fields', to='contenttypes.ContentType', verbose_name='Object(s)')),
+            ],
+            options={
+                'ordering': ['weight', 'name'],
+            },
+        ),
+        migrations.CreateModel(
+            name='CustomFieldValue',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('obj_id', models.PositiveIntegerField()),
+                ('serialized_value', models.CharField(max_length=255)),
+                ('field', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='values', to='extras.CustomField')),
+                ('obj_type', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.ContentType')),
+            ],
+            options={
+                'ordering': ['obj_type', 'obj_id'],
+                'unique_together': {('field', 'obj_type', 'obj_id')},
+            },
+        ),
         migrations.CreateModel(
         migrations.CreateModel(
             name='ExportTemplate',
             name='ExportTemplate',
             fields=[
             fields=[
@@ -53,6 +89,20 @@ class Migration(migrations.Migration):
             ],
             ],
             options={
             options={
                 'ordering': ['content_type', 'name'],
                 'ordering': ['content_type', 'name'],
+                'unique_together': {('content_type', 'name')},
+            },
+        ),
+        migrations.CreateModel(
+            name='CustomFieldChoice',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('value', models.CharField(max_length=100)),
+                ('weight', models.PositiveSmallIntegerField(default=100, help_text='Higher weights appear lower in the list')),
+                ('field', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='choices', to='extras.CustomField')),
+            ],
+            options={
+                'ordering': ['field', 'weight', 'value'],
+                'unique_together': {('field', 'value')},
             },
             },
         ),
         ),
         migrations.CreateModel(
         migrations.CreateModel(
@@ -69,6 +119,22 @@ class Migration(migrations.Migration):
                 'ordering': ['type', 'weight', 'name'],
                 'ordering': ['type', 'weight', 'name'],
             },
             },
         ),
         ),
+        migrations.CreateModel(
+            name='ImageAttachment',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('object_id', models.PositiveIntegerField()),
+                ('image', models.ImageField(height_field='image_height', upload_to=extras.models.image_upload, width_field='image_width')),
+                ('image_height', models.PositiveSmallIntegerField()),
+                ('image_width', models.PositiveSmallIntegerField()),
+                ('name', models.CharField(blank=True, max_length=50)),
+                ('created', models.DateTimeField(auto_now_add=True)),
+                ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')),
+            ],
+            options={
+                'ordering': ['name'],
+            },
+        ),
         migrations.CreateModel(
         migrations.CreateModel(
             name='TopologyMap',
             name='TopologyMap',
             fields=[
             fields=[
@@ -78,7 +144,6 @@ class Migration(migrations.Migration):
                 ('device_patterns', models.TextField(help_text='Identify devices to include in the diagram using regular expressions, one per line. Each line will result in a new tier of the drawing. Separate multiple regexes within a line using semicolons. Devices will be rendered in the order they are defined.')),
                 ('device_patterns', models.TextField(help_text='Identify devices to include in the diagram using regular expressions, one per line. Each line will result in a new tier of the drawing. Separate multiple regexes within a line using semicolons. Devices will be rendered in the order they are defined.')),
                 ('description', models.CharField(blank=True, max_length=100)),
                 ('description', models.CharField(blank=True, max_length=100)),
                 ('site', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='topology_maps', to='dcim.Site')),
                 ('site', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='topology_maps', to='dcim.Site')),
-                ('type', models.PositiveSmallIntegerField(choices=[(1, 'Network'), (2, 'Console'), (3, 'Power')], default=1)),
             ],
             ],
             options={
             options={
                 'ordering': ['name'],
                 'ordering': ['name'],
@@ -99,92 +164,102 @@ class Migration(migrations.Migration):
                 'ordering': ['-time'],
                 'ordering': ['-time'],
             },
             },
         ),
         ),
-        migrations.AlterUniqueTogether(
-            name='exporttemplate',
-            unique_together=set([('content_type', 'name')]),
+        migrations.RunPython(
+            code=verify_postgresql_version,
         ),
         ),
         migrations.CreateModel(
         migrations.CreateModel(
-            name='CustomField',
+            name='ReportResult',
             fields=[
             fields=[
                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('type', models.PositiveSmallIntegerField(choices=[(100, 'Text'), (200, 'Integer'), (300, 'Boolean (true/false)'), (400, 'Date'), (500, 'URL'), (600, 'Selection')], default=100)),
-                ('name', models.CharField(max_length=50, unique=True)),
-                ('label', models.CharField(blank=True, help_text="Name of the field as displayed to users (if not provided, the field's name will be used)", max_length=50)),
-                ('description', models.CharField(blank=True, max_length=100)),
-                ('required', models.BooleanField(default=False, help_text='If true, this field is required when creating new objects or editing an existing object.')),
-                ('default', models.CharField(blank=True, help_text='Default value for the field. Use "true" or "false" for booleans.', max_length=100)),
-                ('weight', models.PositiveSmallIntegerField(default=100, help_text='Fields with higher weights appear lower in a form.')),
-                ('obj_type', models.ManyToManyField(help_text='The object(s) to which this field applies.', related_name='custom_fields', to='contenttypes.ContentType', verbose_name='Object(s)')),
-                ('filter_logic', models.PositiveSmallIntegerField(choices=[(0, 'Disabled'), (1, 'Loose'), (2, 'Exact')], default=1, help_text='Loose matches any instance of a given string; exact matches the entire field.')),
+                ('report', models.CharField(max_length=255, unique=True)),
+                ('created', models.DateTimeField(auto_now_add=True)),
+                ('failed', models.BooleanField()),
+                ('data', django.contrib.postgres.fields.jsonb.JSONField()),
+                ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
             ],
             ],
             options={
             options={
-                'ordering': ['weight', 'name'],
+                'ordering': ['report'],
             },
             },
         ),
         ),
-        migrations.CreateModel(
-            name='CustomFieldChoice',
-            fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('value', models.CharField(max_length=100)),
-                ('weight', models.PositiveSmallIntegerField(default=100, help_text='Higher weights appear lower in the list')),
-                ('field', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='choices', to='extras.CustomField')),
-            ],
-            options={
-                'ordering': ['field', 'weight', 'value'],
-            },
+        migrations.AddField(
+            model_name='topologymap',
+            name='type',
+            field=models.PositiveSmallIntegerField(choices=[(1, 'Network'), (2, 'Console'), (3, 'Power')], default=1),
         ),
         ),
-        migrations.CreateModel(
-            name='CustomFieldValue',
-            fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('obj_id', models.PositiveIntegerField()),
-                ('serialized_value', models.CharField(max_length=255)),
-                ('field', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='values', to='extras.CustomField')),
-                ('obj_type', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.ContentType')),
-            ],
-            options={
-                'ordering': ['obj_type', 'obj_id'],
-            },
+        migrations.AddField(
+            model_name='customfield',
+            name='filter_logic',
+            field=models.PositiveSmallIntegerField(choices=[(0, 'Disabled'), (1, 'Loose'), (2, 'Exact')], default=1, help_text='Loose matches any instance of a given string; exact matches the entire field.'),
+        ),
+        migrations.AlterField(
+            model_name='customfield',
+            name='required',
+            field=models.BooleanField(default=False, help_text='If true, this field is required when creating new objects or editing an existing object.'),
         ),
         ),
-        migrations.AlterUniqueTogether(
-            name='customfieldvalue',
-            unique_together=set([('field', 'obj_type', 'obj_id')]),
+        migrations.AlterField(
+            model_name='customfield',
+            name='weight',
+            field=models.PositiveSmallIntegerField(default=100, help_text='Fields with higher weights appear lower in a form.'),
+        ),
+        migrations.RunPython(
+            code=is_filterable_to_filter_logic,
         ),
         ),
-        migrations.AlterUniqueTogether(
-            name='customfieldchoice',
-            unique_together=set([('field', 'value')]),
+        migrations.RemoveField(
+            model_name='customfield',
+            name='is_filterable',
+        ),
+        migrations.AlterField(
+            model_name='customfield',
+            name='obj_type',
+            field=models.ManyToManyField(help_text='The object(s) to which this field applies.', limit_choices_to={'model__in': ('provider', 'circuit', 'site', 'rack', 'devicetype', 'device', 'aggregate', 'prefix', 'ipaddress', 'vlan', 'vrf', 'tenant', 'cluster', 'virtualmachine')}, related_name='custom_fields', to='contenttypes.ContentType', verbose_name='Object(s)'),
+        ),
+        migrations.AlterField(
+            model_name='customfieldchoice',
+            name='field',
+            field=models.ForeignKey(limit_choices_to={'type': 600}, on_delete=django.db.models.deletion.CASCADE, related_name='choices', to='extras.CustomField'),
+        ),
+        migrations.AlterField(
+            model_name='exporttemplate',
+            name='content_type',
+            field=models.ForeignKey(limit_choices_to={'model__in': ['provider', 'circuit', 'site', 'region', 'rack', 'rackgroup', 'manufacturer', 'devicetype', 'device', 'consoleport', 'powerport', 'interfaceconnection', 'aggregate', 'prefix', 'ipaddress', 'vlan', 'tenant', 'cluster', 'virtualmachine']}, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType'),
         ),
         ),
         migrations.CreateModel(
         migrations.CreateModel(
-            name='ImageAttachment',
+            name='Webhook',
             fields=[
             fields=[
                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('object_id', models.PositiveIntegerField()),
-                ('image', models.ImageField(height_field='image_height', upload_to=extras.models.image_upload, width_field='image_width')),
-                ('image_height', models.PositiveSmallIntegerField()),
-                ('image_width', models.PositiveSmallIntegerField()),
-                ('name', models.CharField(blank=True, max_length=50)),
-                ('created', models.DateTimeField(auto_now_add=True)),
-                ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')),
+                ('name', models.CharField(max_length=150, unique=True)),
+                ('type_create', models.BooleanField(default=False, help_text='Call this webhook when a matching object is created.')),
+                ('type_update', models.BooleanField(default=False, help_text='Call this webhook when a matching object is updated.')),
+                ('type_delete', models.BooleanField(default=False, help_text='Call this webhook when a matching object is deleted.')),
+                ('payload_url', models.CharField(help_text='A POST will be sent to this URL when the webhook is called.', max_length=500, verbose_name='URL')),
+                ('http_content_type', models.PositiveSmallIntegerField(choices=[(1, 'application/json'), (2, 'application/x-www-form-urlencoded')], default=1, verbose_name='HTTP content type')),
+                ('secret', models.CharField(blank=True, help_text="When provided, the request will include a 'X-Hook-Signature' header containing a HMAC hex digest of the payload body using the secret as the key. The secret is not transmitted in the request.", max_length=255)),
+                ('enabled', models.BooleanField(default=True)),
+                ('ssl_verification', models.BooleanField(default=True, help_text='Enable SSL certificate verification. Disable with caution!', verbose_name='SSL verification')),
+                ('obj_type', models.ManyToManyField(help_text='The object(s) to which this Webhook applies.', related_name='webhooks', to='contenttypes.ContentType', verbose_name='Object types')),
             ],
             ],
             options={
             options={
-                'ordering': ['name'],
+                'unique_together': {('payload_url', 'type_create', 'type_update', 'type_delete')},
             },
             },
         ),
         ),
-        migrations.RunPython(
-            code=verify_postgresql_version,
-        ),
         migrations.CreateModel(
         migrations.CreateModel(
-            name='ReportResult',
+            name='ObjectChange',
             fields=[
             fields=[
                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('report', models.CharField(max_length=255, unique=True)),
-                ('created', models.DateTimeField(auto_now_add=True)),
-                ('failed', models.BooleanField()),
-                ('data', django.contrib.postgres.fields.jsonb.JSONField()),
-                ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
+                ('time', models.DateTimeField(auto_now_add=True)),
+                ('user_name', models.CharField(editable=False, max_length=150)),
+                ('request_id', models.UUIDField(editable=False)),
+                ('action', models.PositiveSmallIntegerField(choices=[(1, 'Created'), (2, 'Updated'), (3, 'Deleted')])),
+                ('changed_object_id', models.PositiveIntegerField()),
+                ('related_object_id', models.PositiveIntegerField(blank=True, null=True)),
+                ('object_repr', models.CharField(editable=False, max_length=200)),
+                ('object_data', django.contrib.postgres.fields.jsonb.JSONField(editable=False)),
+                ('changed_object_type', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.ContentType')),
+                ('related_object_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.ContentType')),
+                ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='changes', to=settings.AUTH_USER_MODEL)),
             ],
             ],
             options={
             options={
-                'ordering': ['report'],
+                'ordering': ['-time'],
             },
             },
         ),
         ),
     ]
     ]

+ 5 - 7
netbox/extras/migrations/0010_customfield_filter_logic.py

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

+ 106 - 0
netbox/extras/migrations/0014_configcontexts_squashed_0019_tag_taggeditem.py

@@ -0,0 +1,106 @@
+import django.contrib.postgres.fields.jsonb
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+def set_template_language(apps, schema_editor):
+    """
+    Set the language for all existing ExportTemplates to Django (Jinja2 is the default for new ExportTemplates).
+    """
+    ExportTemplate = apps.get_model('extras', 'ExportTemplate')
+    ExportTemplate.objects.update(template_language=10)
+
+
+class Migration(migrations.Migration):
+
+    replaces = [('extras', '0014_configcontexts'), ('extras', '0015_remove_useraction'), ('extras', '0016_exporttemplate_add_cable'), ('extras', '0017_exporttemplate_mime_type_length'), ('extras', '0018_exporttemplate_add_jinja2'), ('extras', '0019_tag_taggeditem')]
+
+    dependencies = [
+        ('extras', '0013_objectchange'),
+        ('tenancy', '0005_change_logging'),
+        ('dcim', '0061_platform_napalm_args'),
+        ('contenttypes', '0002_remove_content_type_name'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='ConfigContext',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('name', models.CharField(max_length=100, unique=True)),
+                ('weight', models.PositiveSmallIntegerField(default=1000)),
+                ('description', models.CharField(blank=True, max_length=100)),
+                ('is_active', models.BooleanField(default=True)),
+                ('data', django.contrib.postgres.fields.jsonb.JSONField()),
+                ('platforms', models.ManyToManyField(blank=True, related_name='_configcontext_platforms_+', to='dcim.Platform')),
+                ('regions', models.ManyToManyField(blank=True, related_name='_configcontext_regions_+', to='dcim.Region')),
+                ('roles', models.ManyToManyField(blank=True, related_name='_configcontext_roles_+', to='dcim.DeviceRole')),
+                ('sites', models.ManyToManyField(blank=True, related_name='_configcontext_sites_+', to='dcim.Site')),
+                ('tenant_groups', models.ManyToManyField(blank=True, related_name='_configcontext_tenant_groups_+', to='tenancy.TenantGroup')),
+                ('tenants', models.ManyToManyField(blank=True, related_name='_configcontext_tenants_+', to='tenancy.Tenant')),
+            ],
+            options={
+                'ordering': ['weight', 'name'],
+            },
+        ),
+        migrations.AlterField(
+            model_name='customfield',
+            name='obj_type',
+            field=models.ManyToManyField(help_text='The object(s) to which this field applies.', limit_choices_to={'model__in': ('provider', 'circuit', 'site', 'rack', 'devicetype', 'device', 'aggregate', 'prefix', 'ipaddress', 'vlan', 'vrf', 'service', 'secret', 'tenant', 'cluster', 'virtualmachine')}, related_name='custom_fields', to='contenttypes.ContentType', verbose_name='Object(s)'),
+        ),
+        migrations.AlterField(
+            model_name='exporttemplate',
+            name='content_type',
+            field=models.ForeignKey(limit_choices_to={'model__in': ['provider', 'circuit', 'site', 'region', 'rack', 'rackgroup', 'manufacturer', 'devicetype', 'device', 'consoleport', 'powerport', 'interfaceconnection', 'virtualchassis', 'aggregate', 'prefix', 'ipaddress', 'vlan', 'vrf', 'service', 'secret', 'tenant', 'cluster', 'virtualmachine']}, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType'),
+        ),
+        migrations.AlterField(
+            model_name='webhook',
+            name='obj_type',
+            field=models.ManyToManyField(help_text='The object(s) to which this Webhook applies.', limit_choices_to={'model__in': ('provider', 'circuit', 'site', 'rack', 'devicetype', 'device', 'virtualchassis', 'consoleport', 'consoleserverport', 'powerport', 'poweroutlet', 'interface', 'devicebay', 'inventoryitem', 'aggregate', 'prefix', 'ipaddress', 'vlan', 'vrf', 'service', 'secret', 'tenant', 'cluster', 'virtualmachine')}, related_name='webhooks', to='contenttypes.ContentType', verbose_name='Object types'),
+        ),
+        migrations.DeleteModel(
+            name='UserAction',
+        ),
+        migrations.AlterField(
+            model_name='exporttemplate',
+            name='content_type',
+            field=models.ForeignKey(limit_choices_to={'model__in': ['provider', 'circuit', 'site', 'region', 'rack', 'rackgroup', 'manufacturer', 'devicetype', 'device', 'consoleport', 'powerport', 'interface', 'cable', 'virtualchassis', 'aggregate', 'prefix', 'ipaddress', 'vlan', 'vrf', 'service', 'secret', 'tenant', 'cluster', 'virtualmachine']}, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType'),
+        ),
+        migrations.AlterField(
+            model_name='exporttemplate',
+            name='mime_type',
+            field=models.CharField(blank=True, max_length=50),
+        ),
+        migrations.AddField(
+            model_name='exporttemplate',
+            name='template_language',
+            field=models.PositiveSmallIntegerField(default=20),
+        ),
+        migrations.RunPython(
+            code=set_template_language,
+        ),
+        migrations.CreateModel(
+            name='Tag',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
+                ('name', models.CharField(max_length=100, unique=True)),
+                ('slug', models.SlugField(max_length=100, unique=True)),
+            ],
+            options={
+                'abstract': False,
+            },
+        ),
+        migrations.CreateModel(
+            name='TaggedItem',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
+                ('object_id', models.IntegerField(db_index=True)),
+                ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='extras_taggeditem_tagged_items', to='contenttypes.ContentType')),
+                ('tag', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='extras_taggeditem_items', to='extras.Tag')),
+            ],
+            options={
+                'abstract': False,
+                'index_together': {('content_type', 'object_id')},
+            },
+        ),
+    ]

+ 93 - 0
netbox/extras/migrations/0020_tag_data_squashed_0021_add_color_comments_changelog_to_tag.py

@@ -0,0 +1,93 @@
+from django.db import migrations, models
+
+import utilities.fields
+
+
+def copy_tags(apps, schema_editor):
+    """
+    Copy data from taggit_tag to extras_tag
+    """
+    TaggitTag = apps.get_model('taggit', 'Tag')
+    ExtrasTag = apps.get_model('extras', 'Tag')
+
+    tags_values = TaggitTag.objects.all().values('id', 'name', 'slug')
+    tags = [ExtrasTag(**tag) for tag in tags_values]
+    ExtrasTag.objects.bulk_create(tags)
+
+
+def copy_taggeditems(apps, schema_editor):
+    """
+    Copy data from taggit_taggeditem to extras_taggeditem
+    """
+    TaggitTaggedItem = apps.get_model('taggit', 'TaggedItem')
+    ExtrasTaggedItem = apps.get_model('extras', 'TaggedItem')
+
+    tagged_items_values = TaggitTaggedItem.objects.all().values('id', 'object_id', 'content_type_id', 'tag_id')
+    tagged_items = [ExtrasTaggedItem(**tagged_item) for tagged_item in tagged_items_values]
+    ExtrasTaggedItem.objects.bulk_create(tagged_items)
+
+
+def delete_taggit_taggeditems(apps, schema_editor):
+    """
+    Delete all TaggedItem instances from taggit_taggeditem
+    """
+    TaggitTaggedItem = apps.get_model('taggit', 'TaggedItem')
+    TaggitTaggedItem.objects.all().delete()
+
+
+def delete_taggit_tags(apps, schema_editor):
+    """
+    Delete all Tag instances from taggit_tag
+    """
+    TaggitTag = apps.get_model('taggit', 'Tag')
+    TaggitTag.objects.all().delete()
+
+
+class Migration(migrations.Migration):
+
+    replaces = [('extras', '0020_tag_data'), ('extras', '0021_add_color_comments_changelog_to_tag')]
+
+    dependencies = [
+        ('extras', '0019_tag_taggeditem'),
+        ('virtualization', '0009_custom_tag_models'),
+        ('tenancy', '0006_custom_tag_models'),
+        ('secrets', '0006_custom_tag_models'),
+        ('dcim', '0070_custom_tag_models'),
+        ('ipam', '0025_custom_tag_models'),
+        ('circuits', '0015_custom_tag_models'),
+    ]
+
+    operations = [
+        migrations.RunPython(
+            code=copy_tags,
+        ),
+        migrations.RunPython(
+            code=copy_taggeditems,
+        ),
+        migrations.RunPython(
+            code=delete_taggit_taggeditems,
+        ),
+        migrations.RunPython(
+            code=delete_taggit_tags,
+        ),
+        migrations.AddField(
+            model_name='tag',
+            name='color',
+            field=utilities.fields.ColorField(default='9e9e9e', max_length=6),
+        ),
+        migrations.AddField(
+            model_name='tag',
+            name='comments',
+            field=models.TextField(blank=True, default=''),
+        ),
+        migrations.AddField(
+            model_name='tag',
+            name='created',
+            field=models.DateField(auto_now_add=True, null=True),
+        ),
+        migrations.AddField(
+            model_name='tag',
+            name='last_updated',
+            field=models.DateTimeField(auto_now=True, null=True),
+        ),
+    ]

Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio