Merge branch 'develop' into develop-2.8

This commit is contained in:
Jeremy Stretch 2020-02-14 13:44:10 -05:00
commit 926b1fadf2
171 changed files with 6566 additions and 4511 deletions

View File

@ -5,7 +5,9 @@ about: Report a reproducible bug in the current release of NetBox
--- ---
<!-- <!--
NOTE: This form is only for reproducible bugs. If you need assistance with NOTE: IF YOUR ISSUE DOES NOT FOLLOW THIS TEMPLATE, IT WILL BE CLOSED.
This form is only for reproducible bugs. If you need assistance with
NetBox installation, or if you have a general question, DO NOT open an NetBox installation, or if you have a general question, DO NOT open an
issue. Instead, post to our mailing list: issue. Instead, post to our mailing list:
@ -16,8 +18,8 @@ about: Report a reproducible bug in the current release of NetBox
before submitting a bug report. before submitting a bug report.
--> -->
### Environment ### Environment
* Python version: <!-- Example: 3.5.4 --> * Python version: <!-- Example: 3.6.9 -->
* NetBox version: <!-- Example: 2.5.2 --> * NetBox version: <!-- Example: 2.7.3 -->
<!-- <!--
Describe in detail the exact steps that someone else can take to reproduce Describe in detail the exact steps that someone else can take to reproduce

View File

@ -5,6 +5,8 @@ about: Suggest an addition or modification to the NetBox documentation
--- ---
<!-- <!--
NOTE: IF YOUR ISSUE DOES NOT FOLLOW THIS TEMPLATE, IT WILL BE CLOSED.
Please indicate the nature of the change by placing an X in one of the Please indicate the nature of the change by placing an X in one of the
boxes below. boxes below.
--> -->
@ -14,5 +16,13 @@ about: Suggest an addition or modification to the NetBox documentation
[ ] Deprecation [ ] Deprecation
[ ] Cleanup (formatting, typos, etc.) [ ] Cleanup (formatting, typos, etc.)
### Area
[ ] Installation instructions
[ ] Configuration parameters
[ ] Functionality/features
[ ] REST API
[ ] Administration/development
[ ] Other
<!-- Describe the proposed change(s). --> <!-- Describe the proposed change(s). -->
### Proposed Changes ### Proposed Changes

View File

@ -5,7 +5,9 @@ about: Propose a new NetBox feature or enhancement
--- ---
<!-- <!--
NOTE: This form is only for proposing specific new features or enhancements. NOTE: IF YOUR ISSUE DOES NOT FOLLOW THIS TEMPLATE, IT WILL BE CLOSED.
This form is only for proposing specific new features or enhancements.
If you have a general idea or question, please post to our mailing list If you have a general idea or question, please post to our mailing list
instead of opening an issue: instead of opening an issue:
@ -19,8 +21,8 @@ about: Propose a new NetBox feature or enhancement
before submitting a bug report. before submitting a bug report.
--> -->
### Environment ### Environment
* Python version: <!-- Example: 3.5.4 --> * Python version: <!-- Example: 3.6.9 -->
* NetBox version: <!-- Example: 2.3.6 --> * NetBox version: <!-- Example: 2.7.3 -->
<!-- <!--
Describe in detail the new functionality you are proposing. Include any Describe in detail the new functionality you are proposing. Include any

View File

@ -1,14 +1,13 @@
--- ---
name: 🏡 Housekeeping name: 🏡 Housekeeping
about: A change pertaining to the codebase itself about: A change pertaining to the codebase itself (developers only)
--- ---
<!-- <!--
NOTE: This type of issue should be opened only by those reasonably familiar NOTE: This template is for use by maintainers only. Please do not submit
with NetBox's code base and interested in contributing to its development. an issue using this template unless you have been specifically asked to
do so.
Describe the proposed change(s) in detail.
--> -->
### Proposed Changes ### Proposed Changes

3
.github/stale.yml vendored
View File

@ -1,5 +1,8 @@
# Configuration for Stale (https://github.com/apps/stale) # Configuration for Stale (https://github.com/apps/stale)
# Pull requests are exempt from being marked as stale
only: issues
# Number of days of inactivity before an issue becomes stale # Number of days of inactivity before an issue becomes stale
daysUntilStale: 14 daysUntilStale: 14

View File

@ -22,6 +22,10 @@ django-filter
# https://github.com/django-mptt/django-mptt # https://github.com/django-mptt/django-mptt
django-mptt django-mptt
# Context managers for PostgreSQL advisory locks
# https://github.com/Xof/django-pglocks
django-pglocks
# 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

View File

@ -177,10 +177,11 @@ An IPv4 or IPv6 network with a mask. Returns a `netaddr.IPNetwork` object. Two a
All variables support the following default options: All variables support the following default options:
* `label` - The name of the form field
* `description` - A brief description of the field
* `default` - The field's default value * `default` - The field's default value
* `description` - A brief description of the field
* `label` - The name of the form field
* `required` - Indicates whether the field is mandatory (default: true) * `required` - Indicates whether the field is mandatory (default: true)
* `widget` - The class of form widget to use (see the [Django documentation](https://docs.djangoproject.com/en/stable/ref/forms/widgets/))
## Example ## Example

View File

@ -90,6 +90,14 @@ This setting enables debugging. This should be done only during development or t
--- ---
## DEVELOPER
Default: False
This parameter serves as a safeguard to prevent some potentially dangerous behavior, such as generating new database schema migrations. Set this to `True` **only** if you are actively developing the NetBox code base.
---
## EMAIL ## EMAIL
In order to send email, NetBox needs an email server configured. The following items can be defined within the `EMAIL` setting: In order to send email, NetBox needs an email server configured. The following items can be defined within the `EMAIL` setting:
@ -101,6 +109,20 @@ In order to send email, NetBox needs an email server configured. The following i
* TIMEOUT - Amount of time to wait for a connection (seconds) * TIMEOUT - Amount of time to wait for a connection (seconds)
* FROM_EMAIL - Sender address for emails sent by NetBox * FROM_EMAIL - Sender address for emails sent by NetBox
Email is sent from NetBox only for critical events. If you would like to test the email server configuration please use the django function [send_mail()](https://docs.djangoproject.com/en/stable/topics/email/#send-mail):
```
# python ./manage.py nbshell
>>> from django.core.mail import send_mail
>>> send_mail(
'Test Email Subject',
'Test Email Body',
'noreply-netbox@example.com',
['users@example.com'],
fail_silently=False
)
```
--- ---
## EXEMPT_VIEW_PERMISSIONS ## EXEMPT_VIEW_PERMISSIONS
@ -127,7 +149,7 @@ EXEMPT_VIEW_PERMISSIONS = ['*']
--- ---
# ENFORCE_GLOBAL_UNIQUE ## ENFORCE_GLOBAL_UNIQUE
Default: False Default: False

View File

@ -21,7 +21,7 @@ NetBox requires access to a PostgreSQL database service to store data. This serv
* `PASSWORD` - PostgreSQL password * `PASSWORD` - PostgreSQL password
* `HOST` - Name or IP address of the database server (use `localhost` if running locally) * `HOST` - Name or IP address of the database server (use `localhost` if running locally)
* `PORT` - TCP port of the PostgreSQL service; leave blank for default port (5432) * `PORT` - TCP port of the PostgreSQL service; leave blank for default port (5432)
* `CONN_MAX_AGE` - Number in seconds for Netbox to keep database connections open. 150-300 seconds is typically a good starting point ([more info](https://docs.djangoproject.com/en/stable/ref/databases/#persistent-connections)). * `CONN_MAX_AGE` - Lifetime of a [persistent database connection](https://docs.djangoproject.com/en/stable/ref/databases/#persistent-connections), in seconds (150-300 is recommended)
Example: Example:
@ -36,6 +36,9 @@ DATABASE = {
} }
``` ```
!!! note
NetBox supports all PostgreSQL database options supported by the underlying Django framework. For a complete list of available parameters, please see [the Django documentation](https://docs.djangoproject.com/en/stable/ref/settings/#databases).
--- ---
## REDIS ## REDIS
@ -77,14 +80,56 @@ REDIS = {
} }
``` ```
!!! note: !!! note
If you are upgrading from a version prior to v2.7, please note that the Redis connection configuration settings have 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 changed. Manual modification to bring the `REDIS` section inline with the above specification is necessary
!!! warning: !!! note
It is highly recommended to keep the webhook and cache databases separate. Using the same database number on the It is highly recommended to keep the webhook and cache databases 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. same Redis instance for both may result in webhook processing data being lost during cache flushing events.
### Using Redis Sentinel
If you are using [Redis Sentinel](https://redis.io/topics/sentinel) for high-availability purposes, there is minimal
configuration necessary to convert NetBox to recognize it. It requires the removal of the `HOST` and `PORT` keys from
above and the addition of two new keys.
* `SENTINELS`: List of tuples or tuple of tuples with each inner tuple containing the name or IP address
of the Redis server and port for each sentinel instance to connect to
* `SENTINEL_SERVICE`: Name of the master / service to connect to
Example:
```python
REDIS = {
'webhooks': {
'SENTINELS': [('mysentinel.redis.example.com', 6379)],
'SENTINEL_SERVICE': 'netbox',
'PASSWORD': '',
'DATABASE': 0,
'DEFAULT_TIMEOUT': 300,
'SSL': False,
},
'caching': {
'SENTINELS': [
('mysentinel.redis.example.com', 6379),
('othersentinel.redis.example.com', 6379)
],
'SENTINEL_SERVICE': 'netbox',
'PASSWORD': '',
'DATABASE': 1,
'DEFAULT_TIMEOUT': 300,
'SSL': False,
}
}
```
!!! note
It is possible to have only one or the other Redis configurations to use Sentinel functionality. It is possible
for example to have the webhook use sentinel via `HOST`/`PORT` and for caching to use Sentinel via
`SENTINELS`/`SENTINEL_SERVICE`.
--- ---
## SECRET_KEY ## SECRET_KEY

View File

@ -32,7 +32,7 @@ pycodestyle --ignore=W504,E501 netbox/
The introduction of a new dependency is best avoided unless it is absolutely necessary. For small features, it's generally preferable to replicate functionality within the NetBox code base rather than to introduce reliance on an external project. This reduces both the burden of tracking new releases and our exposure to outside bugs and attacks. The introduction of a new dependency is best avoided unless it is absolutely necessary. For small features, it's generally preferable to replicate functionality within the NetBox code base rather than to introduce reliance on an external project. This reduces both the burden of tracking new releases and our exposure to outside bugs and attacks.
If there's a strong case for introducing a new depdency, it must meet the following criteria: If there's a strong case for introducing a new dependency, it must meet the following criteria:
* Its complete source code must be published and freely accessible without registration. * Its complete source code must be published and freely accessible without registration.
* Its license must be conducive to inclusion in an open source project. * Its license must be conducive to inclusion in an open source project.
@ -45,10 +45,18 @@ When adding a new dependency, a short description of the package and the URL of
* When in doubt, remain consistent: It is better to be consistently incorrect than inconsistently correct. If you notice in the course of unrelated work a pattern that should be corrected, continue to follow the pattern for now and open a bug so that the entire code base can be evaluated at a later point. * When in doubt, remain consistent: It is better to be consistently incorrect than inconsistently correct. If you notice in the course of unrelated work a pattern that should be corrected, continue to follow the pattern for now and open a bug so that the entire code base can be evaluated at a later point.
* Prioritize readability over concision. Python is a very flexible language that typically gives us several options for expressing a given piece of logic, but some may be more friendly to the reader than others. (List comprehensions are particularly vulnerable to over-optimization.) Always remain considerate of the future reader who may need to interpret your code without the benefit of the context within which you are writing it.
* No easter eggs. While they can be fun, NetBox must be considered as a business-critical tool. The potential, however minor, for introducing a bug caused by unnecessary logic is best avoided entirely. * No easter eggs. While they can be fun, NetBox must be considered as a business-critical tool. The potential, however minor, for introducing a bug caused by unnecessary logic is best avoided entirely.
* Constants (variables which generally do not change) should be declared in `constants.py` within each app. Wildcard imports from the file are acceptable. * Constants (variables which generally do not change) should be declared in `constants.py` within each app. Wildcard imports from the file are acceptable.
* Every model should have a docstring. Every custom method should include an expalantion of its function. * Every model should have a docstring. Every custom method should include an explanation of its function.
* Nested API serializers generate minimal representations of an object. These are stored separately from the primary serializers to avoid circular dependencies. Always import nested serializers from other apps directly. For example, from within the DCIM app you would write `from ipam.api.nested_serializers import NestedIPAddressSerializer`. * Nested API serializers generate minimal representations of an object. These are stored separately from the primary serializers to avoid circular dependencies. Always import nested serializers from other apps directly. For example, from within the DCIM app you would write `from ipam.api.nested_serializers import NestedIPAddressSerializer`.
## Branding
* When referring to NetBox in writing, use the proper form "NetBox," with the letters N and B capitalized. The lowercase form "netbox" should be used in code, filenames, etc. But never "Netbox" or any other deviation.
* There is an SVG form of the NetBox logo at [docs/netbox_logo.svg](../netbox_logo.svg). It is preferred to use this logo for all purposes as it scales to arbitrary sizes without loss of resolution. If a raster image is required, the SVG logo should be converted to a PNG image of the prescribed size.

View File

@ -107,9 +107,10 @@ Install gunicorn:
# pip3 install gunicorn # pip3 install gunicorn
``` ```
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. Copy `/opt/netbox/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
# cd /opt/netbox
# cp contrib/gunicorn.py /opt/netbox/gunicorn.py # cp contrib/gunicorn.py /opt/netbox/gunicorn.py
``` ```

View File

@ -4,7 +4,7 @@ The following sections detail how to set up a new instance of NetBox:
1. [PostgreSQL database](1-postgresql.md) 1. [PostgreSQL database](1-postgresql.md)
2. [NetBox components](2-netbox.md) 2. [NetBox components](2-netbox.md)
3. [HTTP dameon](3-http-daemon.md) 3. [HTTP daemon](3-http-daemon.md)
4. [LDAP authentication](4-ldap.md) (optional) 4. [LDAP authentication](4-ldap.md) (optional)
# Upgrading # Upgrading

View File

@ -12,84 +12,19 @@ Migration is not required, as supervisord will still continue to function.
### systemd configuration: ### 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 We'll use systemd to control the daemonization of NetBox services. First, copy `contrib/netbox.service` and `contrib/netbox-rq.service` to the `/etc/systemd/system/` directory:
```no-highlight ```no-highlight
# cp contrib/netbox.service /etc/systemd/system/netbox.service # cp contrib/*.service /etc/systemd/system/
# 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`: !!! 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.
```no-highlight !!! note
/usr/local/bin/gunicorn --pid ${PidPath} --pythonpath ${WorkingDirectory}/netbox --config ${ConfigPath} netbox.wsgi You may need to modify the user that the systemd service runs as. Please verify the user for httpd on your specific release and edit both files to match your httpd service under user and group. The username could be "nobody", "nginx", "apache", "www-data" or any number of other usernames.
```
```no-highlight Then, start the `netbox` and `netbox-rq` services and enable them to initiate at boot time:
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 ```no-highlight
# systemctl daemon-reload # systemctl daemon-reload
@ -98,3 +33,25 @@ Finally, start the `netbox` and `netbox-rq` services and enable them to initiate
# systemctl enable netbox.service # systemctl enable netbox.service
# systemctl enable netbox-rq.service # systemctl enable netbox-rq.service
``` ```
You can use the command `systemctl status netbox` to verify that the WSGI service is running:
```
# 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 HTTP service at the server name or IP address you provided. If you are unable to connect, check that the nginx service is running and properly configured. If you receive a 502 (bad gateway) error, this indicates that gunicorn is misconfigured or not running.
!!! info
Please keep in mind that the configurations provided here are bare minimums required to get NetBox up and running. You may want to make adjustments to better suit your production environment.

View File

@ -88,7 +88,7 @@ Finally, restart the WSGI services to run the new code. If you followed this gui
```no-highlight ```no-highlight
# sudo systemctl restart netbox # sudo systemctl restart netbox
# sudo systemctl restart netbox-rqworker # sudo systemctl restart netbox-rq
``` ```
!!! note !!! note

View File

@ -1,9 +1,81 @@
# v2.7.4 (FUTURE) # v2.7.7 (FUTURE)
## Enhancements
* [#3840](https://github.com/netbox-community/netbox/issues/3840) - Enhance search function when selecting VLANs for interface assignment
* [#4170](https://github.com/netbox-community/netbox/issues/4170) - Improve color contrast in rack elevation drawings
## Bug Fixes ## Bug Fixes
* [#2519](https://github.com/netbox-community/netbox/issues/2519) - Avoid race condition when provisioning "next available" IPs/prefixes via the API
* [#4168](https://github.com/netbox-community/netbox/issues/4168) - Role is not required when creating a virtual machine
---
# v2.7.6 (2020-02-13)
## Bug Fixes
* [#4166](https://github.com/netbox-community/netbox/issues/4166) - Fix schema migrations to enforce maximum character length for naturalized fields
---
# v2.7.5 (2020-02-13)
**Note:** This release includes several database schema migrations that calculate and store copies of names for certain objects to improve natural ordering performance (see [#3799](https://github.com/netbox-community/netbox/issues/3799)). These migrations may take a few minutes to run if you have a very large number of objects defined in NetBox.
## Enhancements
* [#3766](https://github.com/netbox-community/netbox/issues/3766) - Allow custom script authors to specify the form widget for each variable
* [#3799](https://github.com/netbox-community/netbox/issues/3799) - Greatly improve performance when ordering device components
* [#3984](https://github.com/netbox-community/netbox/issues/3984) - Add support for Redis Sentinel
* [#3986](https://github.com/netbox-community/netbox/issues/3986) - Include position numbers in SVG image when rendering rack elevations
* [#4093](https://github.com/netbox-community/netbox/issues/4093) - Add more status choices for virtual machines
* [#4100](https://github.com/netbox-community/netbox/issues/4100) - Add device filter to component list views
* [#4113](https://github.com/netbox-community/netbox/issues/4113) - Add bulk edit functionality for device type components
* [#4116](https://github.com/netbox-community/netbox/issues/4116) - Enable bulk edit and delete functions for device component list views
* [#4129](https://github.com/netbox-community/netbox/issues/4129) - Add buttons to delete individual device type components
## Bug Fixes
* [#3507](https://github.com/netbox-community/netbox/issues/3507) - Fix filtering IP addresses by multiple devices
* [#3995](https://github.com/netbox-community/netbox/issues/3995) - Make dropdown menus in the navigation bar scrollable on small screens
* [#4083](https://github.com/netbox-community/netbox/issues/4083) - Permit nullifying applicable choice fields via API requests
* [#4089](https://github.com/netbox-community/netbox/issues/4089) - Selection of power outlet type during bulk update is optional
* [#4090](https://github.com/netbox-community/netbox/issues/4090) - Render URL custom fields as links under object view
* [#4091](https://github.com/netbox-community/netbox/issues/4091) - Fix filtering of objects by custom fields using UI search form
* [#4099](https://github.com/netbox-community/netbox/issues/4099) - Linkify interfaces on global interfaces list
* [#4108](https://github.com/netbox-community/netbox/issues/4108) - Avoid extraneous database queries when rendering search forms
* [#4134](https://github.com/netbox-community/netbox/issues/4134) - Device power ports and outlets should inherit type from the parent device type
* [#4137](https://github.com/netbox-community/netbox/issues/4137) - Disable occupied terminations when connecting a cable to a circuit
* [#4138](https://github.com/netbox-community/netbox/issues/4138) - Restore device bay counts in rack elevation diagrams
* [#4146](https://github.com/netbox-community/netbox/issues/4146) - Fix enforcement of secret role assignment for secret decryption
* [#4150](https://github.com/netbox-community/netbox/issues/4150) - Correct YAML rendering of config contexts
* [#4159](https://github.com/netbox-community/netbox/issues/4159) - Fix implementation of Redis caching configuration
---
# v2.7.4 (2020-02-04)
## Enhancements
* [#568](https://github.com/netbox-community/netbox/issues/568) - Allow custom fields to be imported and exported using CSV
* [#2921](https://github.com/netbox-community/netbox/issues/2921) - Replace tags filter with Select2 widget
* [#3313](https://github.com/netbox-community/netbox/issues/3313) - Toggle config context display between JSON and YAML
* [#3886](https://github.com/netbox-community/netbox/issues/3886) - Enable assigning config contexts by cluster and cluster group
* [#4051](https://github.com/netbox-community/netbox/issues/4051) - Disable the `makemigrations` management command
## Bug Fixes
* [#4030](https://github.com/netbox-community/netbox/issues/4030) - Fix exception when bulk editing interfaces (revised)
* [#4043](https://github.com/netbox-community/netbox/issues/4043) - Fix toggling of required fields in custom scripts * [#4043](https://github.com/netbox-community/netbox/issues/4043) - Fix toggling of required fields in custom scripts
* [#4049](https://github.com/netbox-community/netbox/issues/4049) - Restore missing `tags` field in IPAM service serializer * [#4049](https://github.com/netbox-community/netbox/issues/4049) - Restore missing `tags` field in IPAM service serializer
* [#4052](https://github.com/netbox-community/netbox/issues/4052) - Fix error when bulk importing interfaces to virtual machines
* [#4056](https://github.com/netbox-community/netbox/issues/4056) - Repair schema migration for Rack.outer_unit (from #3569)
* [#4067](https://github.com/netbox-community/netbox/issues/4067) - Correct permission checked when creating a rack (vs. editing)
* [#4071](https://github.com/netbox-community/netbox/issues/4071) - Enforce "view tag" permission on individual tag view
* [#4079](https://github.com/netbox-community/netbox/issues/4079) - Fix assignment of power panel when bulk editing power feeds
* [#4084](https://github.com/netbox-community/netbox/issues/4084) - Fix exception when creating an interface with tagged VLANs
--- ---

View File

@ -41,7 +41,6 @@ pages:
- Prometheus Metrics: 'additional-features/prometheus-metrics.md' - Prometheus Metrics: 'additional-features/prometheus-metrics.md'
- Reports: 'additional-features/reports.md' - Reports: 'additional-features/reports.md'
- Tags: 'additional-features/tags.md' - Tags: 'additional-features/tags.md'
- Topology Maps: 'additional-features/topology-maps.md'
- Webhooks: 'additional-features/webhooks.md' - Webhooks: 'additional-features/webhooks.md'
- Administration: - Administration:
- Replicating NetBox: 'administration/replicating-netbox.md' - Replicating NetBox: 'administration/replicating-netbox.md'

View File

@ -15,15 +15,15 @@ router = routers.DefaultRouter()
router.APIRootView = CircuitsRootView router.APIRootView = CircuitsRootView
# Field choices # Field choices
router.register(r'_choices', views.CircuitsFieldChoicesViewSet, basename='field-choice') router.register('_choices', views.CircuitsFieldChoicesViewSet, basename='field-choice')
# Providers # Providers
router.register(r'providers', views.ProviderViewSet) router.register('providers', views.ProviderViewSet)
# Circuits # Circuits
router.register(r'circuit-types', views.CircuitTypeViewSet) router.register('circuit-types', views.CircuitTypeViewSet)
router.register(r'circuits', views.CircuitViewSet) router.register('circuits', views.CircuitViewSet)
router.register(r'circuit-terminations', views.CircuitTerminationViewSet) router.register('circuit-terminations', views.CircuitTerminationViewSet)
app_name = 'circuits-api' app_name = 'circuits-api'
urlpatterns = router.urls urlpatterns = router.urls

View File

@ -2,12 +2,15 @@ from django import forms
from taggit.forms import TagField from taggit.forms import TagField
from dcim.models import Region, Site from dcim.models import Region, Site
from extras.forms import AddRemoveTagsForm, CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm from extras.forms import (
AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldFilterForm, CustomFieldModelForm, CustomFieldModelCSVForm,
)
from tenancy.forms import TenancyFilterForm, TenancyForm from tenancy.forms import TenancyFilterForm, TenancyForm
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.forms import ( from utilities.forms import (
APISelect, APISelectMultiple, add_blank_choice, BootstrapMixin, CommentField, CSVChoiceField, APISelect, APISelectMultiple, add_blank_choice, BootstrapMixin, CommentField, CSVChoiceField, DatePicker,
DatePicker, FilterChoiceField, SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple DynamicModelChoiceField, DynamicModelMultipleChoiceField, SmallTextarea, SlugField, StaticSelect2,
StaticSelect2Multiple, TagFilterField,
) )
from .choices import CircuitStatusChoices from .choices import CircuitStatusChoices
from .models import Circuit, CircuitTermination, CircuitType, Provider from .models import Circuit, CircuitTermination, CircuitType, Provider
@ -17,7 +20,7 @@ from .models import Circuit, CircuitTermination, CircuitType, Provider
# Providers # Providers
# #
class ProviderForm(BootstrapMixin, CustomFieldForm): class ProviderForm(BootstrapMixin, CustomFieldModelForm):
slug = SlugField() slug = SlugField()
comments = CommentField() comments = CommentField()
tags = TagField( tags = TagField(
@ -46,7 +49,7 @@ class ProviderForm(BootstrapMixin, CustomFieldForm):
} }
class ProviderCSVForm(forms.ModelForm): class ProviderCSVForm(CustomFieldModelCSVForm):
slug = SlugField() slug = SlugField()
class Meta: class Meta:
@ -105,7 +108,7 @@ class ProviderFilterForm(BootstrapMixin, CustomFieldFilterForm):
required=False, required=False,
label='Search' label='Search'
) )
region = FilterChoiceField( region = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(), queryset=Region.objects.all(),
to_field_name='slug', to_field_name='slug',
required=False, required=False,
@ -117,9 +120,10 @@ class ProviderFilterForm(BootstrapMixin, CustomFieldFilterForm):
} }
) )
) )
site = FilterChoiceField( site = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(), queryset=Site.objects.all(),
to_field_name='slug', to_field_name='slug',
required=False,
widget=APISelectMultiple( widget=APISelectMultiple(
api_url="/api/dcim/sites/", api_url="/api/dcim/sites/",
value_field="slug", value_field="slug",
@ -129,6 +133,7 @@ class ProviderFilterForm(BootstrapMixin, CustomFieldFilterForm):
required=False, required=False,
label='ASN' label='ASN'
) )
tag = TagFilterField(model)
# #
@ -160,7 +165,19 @@ class CircuitTypeCSVForm(forms.ModelForm):
# Circuits # Circuits
# #
class CircuitForm(BootstrapMixin, TenancyForm, CustomFieldForm): class CircuitForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
provider = DynamicModelChoiceField(
queryset=Provider.objects.all(),
widget=APISelect(
api_url="/api/circuits/providers/"
)
)
type = DynamicModelChoiceField(
queryset=CircuitType.objects.all(),
widget=APISelect(
api_url="/api/circuits/circuit-types/"
)
)
comments = CommentField() comments = CommentField()
tags = TagField( tags = TagField(
required=False required=False
@ -177,18 +194,12 @@ class CircuitForm(BootstrapMixin, TenancyForm, CustomFieldForm):
'commit_rate': "Committed rate", 'commit_rate': "Committed rate",
} }
widgets = { widgets = {
'provider': APISelect(
api_url="/api/circuits/providers/"
),
'type': APISelect(
api_url="/api/circuits/circuit-types/"
),
'status': StaticSelect2(), 'status': StaticSelect2(),
'install_date': DatePicker(), 'install_date': DatePicker(),
} }
class CircuitCSVForm(forms.ModelForm): class CircuitCSVForm(CustomFieldModelCSVForm):
provider = forms.ModelChoiceField( provider = forms.ModelChoiceField(
queryset=Provider.objects.all(), queryset=Provider.objects.all(),
to_field_name='name', to_field_name='name',
@ -232,14 +243,14 @@ class CircuitBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEdit
queryset=Circuit.objects.all(), queryset=Circuit.objects.all(),
widget=forms.MultipleHiddenInput widget=forms.MultipleHiddenInput
) )
type = forms.ModelChoiceField( type = DynamicModelChoiceField(
queryset=CircuitType.objects.all(), queryset=CircuitType.objects.all(),
required=False, required=False,
widget=APISelect( widget=APISelect(
api_url="/api/circuits/circuit-types/" api_url="/api/circuits/circuit-types/"
) )
) )
provider = forms.ModelChoiceField( provider = DynamicModelChoiceField(
queryset=Provider.objects.all(), queryset=Provider.objects.all(),
required=False, required=False,
widget=APISelect( widget=APISelect(
@ -252,7 +263,7 @@ class CircuitBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEdit
initial='', initial='',
widget=StaticSelect2() widget=StaticSelect2()
) )
tenant = forms.ModelChoiceField( tenant = DynamicModelChoiceField(
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
required=False, required=False,
widget=APISelect( widget=APISelect(
@ -287,17 +298,19 @@ class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm
required=False, required=False,
label='Search' label='Search'
) )
type = FilterChoiceField( type = DynamicModelMultipleChoiceField(
queryset=CircuitType.objects.all(), queryset=CircuitType.objects.all(),
to_field_name='slug', to_field_name='slug',
required=False,
widget=APISelectMultiple( widget=APISelectMultiple(
api_url="/api/circuits/circuit-types/", api_url="/api/circuits/circuit-types/",
value_field="slug", value_field="slug",
) )
) )
provider = FilterChoiceField( provider = DynamicModelMultipleChoiceField(
queryset=Provider.objects.all(), queryset=Provider.objects.all(),
to_field_name='slug', to_field_name='slug',
required=False,
widget=APISelectMultiple( widget=APISelectMultiple(
api_url="/api/circuits/providers/", api_url="/api/circuits/providers/",
value_field="slug", value_field="slug",
@ -308,7 +321,7 @@ class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm
required=False, required=False,
widget=StaticSelect2Multiple() widget=StaticSelect2Multiple()
) )
region = forms.ModelMultipleChoiceField( region = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(), queryset=Region.objects.all(),
to_field_name='slug', to_field_name='slug',
required=False, required=False,
@ -320,9 +333,10 @@ class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm
} }
) )
) )
site = FilterChoiceField( site = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(), queryset=Site.objects.all(),
to_field_name='slug', to_field_name='slug',
required=False,
widget=APISelectMultiple( widget=APISelectMultiple(
api_url="/api/dcim/sites/", api_url="/api/dcim/sites/",
value_field="slug", value_field="slug",
@ -333,6 +347,7 @@ class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm
min_value=0, min_value=0,
label='Commit rate (Kbps)' label='Commit rate (Kbps)'
) )
tag = TagFilterField(model)
# #

View File

@ -1,23 +1,15 @@
import urllib.parse import datetime
from django.test import Client, TestCase
from django.urls import reverse
from circuits.choices import *
from circuits.models import Circuit, CircuitType, Provider from circuits.models import Circuit, CircuitType, Provider
from utilities.testing import create_test_user from utilities.testing import ViewTestCases
class ProviderTestCase(TestCase): class ProviderTestCase(ViewTestCases.PrimaryObjectViewTestCase):
model = Provider
def setUp(self): @classmethod
user = create_test_user( def setUpTestData(cls):
permissions=[
'circuits.view_provider',
'circuits.add_provider',
]
)
self.client = Client()
self.client.force_login(user)
Provider.objects.bulk_create([ Provider.objects.bulk_create([
Provider(name='Provider 1', slug='provider-1', asn=65001), Provider(name='Provider 1', slug='provider-1', asn=65001),
@ -25,48 +17,40 @@ class ProviderTestCase(TestCase):
Provider(name='Provider 3', slug='provider-3', asn=65003), Provider(name='Provider 3', slug='provider-3', asn=65003),
]) ])
def test_provider_list(self): cls.form_data = {
'name': 'Provider X',
url = reverse('circuits:provider_list') 'slug': 'provider-x',
params = { 'asn': 65123,
"q": "test", 'account': '1234',
'portal_url': 'http://example.com/portal',
'noc_contact': 'noc@example.com',
'admin_contact': 'admin@example.com',
'comments': 'Another provider',
'tags': 'Alpha,Bravo,Charlie',
} }
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params))) cls.csv_data = (
self.assertEqual(response.status_code, 200)
def test_provider(self):
provider = Provider.objects.first()
response = self.client.get(provider.get_absolute_url())
self.assertEqual(response.status_code, 200)
def test_provider_import(self):
csv_data = (
"name,slug", "name,slug",
"Provider 4,provider-4", "Provider 4,provider-4",
"Provider 5,provider-5", "Provider 5,provider-5",
"Provider 6,provider-6", "Provider 6,provider-6",
) )
response = self.client.post(reverse('circuits:provider_import'), {'csv': '\n'.join(csv_data)}) cls.bulk_edit_data = {
'asn': 65009,
self.assertEqual(response.status_code, 200) 'account': '5678',
self.assertEqual(Provider.objects.count(), 6) 'portal_url': 'http://example.com/portal2',
'noc_contact': 'noc2@example.com',
'admin_contact': 'admin2@example.com',
'comments': 'New comments',
}
class CircuitTypeTestCase(TestCase): class CircuitTypeTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
model = CircuitType
def setUp(self): @classmethod
user = create_test_user( def setUpTestData(cls):
permissions=[
'circuits.view_circuittype',
'circuits.add_circuittype',
]
)
self.client = Client()
self.client.force_login(user)
CircuitType.objects.bulk_create([ CircuitType.objects.bulk_create([
CircuitType(name='Circuit Type 1', slug='circuit-type-1'), CircuitType(name='Circuit Type 1', slug='circuit-type-1'),
@ -74,79 +58,71 @@ class CircuitTypeTestCase(TestCase):
CircuitType(name='Circuit Type 3', slug='circuit-type-3'), CircuitType(name='Circuit Type 3', slug='circuit-type-3'),
]) ])
def test_circuittype_list(self): cls.form_data = {
'name': 'Circuit Type X',
'slug': 'circuit-type-x',
'description': 'A new circuit type',
}
url = reverse('circuits:circuittype_list') cls.csv_data = (
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
def test_circuittype_import(self):
csv_data = (
"name,slug", "name,slug",
"Circuit Type 4,circuit-type-4", "Circuit Type 4,circuit-type-4",
"Circuit Type 5,circuit-type-5", "Circuit Type 5,circuit-type-5",
"Circuit Type 6,circuit-type-6", "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) class CircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase):
self.assertEqual(CircuitType.objects.count(), 6) model = Circuit
@classmethod
def setUpTestData(cls):
class CircuitTestCase(TestCase): providers = (
Provider(name='Provider 1', slug='provider-1', asn=65001),
def setUp(self): Provider(name='Provider 2', slug='provider-2', asn=65002),
user = create_test_user(
permissions=[
'circuits.view_circuit',
'circuits.add_circuit',
]
) )
self.client = Client() Provider.objects.bulk_create(providers)
self.client.force_login(user)
provider = Provider(name='Provider 1', slug='provider-1', asn=65001) circuittypes = (
provider.save() CircuitType(name='Circuit Type 1', slug='circuit-type-1'),
CircuitType(name='Circuit Type 2', slug='circuit-type-2'),
circuittype = CircuitType(name='Circuit Type 1', slug='circuit-type-1') )
circuittype.save() CircuitType.objects.bulk_create(circuittypes)
Circuit.objects.bulk_create([ Circuit.objects.bulk_create([
Circuit(cid='Circuit 1', provider=provider, type=circuittype), Circuit(cid='Circuit 1', provider=providers[0], type=circuittypes[0]),
Circuit(cid='Circuit 2', provider=provider, type=circuittype), Circuit(cid='Circuit 2', provider=providers[0], type=circuittypes[0]),
Circuit(cid='Circuit 3', provider=provider, type=circuittype), Circuit(cid='Circuit 3', provider=providers[0], type=circuittypes[0]),
]) ])
def test_circuit_list(self): cls.form_data = {
'cid': 'Circuit X',
url = reverse('circuits:circuit_list') 'provider': providers[1].pk,
params = { 'type': circuittypes[1].pk,
"provider": Provider.objects.first().slug, 'status': CircuitStatusChoices.STATUS_DECOMMISSIONED,
"type": CircuitType.objects.first().slug, 'tenant': None,
'install_date': datetime.date(2020, 1, 1),
'commit_rate': 1000,
'description': 'A new circuit',
'comments': 'Some comments',
'tags': 'Alpha,Bravo,Charlie',
} }
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params))) cls.csv_data = (
self.assertEqual(response.status_code, 200)
def test_circuit(self):
circuit = Circuit.objects.first()
response = self.client.get(circuit.get_absolute_url())
self.assertEqual(response.status_code, 200)
def test_circuit_import(self):
csv_data = (
"cid,provider,type", "cid,provider,type",
"Circuit 4,Provider 1,Circuit Type 1", "Circuit 4,Provider 1,Circuit Type 1",
"Circuit 5,Provider 1,Circuit Type 1", "Circuit 5,Provider 1,Circuit Type 1",
"Circuit 6,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)}) cls.bulk_edit_data = {
'provider': providers[1].pk,
'type': circuittypes[1].pk,
'status': CircuitStatusChoices.STATUS_DECOMMISSIONED,
'tenant': None,
'commit_rate': 2000,
'description': 'New description',
'comments': 'New comments',
self.assertEqual(response.status_code, 200) }
self.assertEqual(Circuit.objects.count(), 6)

View File

@ -9,42 +9,42 @@ app_name = 'circuits'
urlpatterns = [ urlpatterns = [
# Providers # Providers
path(r'providers/', views.ProviderListView.as_view(), name='provider_list'), path('providers/', views.ProviderListView.as_view(), name='provider_list'),
path(r'providers/add/', views.ProviderCreateView.as_view(), name='provider_add'), path('providers/add/', views.ProviderCreateView.as_view(), name='provider_add'),
path(r'providers/import/', views.ProviderBulkImportView.as_view(), name='provider_import'), path('providers/import/', views.ProviderBulkImportView.as_view(), name='provider_import'),
path(r'providers/edit/', views.ProviderBulkEditView.as_view(), name='provider_bulk_edit'), path('providers/edit/', views.ProviderBulkEditView.as_view(), name='provider_bulk_edit'),
path(r'providers/delete/', views.ProviderBulkDeleteView.as_view(), name='provider_bulk_delete'), path('providers/delete/', views.ProviderBulkDeleteView.as_view(), name='provider_bulk_delete'),
path(r'providers/<slug:slug>/', views.ProviderView.as_view(), name='provider'), path('providers/<slug:slug>/', views.ProviderView.as_view(), name='provider'),
path(r'providers/<slug:slug>/edit/', views.ProviderEditView.as_view(), name='provider_edit'), path('providers/<slug:slug>/edit/', views.ProviderEditView.as_view(), name='provider_edit'),
path(r'providers/<slug:slug>/delete/', views.ProviderDeleteView.as_view(), name='provider_delete'), path('providers/<slug:slug>/delete/', views.ProviderDeleteView.as_view(), name='provider_delete'),
path(r'providers/<slug:slug>/changelog/', ObjectChangeLogView.as_view(), name='provider_changelog', kwargs={'model': Provider}), path('providers/<slug:slug>/changelog/', ObjectChangeLogView.as_view(), name='provider_changelog', kwargs={'model': Provider}),
# Circuit types # Circuit types
path(r'circuit-types/', views.CircuitTypeListView.as_view(), name='circuittype_list'), path('circuit-types/', views.CircuitTypeListView.as_view(), name='circuittype_list'),
path(r'circuit-types/add/', views.CircuitTypeCreateView.as_view(), name='circuittype_add'), path('circuit-types/add/', views.CircuitTypeCreateView.as_view(), name='circuittype_add'),
path(r'circuit-types/import/', views.CircuitTypeBulkImportView.as_view(), name='circuittype_import'), path('circuit-types/import/', views.CircuitTypeBulkImportView.as_view(), name='circuittype_import'),
path(r'circuit-types/delete/', views.CircuitTypeBulkDeleteView.as_view(), name='circuittype_bulk_delete'), path('circuit-types/delete/', views.CircuitTypeBulkDeleteView.as_view(), name='circuittype_bulk_delete'),
path(r'circuit-types/<slug:slug>/edit/', views.CircuitTypeEditView.as_view(), name='circuittype_edit'), path('circuit-types/<slug:slug>/edit/', views.CircuitTypeEditView.as_view(), name='circuittype_edit'),
path(r'circuit-types/<slug:slug>/changelog/', ObjectChangeLogView.as_view(), name='circuittype_changelog', kwargs={'model': CircuitType}), path('circuit-types/<slug:slug>/changelog/', ObjectChangeLogView.as_view(), name='circuittype_changelog', kwargs={'model': CircuitType}),
# Circuits # Circuits
path(r'circuits/', views.CircuitListView.as_view(), name='circuit_list'), path('circuits/', views.CircuitListView.as_view(), name='circuit_list'),
path(r'circuits/add/', views.CircuitCreateView.as_view(), name='circuit_add'), path('circuits/add/', views.CircuitCreateView.as_view(), name='circuit_add'),
path(r'circuits/import/', views.CircuitBulkImportView.as_view(), name='circuit_import'), path('circuits/import/', views.CircuitBulkImportView.as_view(), name='circuit_import'),
path(r'circuits/edit/', views.CircuitBulkEditView.as_view(), name='circuit_bulk_edit'), path('circuits/edit/', views.CircuitBulkEditView.as_view(), name='circuit_bulk_edit'),
path(r'circuits/delete/', views.CircuitBulkDeleteView.as_view(), name='circuit_bulk_delete'), path('circuits/delete/', views.CircuitBulkDeleteView.as_view(), name='circuit_bulk_delete'),
path(r'circuits/<int:pk>/', views.CircuitView.as_view(), name='circuit'), path('circuits/<int:pk>/', views.CircuitView.as_view(), name='circuit'),
path(r'circuits/<int:pk>/edit/', views.CircuitEditView.as_view(), name='circuit_edit'), path('circuits/<int:pk>/edit/', views.CircuitEditView.as_view(), name='circuit_edit'),
path(r'circuits/<int:pk>/delete/', views.CircuitDeleteView.as_view(), name='circuit_delete'), path('circuits/<int:pk>/delete/', views.CircuitDeleteView.as_view(), name='circuit_delete'),
path(r'circuits/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='circuit_changelog', kwargs={'model': Circuit}), path('circuits/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='circuit_changelog', kwargs={'model': Circuit}),
path(r'circuits/<int:pk>/terminations/swap/', views.circuit_terminations_swap, name='circuit_terminations_swap'), path('circuits/<int:pk>/terminations/swap/', views.circuit_terminations_swap, name='circuit_terminations_swap'),
# Circuit terminations # Circuit terminations
path(r'circuits/<int:circuit>/terminations/add/', views.CircuitTerminationCreateView.as_view(), name='circuittermination_add'), path('circuits/<int:circuit>/terminations/add/', views.CircuitTerminationCreateView.as_view(), name='circuittermination_add'),
path(r'circuit-terminations/<int:pk>/edit/', views.CircuitTerminationEditView.as_view(), name='circuittermination_edit'), path('circuit-terminations/<int:pk>/edit/', views.CircuitTerminationEditView.as_view(), name='circuittermination_edit'),
path(r'circuit-terminations/<int:pk>/delete/', views.CircuitTerminationDeleteView.as_view(), name='circuittermination_delete'), path('circuit-terminations/<int:pk>/delete/', views.CircuitTerminationDeleteView.as_view(), name='circuittermination_delete'),
path(r'circuit-terminations/<int:termination_a_id>/connect/<str:termination_b_type>/', CableCreateView.as_view(), name='circuittermination_connect', kwargs={'termination_a_type': CircuitTermination}), path('circuit-terminations/<int:termination_a_id>/connect/<str:termination_b_type>/', CableCreateView.as_view(), name='circuittermination_connect', kwargs={'termination_a_type': CircuitTermination}),
path(r'circuit-terminations/<int:pk>/trace/', CableTraceView.as_view(), name='circuittermination_trace', kwargs={'model': CircuitTermination}), path('circuit-terminations/<int:pk>/trace/', CableTraceView.as_view(), name='circuittermination_trace', kwargs={'model': CircuitTermination}),
] ]

View File

@ -29,7 +29,6 @@ class ProviderListView(PermissionRequiredMixin, ObjectListView):
filterset = filters.ProviderFilterSet filterset = filters.ProviderFilterSet
filterset_form = forms.ProviderFilterForm filterset_form = forms.ProviderFilterForm
table = tables.ProviderDetailTable table = tables.ProviderDetailTable
template_name = 'circuits/provider_list.html'
class ProviderView(PermissionRequiredMixin, View): class ProviderView(PermissionRequiredMixin, View):
@ -107,7 +106,6 @@ class CircuitTypeListView(PermissionRequiredMixin, ObjectListView):
permission_required = 'circuits.view_circuittype' permission_required = 'circuits.view_circuittype'
queryset = CircuitType.objects.annotate(circuit_count=Count('circuits')) queryset = CircuitType.objects.annotate(circuit_count=Count('circuits'))
table = tables.CircuitTypeTable table = tables.CircuitTypeTable
template_name = 'circuits/circuittype_list.html'
class CircuitTypeCreateView(PermissionRequiredMixin, ObjectEditView): class CircuitTypeCreateView(PermissionRequiredMixin, ObjectEditView):
@ -151,7 +149,6 @@ class CircuitListView(PermissionRequiredMixin, ObjectListView):
filterset = filters.CircuitFilterSet filterset = filters.CircuitFilterSet
filterset_form = forms.CircuitFilterForm filterset_form = forms.CircuitFilterForm
table = tables.CircuitTable table = tables.CircuitTable
template_name = 'circuits/circuit_list.html'
class CircuitView(PermissionRequiredMixin, View): class CircuitView(PermissionRequiredMixin, View):

View File

@ -117,9 +117,9 @@ class RackSerializer(TaggitSerializer, CustomFieldModelSerializer):
tenant = NestedTenantSerializer(required=False, allow_null=True) tenant = NestedTenantSerializer(required=False, allow_null=True)
status = ChoiceField(choices=RackStatusChoices, 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=RackTypeChoices, required=False, allow_null=True) type = ChoiceField(choices=RackTypeChoices, allow_blank=True, required=False)
width = ChoiceField(choices=RackWidthChoices, required=False) width = ChoiceField(choices=RackWidthChoices, required=False)
outer_unit = ChoiceField(choices=RackDimensionUnitChoices, required=False) outer_unit = ChoiceField(choices=RackDimensionUnitChoices, allow_blank=True, 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)
@ -212,7 +212,7 @@ class ManufacturerSerializer(ValidatedModelSerializer):
class DeviceTypeSerializer(TaggitSerializer, CustomFieldModelSerializer): class DeviceTypeSerializer(TaggitSerializer, CustomFieldModelSerializer):
manufacturer = NestedManufacturerSerializer() manufacturer = NestedManufacturerSerializer()
subdevice_role = ChoiceField(choices=SubdeviceRoleChoices, required=False, allow_null=True) subdevice_role = ChoiceField(choices=SubdeviceRoleChoices, allow_blank=True, required=False)
tags = TagListSerializerField(required=False) tags = TagListSerializerField(required=False)
device_count = serializers.IntegerField(read_only=True) device_count = serializers.IntegerField(read_only=True)
@ -228,6 +228,7 @@ class ConsolePortTemplateSerializer(ValidatedModelSerializer):
device_type = NestedDeviceTypeSerializer() device_type = NestedDeviceTypeSerializer()
type = ChoiceField( type = ChoiceField(
choices=ConsolePortTypeChoices, choices=ConsolePortTypeChoices,
allow_blank=True,
required=False required=False
) )
@ -240,6 +241,7 @@ class ConsoleServerPortTemplateSerializer(ValidatedModelSerializer):
device_type = NestedDeviceTypeSerializer() device_type = NestedDeviceTypeSerializer()
type = ChoiceField( type = ChoiceField(
choices=ConsolePortTypeChoices, choices=ConsolePortTypeChoices,
allow_blank=True,
required=False required=False
) )
@ -252,6 +254,7 @@ class PowerPortTemplateSerializer(ValidatedModelSerializer):
device_type = NestedDeviceTypeSerializer() device_type = NestedDeviceTypeSerializer()
type = ChoiceField( type = ChoiceField(
choices=PowerPortTypeChoices, choices=PowerPortTypeChoices,
allow_blank=True,
required=False required=False
) )
@ -264,6 +267,7 @@ class PowerOutletTemplateSerializer(ValidatedModelSerializer):
device_type = NestedDeviceTypeSerializer() device_type = NestedDeviceTypeSerializer()
type = ChoiceField( type = ChoiceField(
choices=PowerOutletTypeChoices, choices=PowerOutletTypeChoices,
allow_blank=True,
required=False required=False
) )
power_port = PowerPortTemplateSerializer( power_port = PowerPortTemplateSerializer(
@ -271,8 +275,8 @@ class PowerOutletTemplateSerializer(ValidatedModelSerializer):
) )
feed_leg = ChoiceField( feed_leg = ChoiceField(
choices=PowerOutletFeedLegChoices, choices=PowerOutletFeedLegChoices,
required=False, allow_blank=True,
allow_null=True required=False
) )
class Meta: class Meta:
@ -351,7 +355,7 @@ class DeviceSerializer(TaggitSerializer, CustomFieldModelSerializer):
platform = NestedPlatformSerializer(required=False, allow_null=True) platform = NestedPlatformSerializer(required=False, allow_null=True)
site = NestedSiteSerializer() site = NestedSiteSerializer()
rack = NestedRackSerializer(required=False, allow_null=True) rack = NestedRackSerializer(required=False, allow_null=True)
face = ChoiceField(choices=DeviceFaceChoices, required=False, allow_null=True) face = ChoiceField(choices=DeviceFaceChoices, allow_blank=True, required=False)
status = ChoiceField(choices=DeviceStatusChoices, required=False) 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)
@ -420,6 +424,7 @@ class ConsoleServerPortSerializer(TaggitSerializer, ConnectedEndpointSerializer)
device = NestedDeviceSerializer() device = NestedDeviceSerializer()
type = ChoiceField( type = ChoiceField(
choices=ConsolePortTypeChoices, choices=ConsolePortTypeChoices,
allow_blank=True,
required=False required=False
) )
cable = NestedCableSerializer(read_only=True) cable = NestedCableSerializer(read_only=True)
@ -437,6 +442,7 @@ class ConsolePortSerializer(TaggitSerializer, ConnectedEndpointSerializer):
device = NestedDeviceSerializer() device = NestedDeviceSerializer()
type = ChoiceField( type = ChoiceField(
choices=ConsolePortTypeChoices, choices=ConsolePortTypeChoices,
allow_blank=True,
required=False required=False
) )
cable = NestedCableSerializer(read_only=True) cable = NestedCableSerializer(read_only=True)
@ -454,6 +460,7 @@ class PowerOutletSerializer(TaggitSerializer, ConnectedEndpointSerializer):
device = NestedDeviceSerializer() device = NestedDeviceSerializer()
type = ChoiceField( type = ChoiceField(
choices=PowerOutletTypeChoices, choices=PowerOutletTypeChoices,
allow_blank=True,
required=False required=False
) )
power_port = NestedPowerPortSerializer( power_port = NestedPowerPortSerializer(
@ -461,8 +468,8 @@ class PowerOutletSerializer(TaggitSerializer, ConnectedEndpointSerializer):
) )
feed_leg = ChoiceField( feed_leg = ChoiceField(
choices=PowerOutletFeedLegChoices, choices=PowerOutletFeedLegChoices,
required=False, allow_blank=True,
allow_null=True required=False
) )
cable = NestedCableSerializer( cable = NestedCableSerializer(
read_only=True read_only=True
@ -483,6 +490,7 @@ class PowerPortSerializer(TaggitSerializer, ConnectedEndpointSerializer):
device = NestedDeviceSerializer() device = NestedDeviceSerializer()
type = ChoiceField( type = ChoiceField(
choices=PowerPortTypeChoices, choices=PowerPortTypeChoices,
allow_blank=True,
required=False required=False
) )
cable = NestedCableSerializer(read_only=True) cable = NestedCableSerializer(read_only=True)
@ -500,7 +508,7 @@ class InterfaceSerializer(TaggitSerializer, ConnectedEndpointSerializer):
device = NestedDeviceSerializer() device = NestedDeviceSerializer()
type = ChoiceField(choices=InterfaceTypeChoices, 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=InterfaceModeChoices, required=False, allow_null=True) mode = ChoiceField(choices=InterfaceModeChoices, allow_blank=True, required=False)
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(),
@ -617,7 +625,7 @@ class CableSerializer(ValidatedModelSerializer):
termination_a = serializers.SerializerMethodField(read_only=True) termination_a = serializers.SerializerMethodField(read_only=True)
termination_b = serializers.SerializerMethodField(read_only=True) termination_b = serializers.SerializerMethodField(read_only=True)
status = ChoiceField(choices=CableStatusChoices, required=False) status = ChoiceField(choices=CableStatusChoices, required=False)
length_unit = ChoiceField(choices=CableLengthUnitChoices, required=False, allow_null=True) length_unit = ChoiceField(choices=CableLengthUnitChoices, allow_blank=True, required=False)
class Meta: class Meta:
model = Cable model = Cable

View File

@ -15,65 +15,65 @@ router = routers.DefaultRouter()
router.APIRootView = DCIMRootView router.APIRootView = DCIMRootView
# Field choices # Field choices
router.register(r'_choices', views.DCIMFieldChoicesViewSet, basename='field-choice') router.register('_choices', views.DCIMFieldChoicesViewSet, basename='field-choice')
# Sites # Sites
router.register(r'regions', views.RegionViewSet) router.register('regions', views.RegionViewSet)
router.register(r'sites', views.SiteViewSet) router.register('sites', views.SiteViewSet)
# Racks # Racks
router.register(r'rack-groups', views.RackGroupViewSet) router.register('rack-groups', views.RackGroupViewSet)
router.register(r'rack-roles', views.RackRoleViewSet) router.register('rack-roles', views.RackRoleViewSet)
router.register(r'racks', views.RackViewSet) router.register('racks', views.RackViewSet)
router.register(r'rack-reservations', views.RackReservationViewSet) router.register('rack-reservations', views.RackReservationViewSet)
# Device types # Device types
router.register(r'manufacturers', views.ManufacturerViewSet) router.register('manufacturers', views.ManufacturerViewSet)
router.register(r'device-types', views.DeviceTypeViewSet) router.register('device-types', views.DeviceTypeViewSet)
# Device type components # Device type components
router.register(r'console-port-templates', views.ConsolePortTemplateViewSet) router.register('console-port-templates', views.ConsolePortTemplateViewSet)
router.register(r'console-server-port-templates', views.ConsoleServerPortTemplateViewSet) router.register('console-server-port-templates', views.ConsoleServerPortTemplateViewSet)
router.register(r'power-port-templates', views.PowerPortTemplateViewSet) router.register('power-port-templates', views.PowerPortTemplateViewSet)
router.register(r'power-outlet-templates', views.PowerOutletTemplateViewSet) router.register('power-outlet-templates', views.PowerOutletTemplateViewSet)
router.register(r'interface-templates', views.InterfaceTemplateViewSet) router.register('interface-templates', views.InterfaceTemplateViewSet)
router.register(r'front-port-templates', views.FrontPortTemplateViewSet) router.register('front-port-templates', views.FrontPortTemplateViewSet)
router.register(r'rear-port-templates', views.RearPortTemplateViewSet) router.register('rear-port-templates', views.RearPortTemplateViewSet)
router.register(r'device-bay-templates', views.DeviceBayTemplateViewSet) router.register('device-bay-templates', views.DeviceBayTemplateViewSet)
# Devices # Devices
router.register(r'device-roles', views.DeviceRoleViewSet) router.register('device-roles', views.DeviceRoleViewSet)
router.register(r'platforms', views.PlatformViewSet) router.register('platforms', views.PlatformViewSet)
router.register(r'devices', views.DeviceViewSet) router.register('devices', views.DeviceViewSet)
# Device components # Device components
router.register(r'console-ports', views.ConsolePortViewSet) router.register('console-ports', views.ConsolePortViewSet)
router.register(r'console-server-ports', views.ConsoleServerPortViewSet) router.register('console-server-ports', views.ConsoleServerPortViewSet)
router.register(r'power-ports', views.PowerPortViewSet) router.register('power-ports', views.PowerPortViewSet)
router.register(r'power-outlets', views.PowerOutletViewSet) router.register('power-outlets', views.PowerOutletViewSet)
router.register(r'interfaces', views.InterfaceViewSet) router.register('interfaces', views.InterfaceViewSet)
router.register(r'front-ports', views.FrontPortViewSet) router.register('front-ports', views.FrontPortViewSet)
router.register(r'rear-ports', views.RearPortViewSet) router.register('rear-ports', views.RearPortViewSet)
router.register(r'device-bays', views.DeviceBayViewSet) router.register('device-bays', views.DeviceBayViewSet)
router.register(r'inventory-items', views.InventoryItemViewSet) router.register('inventory-items', views.InventoryItemViewSet)
# Connections # Connections
router.register(r'console-connections', views.ConsoleConnectionViewSet, basename='consoleconnections') router.register('console-connections', views.ConsoleConnectionViewSet, basename='consoleconnections')
router.register(r'power-connections', views.PowerConnectionViewSet, basename='powerconnections') router.register('power-connections', views.PowerConnectionViewSet, basename='powerconnections')
router.register(r'interface-connections', views.InterfaceConnectionViewSet, basename='interfaceconnections') router.register('interface-connections', views.InterfaceConnectionViewSet, basename='interfaceconnections')
# Cables # Cables
router.register(r'cables', views.CableViewSet) router.register('cables', views.CableViewSet)
# Virtual chassis # Virtual chassis
router.register(r'virtual-chassis', views.VirtualChassisViewSet) router.register('virtual-chassis', views.VirtualChassisViewSet)
# Power # Power
router.register(r'power-panels', views.PowerPanelViewSet) router.register('power-panels', views.PowerPanelViewSet)
router.register(r'power-feeds', views.PowerFeedViewSet) router.register('power-feeds', views.PowerFeedViewSet)
# Miscellaneous # Miscellaneous
router.register(r'connected-device', views.ConnectedDeviceViewSet, basename='connected-device') router.register('connected-device', views.ConnectedDeviceViewSet, basename='connected-device')
app_name = 'dcim-api' app_name = 'dcim-api'
urlpatterns = router.urls urlpatterns = router.urls

View File

@ -9,6 +9,8 @@ from .choices import InterfaceTypeChoices
RACK_U_HEIGHT_DEFAULT = 42 RACK_U_HEIGHT_DEFAULT = 42
RACK_ELEVATION_LEGEND_WIDTH_DEFAULT = 30
RACK_ELEVATION_UNIT_WIDTH_DEFAULT = 230 RACK_ELEVATION_UNIT_WIDTH_DEFAULT = 230
RACK_ELEVATION_UNIT_HEIGHT_DEFAULT = 20 RACK_ELEVATION_UNIT_HEIGHT_DEFAULT = 20

File diff suppressed because it is too large Load Diff

View File

@ -1,18 +1,7 @@
from django.db.models import Manager, QuerySet from django.db.models import Manager, QuerySet
from django.db.models.expressions import RawSQL
from .constants import NONCONNECTABLE_IFACE_TYPES from .constants import NONCONNECTABLE_IFACE_TYPES
# Regular expressions for parsing Interface names
TYPE_RE = r"SUBSTRING({} FROM '^([^0-9\.:]+)')"
SLOT_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(\d{{1,9}})/') AS integer), NULL)"
SUBSLOT_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9\.:]+)?\d{{1,9}}/(\d{{1,9}})') AS integer), NULL)"
POSITION_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(?:\d{{1,9}}/){{2}}(\d{{1,9}})') AS integer), NULL)"
SUBPOSITION_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(?:\d{{1,9}}/){{3}}(\d{{1,9}})') AS integer), NULL)"
ID_RE = r"CAST(SUBSTRING({} FROM '^(?:[^0-9\.:]+)?(\d{{1,9}})([^/]|$)') AS integer)"
CHANNEL_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^.*:(\d{{1,9}})(\.\d{{1,9}})?$') AS integer), 0)"
VC_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^.*\.(\d{{1,9}})$') AS integer), 0)"
class InterfaceQuerySet(QuerySet): class InterfaceQuerySet(QuerySet):
@ -27,47 +16,4 @@ class InterfaceQuerySet(QuerySet):
class InterfaceManager(Manager): class InterfaceManager(Manager):
def get_queryset(self): def get_queryset(self):
""" return InterfaceQuerySet(self.model, using=self._db)
Naturally order interfaces by their type and numeric position. To order interfaces naturally, the `name` field
is split into eight distinct components: leading text (type), slot, subslot, position, subposition, ID, channel,
and virtual circuit:
{type}{slot or ID}/{subslot}/{position}/{subposition}:{channel}.{vc}
Components absent from the interface name are coalesced to zero or null. For example, an interface named
GigabitEthernet1/2/3 would be parsed as follows:
type = 'GigabitEthernet'
slot = 1
subslot = 2
position = 3
subposition = None
id = None
channel = 0
vc = 0
The original `name` field is considered in its entirety to serve as a fallback in the event interfaces do not
match any of the prescribed fields.
The `id` field is included to enforce deterministic ordering of interfaces in similar vein of other device
components.
"""
sql_col = '{}.name'.format(self.model._meta.db_table)
ordering = [
'_slot', '_subslot', '_position', '_subposition', '_type', '_id', '_channel', '_vc', 'name', 'pk'
]
fields = {
'_type': RawSQL(TYPE_RE.format(sql_col), []),
'_id': RawSQL(ID_RE.format(sql_col), []),
'_slot': RawSQL(SLOT_RE.format(sql_col), []),
'_subslot': RawSQL(SUBSLOT_RE.format(sql_col), []),
'_position': RawSQL(POSITION_RE.format(sql_col), []),
'_subposition': RawSQL(SUBPOSITION_RE.format(sql_col), []),
'_channel': RawSQL(CHANNEL_RE.format(sql_col), []),
'_vc': RawSQL(VC_RE.format(sql_col), []),
}
return InterfaceQuerySet(self.model, using=self._db).annotate(**fields).order_by(*ordering)

View File

@ -37,7 +37,7 @@ def rack_status_to_slug(apps, schema_editor):
def rack_outer_unit_to_slug(apps, schema_editor): def rack_outer_unit_to_slug(apps, schema_editor):
Rack = apps.get_model('dcim', 'Rack') Rack = apps.get_model('dcim', 'Rack')
for id, slug in RACK_DIMENSION_CHOICES: for id, slug in RACK_DIMENSION_CHOICES:
Rack.objects.filter(status=str(id)).update(status=slug) Rack.objects.filter(outer_unit=str(id)).update(outer_unit=slug)
class Migration(migrations.Migration): class Migration(migrations.Migration):

View File

@ -0,0 +1,27 @@
from django.db import migrations
RACK_DIMENSION_CHOICES = (
(1000, 'mm'),
(2000, 'in'),
)
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(outer_unit=str(id)).update(outer_unit=slug)
class Migration(migrations.Migration):
dependencies = [
('dcim', '0091_interface_type_other'),
]
operations = [
# Fixes a missed field migration from #3569; see bug #4056. The original migration has also been fixed,
# so this can be omitted when squashing in the future.
migrations.RunPython(
code=rack_outer_unit_to_slug
),
]

View File

@ -0,0 +1,147 @@
from django.db import migrations
import utilities.fields
import utilities.ordering
def _update_model_names(model):
# Update each unique field value in bulk
for name in model.objects.values_list('name', flat=True).order_by('name').distinct():
model.objects.filter(name=name).update(_name=utilities.ordering.naturalize(name, max_length=100))
def naturalize_consoleports(apps, schema_editor):
_update_model_names(apps.get_model('dcim', 'ConsolePort'))
def naturalize_consoleserverports(apps, schema_editor):
_update_model_names(apps.get_model('dcim', 'ConsoleServerPort'))
def naturalize_powerports(apps, schema_editor):
_update_model_names(apps.get_model('dcim', 'PowerPort'))
def naturalize_poweroutlets(apps, schema_editor):
_update_model_names(apps.get_model('dcim', 'PowerOutlet'))
def naturalize_frontports(apps, schema_editor):
_update_model_names(apps.get_model('dcim', 'FrontPort'))
def naturalize_rearports(apps, schema_editor):
_update_model_names(apps.get_model('dcim', 'RearPort'))
def naturalize_devicebays(apps, schema_editor):
_update_model_names(apps.get_model('dcim', 'DeviceBay'))
class Migration(migrations.Migration):
dependencies = [
('dcim', '0092_fix_rack_outer_unit'),
]
operations = [
migrations.AlterModelOptions(
name='consoleport',
options={'ordering': ('device', '_name')},
),
migrations.AlterModelOptions(
name='consoleserverport',
options={'ordering': ('device', '_name')},
),
migrations.AlterModelOptions(
name='devicebay',
options={'ordering': ('device', '_name')},
),
migrations.AlterModelOptions(
name='frontport',
options={'ordering': ('device', '_name')},
),
migrations.AlterModelOptions(
name='inventoryitem',
options={'ordering': ('device__id', 'parent__id', '_name')},
),
migrations.AlterModelOptions(
name='poweroutlet',
options={'ordering': ('device', '_name')},
),
migrations.AlterModelOptions(
name='powerport',
options={'ordering': ('device', '_name')},
),
migrations.AlterModelOptions(
name='rearport',
options={'ordering': ('device', '_name')},
),
migrations.AddField(
model_name='consoleport',
name='_name',
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
),
migrations.AddField(
model_name='consoleserverport',
name='_name',
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
),
migrations.AddField(
model_name='devicebay',
name='_name',
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
),
migrations.AddField(
model_name='frontport',
name='_name',
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
),
migrations.AddField(
model_name='inventoryitem',
name='_name',
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
),
migrations.AddField(
model_name='poweroutlet',
name='_name',
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
),
migrations.AddField(
model_name='powerport',
name='_name',
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
),
migrations.AddField(
model_name='rearport',
name='_name',
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
),
migrations.RunPython(
code=naturalize_consoleports,
reverse_code=migrations.RunPython.noop
),
migrations.RunPython(
code=naturalize_consoleserverports,
reverse_code=migrations.RunPython.noop
),
migrations.RunPython(
code=naturalize_powerports,
reverse_code=migrations.RunPython.noop
),
migrations.RunPython(
code=naturalize_poweroutlets,
reverse_code=migrations.RunPython.noop
),
migrations.RunPython(
code=naturalize_frontports,
reverse_code=migrations.RunPython.noop
),
migrations.RunPython(
code=naturalize_rearports,
reverse_code=migrations.RunPython.noop
),
migrations.RunPython(
code=naturalize_devicebays,
reverse_code=migrations.RunPython.noop
),
]

View File

@ -0,0 +1,138 @@
from django.db import migrations
import utilities.fields
import utilities.ordering
def _update_model_names(model):
# Update each unique field value in bulk
for name in model.objects.values_list('name', flat=True).order_by('name').distinct():
model.objects.filter(name=name).update(_name=utilities.ordering.naturalize(name, max_length=100))
def naturalize_consoleporttemplates(apps, schema_editor):
_update_model_names(apps.get_model('dcim', 'ConsolePortTemplate'))
def naturalize_consoleserverporttemplates(apps, schema_editor):
_update_model_names(apps.get_model('dcim', 'ConsoleServerPortTemplate'))
def naturalize_powerporttemplates(apps, schema_editor):
_update_model_names(apps.get_model('dcim', 'PowerPortTemplate'))
def naturalize_poweroutlettemplates(apps, schema_editor):
_update_model_names(apps.get_model('dcim', 'PowerOutletTemplate'))
def naturalize_frontporttemplates(apps, schema_editor):
_update_model_names(apps.get_model('dcim', 'FrontPortTemplate'))
def naturalize_rearporttemplates(apps, schema_editor):
_update_model_names(apps.get_model('dcim', 'RearPortTemplate'))
def naturalize_devicebaytemplates(apps, schema_editor):
_update_model_names(apps.get_model('dcim', 'DeviceBayTemplate'))
class Migration(migrations.Migration):
dependencies = [
('dcim', '0093_device_component_ordering'),
]
operations = [
migrations.AlterModelOptions(
name='consoleporttemplate',
options={'ordering': ('device_type', '_name')},
),
migrations.AlterModelOptions(
name='consoleserverporttemplate',
options={'ordering': ('device_type', '_name')},
),
migrations.AlterModelOptions(
name='devicebaytemplate',
options={'ordering': ('device_type', '_name')},
),
migrations.AlterModelOptions(
name='frontporttemplate',
options={'ordering': ('device_type', '_name')},
),
migrations.AlterModelOptions(
name='poweroutlettemplate',
options={'ordering': ('device_type', '_name')},
),
migrations.AlterModelOptions(
name='powerporttemplate',
options={'ordering': ('device_type', '_name')},
),
migrations.AlterModelOptions(
name='rearporttemplate',
options={'ordering': ('device_type', '_name')},
),
migrations.AddField(
model_name='consoleporttemplate',
name='_name',
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
),
migrations.AddField(
model_name='consoleserverporttemplate',
name='_name',
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
),
migrations.AddField(
model_name='devicebaytemplate',
name='_name',
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
),
migrations.AddField(
model_name='frontporttemplate',
name='_name',
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
),
migrations.AddField(
model_name='poweroutlettemplate',
name='_name',
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
),
migrations.AddField(
model_name='powerporttemplate',
name='_name',
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
),
migrations.AddField(
model_name='rearporttemplate',
name='_name',
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
),
migrations.RunPython(
code=naturalize_consoleporttemplates,
reverse_code=migrations.RunPython.noop
),
migrations.RunPython(
code=naturalize_consoleserverporttemplates,
reverse_code=migrations.RunPython.noop
),
migrations.RunPython(
code=naturalize_powerporttemplates,
reverse_code=migrations.RunPython.noop
),
migrations.RunPython(
code=naturalize_poweroutlettemplates,
reverse_code=migrations.RunPython.noop
),
migrations.RunPython(
code=naturalize_frontporttemplates,
reverse_code=migrations.RunPython.noop
),
migrations.RunPython(
code=naturalize_rearporttemplates,
reverse_code=migrations.RunPython.noop
),
migrations.RunPython(
code=naturalize_devicebaytemplates,
reverse_code=migrations.RunPython.noop
),
]

View File

@ -0,0 +1,70 @@
from django.db import migrations
import utilities.fields
import utilities.ordering
def _update_model_names(model):
# Update each unique field value in bulk
for name in model.objects.values_list('name', flat=True).order_by('name').distinct():
model.objects.filter(name=name).update(_name=utilities.ordering.naturalize(name, max_length=100))
def naturalize_sites(apps, schema_editor):
_update_model_names(apps.get_model('dcim', 'Site'))
def naturalize_racks(apps, schema_editor):
_update_model_names(apps.get_model('dcim', 'Rack'))
def naturalize_devices(apps, schema_editor):
_update_model_names(apps.get_model('dcim', 'Device'))
class Migration(migrations.Migration):
dependencies = [
('dcim', '0094_device_component_template_ordering'),
]
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')},
),
migrations.AlterModelOptions(
name='site',
options={'ordering': ('_name',)},
),
migrations.AddField(
model_name='device',
name='_name',
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize, null=True),
),
migrations.AddField(
model_name='rack',
name='_name',
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
),
migrations.AddField(
model_name='site',
name='_name',
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
),
migrations.RunPython(
code=naturalize_sites,
reverse_code=migrations.RunPython.noop
),
migrations.RunPython(
code=naturalize_racks,
reverse_code=migrations.RunPython.noop
),
migrations.RunPython(
code=naturalize_devices,
reverse_code=migrations.RunPython.noop
),
]

View File

@ -0,0 +1,53 @@
from django.db import migrations
import utilities.fields
import utilities.ordering
def _update_model_names(model):
# Update each unique field value in bulk
for name in model.objects.values_list('name', flat=True).order_by('name').distinct():
model.objects.filter(name=name).update(_name=utilities.ordering.naturalize_interface(name, max_length=100))
def naturalize_interfacetemplates(apps, schema_editor):
_update_model_names(apps.get_model('dcim', 'InterfaceTemplate'))
def naturalize_interfaces(apps, schema_editor):
_update_model_names(apps.get_model('dcim', 'Interface'))
class Migration(migrations.Migration):
dependencies = [
('dcim', '0095_primary_model_ordering'),
]
operations = [
migrations.AlterModelOptions(
name='interface',
options={'ordering': ('device', '_name')},
),
migrations.AlterModelOptions(
name='interfacetemplate',
options={'ordering': ('device_type', '_name')},
),
migrations.AddField(
model_name='interface',
name='_name',
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize_interface),
),
migrations.AddField(
model_name='interfacetemplate',
name='_name',
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize_interface),
),
migrations.RunPython(
code=naturalize_interfacetemplates,
reverse_code=migrations.RunPython.noop
),
migrations.RunPython(
code=naturalize_interfaces,
reverse_code=migrations.RunPython.noop
),
]

View File

@ -22,8 +22,7 @@ from dcim.choices import *
from dcim.constants import * from dcim.constants import *
from dcim.fields import ASNField from dcim.fields import ASNField
from extras.models import ConfigContextModel, CustomFieldModel, TaggedItem from extras.models import ConfigContextModel, CustomFieldModel, TaggedItem
from utilities.fields import ColorField from utilities.fields import ColorField, NaturalOrderingField
from utilities.managers import NaturalOrderingManager
from utilities.models import ChangeLoggedModel from utilities.models import ChangeLoggedModel
from utilities.utils import foreground_color, to_meters from utilities.utils import foreground_color, to_meters
from .device_component_templates import ( from .device_component_templates import (
@ -134,6 +133,11 @@ class Site(ChangeLoggedModel, CustomFieldModel):
max_length=50, max_length=50,
unique=True unique=True
) )
_name = NaturalOrderingField(
target_field='name',
max_length=100,
blank=True
)
slug = models.SlugField( slug = models.SlugField(
unique=True unique=True
) )
@ -215,8 +219,6 @@ class Site(ChangeLoggedModel, CustomFieldModel):
images = GenericRelation( images = GenericRelation(
to='extras.ImageAttachment' to='extras.ImageAttachment'
) )
objects = NaturalOrderingManager()
tags = TaggableManager(through=TaggedItem) tags = TaggableManager(through=TaggedItem)
csv_headers = [ csv_headers = [
@ -235,7 +237,7 @@ class Site(ChangeLoggedModel, CustomFieldModel):
} }
class Meta: class Meta:
ordering = ['name'] ordering = ('_name',)
def __str__(self): def __str__(self):
return self.name return self.name
@ -380,13 +382,17 @@ class RackElevationHelperMixin:
# add gradients # add gradients
RackElevationHelperMixin._add_gradient(drawing, 'reserved', '#c7c7ff') RackElevationHelperMixin._add_gradient(drawing, 'reserved', '#c7c7ff')
RackElevationHelperMixin._add_gradient(drawing, 'occupied', '#f0f0f0') RackElevationHelperMixin._add_gradient(drawing, 'occupied', '#d7d7d7')
RackElevationHelperMixin._add_gradient(drawing, 'blocked', '#ffc7c7') RackElevationHelperMixin._add_gradient(drawing, 'blocked', '#ffc0c0')
return drawing return drawing
@staticmethod @staticmethod
def _draw_device_front(drawing, device, start, end, text): def _draw_device_front(drawing, device, start, end, text):
name = str(device)
if device.devicebay_count:
name += ' ({}/{})'.format(device.get_children().count(), device.devicebay_count)
color = device.device_role.color color = device.device_role.color
link = drawing.add( link = drawing.add(
drawing.a( drawing.a(
@ -401,7 +407,7 @@ class RackElevationHelperMixin:
)) ))
link.add(drawing.rect(start, end, style='fill: #{}'.format(color), class_='slot')) link.add(drawing.rect(start, end, style='fill: #{}'.format(color), class_='slot'))
hex_color = '#{}'.format(foreground_color(color)) hex_color = '#{}'.format(foreground_color(color))
link.add(drawing.text(str(device), insert=text, fill=hex_color)) link.add(drawing.text(str(name), insert=text, fill=hex_color))
@staticmethod @staticmethod
def _draw_device_rear(drawing, device, start, end, text): def _draw_device_rear(drawing, device, start, end, text):
@ -431,11 +437,19 @@ class RackElevationHelperMixin:
link.add(drawing.rect(start, end, class_=class_)) link.add(drawing.rect(start, end, class_=class_))
link.add(drawing.text("add device", insert=text, class_='add-device')) link.add(drawing.text("add device", insert=text, class_='add-device'))
def _draw_elevations(self, elevation, reserved_units, face, unit_width, unit_height): def _draw_elevations(self, elevation, reserved_units, face, unit_width, unit_height, legend_width):
drawing = self._setup_drawing(unit_width, unit_height * self.u_height) drawing = self._setup_drawing(unit_width + legend_width, unit_height * self.u_height)
unit_cursor = 0 unit_cursor = 0
for ru in range(0, self.u_height):
start_y = ru * unit_height
position_coordinates = (legend_width / 2, start_y + unit_height / 2 + 2)
unit = ru + 1 if self.desc_units else self.u_height - ru
drawing.add(
drawing.text(str(unit), position_coordinates, class_="unit")
)
for unit in elevation: for unit in elevation:
# Loop through all units in the elevation # Loop through all units in the elevation
@ -445,9 +459,9 @@ class RackElevationHelperMixin:
# Setup drawing coordinates # Setup drawing coordinates
start_y = unit_cursor * unit_height start_y = unit_cursor * unit_height
end_y = unit_height * height end_y = unit_height * height
start_cordinates = (0, start_y) start_cordinates = (legend_width, start_y)
end_cordinates = (unit_width, end_y) end_cordinates = (legend_width + unit_width, end_y)
text_cordinates = (unit_width / 2, start_y + end_y / 2) text_cordinates = (legend_width + (unit_width / 2), start_y + end_y / 2)
# Draw the device # Draw the device
if device and device.face == face: if device and device.face == face:
@ -469,7 +483,7 @@ class RackElevationHelperMixin:
unit_cursor += height unit_cursor += height
# Wrap the drawing with a border # Wrap the drawing with a border
drawing.add(drawing.rect((0, 0), (unit_width, self.u_height * unit_height), class_='rack')) drawing.add(drawing.rect((legend_width, 0), (unit_width, self.u_height * unit_height), class_='rack'))
return drawing return drawing
@ -492,7 +506,8 @@ class RackElevationHelperMixin:
self, self,
face=DeviceFaceChoices.FACE_FRONT, face=DeviceFaceChoices.FACE_FRONT,
unit_width=RACK_ELEVATION_UNIT_WIDTH_DEFAULT, unit_width=RACK_ELEVATION_UNIT_WIDTH_DEFAULT,
unit_height=RACK_ELEVATION_UNIT_HEIGHT_DEFAULT unit_height=RACK_ELEVATION_UNIT_HEIGHT_DEFAULT,
legend_width=RACK_ELEVATION_LEGEND_WIDTH_DEFAULT
): ):
""" """
Return an SVG of the rack elevation Return an SVG of the rack elevation
@ -505,7 +520,7 @@ class RackElevationHelperMixin:
elevation = self.merge_elevations(face) elevation = self.merge_elevations(face)
reserved_units = self.get_reserved_units() reserved_units = self.get_reserved_units()
return self._draw_elevations(elevation, reserved_units, face, unit_width, unit_height) return self._draw_elevations(elevation, reserved_units, face, unit_width, unit_height, legend_width)
class Rack(ChangeLoggedModel, CustomFieldModel, RackElevationHelperMixin): class Rack(ChangeLoggedModel, CustomFieldModel, RackElevationHelperMixin):
@ -516,6 +531,11 @@ class Rack(ChangeLoggedModel, CustomFieldModel, RackElevationHelperMixin):
name = models.CharField( name = models.CharField(
max_length=50 max_length=50
) )
_name = NaturalOrderingField(
target_field='name',
max_length=100,
blank=True
)
facility_id = models.CharField( facility_id = models.CharField(
max_length=50, max_length=50,
blank=True, blank=True,
@ -612,8 +632,6 @@ class Rack(ChangeLoggedModel, CustomFieldModel, RackElevationHelperMixin):
images = GenericRelation( images = GenericRelation(
to='extras.ImageAttachment' to='extras.ImageAttachment'
) )
objects = NaturalOrderingManager()
tags = TaggableManager(through=TaggedItem) tags = TaggableManager(through=TaggedItem)
csv_headers = [ csv_headers = [
@ -634,12 +652,12 @@ class Rack(ChangeLoggedModel, CustomFieldModel, RackElevationHelperMixin):
} }
class Meta: class Meta:
ordering = ('site', 'group', 'name', 'pk') # (site, group, name) may be non-unique ordering = ('site', 'group', '_name', 'pk') # (site, group, name) may be non-unique
unique_together = [ unique_together = (
# Name and facility_id must be unique *only* within a RackGroup # Name and facility_id must be unique *only* within a RackGroup
['group', 'name'], ('group', 'name'),
['group', 'facility_id'], ('group', 'facility_id'),
] )
def __str__(self): def __str__(self):
return self.display_name or super().__str__() return self.display_name or super().__str__()
@ -1018,9 +1036,6 @@ class DeviceType(ChangeLoggedModel, CustomFieldModel):
tags = TaggableManager(through=TaggedItem) tags = TaggableManager(through=TaggedItem)
csv_headers = [
'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role', 'comments',
]
clone_fields = [ clone_fields = [
'manufacturer', 'u_height', 'is_full_depth', 'subdevice_role', 'manufacturer', 'u_height', 'is_full_depth', 'subdevice_role',
] ]
@ -1316,6 +1331,12 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
blank=True, blank=True,
null=True null=True
) )
_name = NaturalOrderingField(
target_field='name',
max_length=100,
blank=True,
null=True
)
serial = models.CharField( serial = models.CharField(
max_length=50, max_length=50,
blank=True, blank=True,
@ -1410,8 +1431,6 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
images = GenericRelation( images = GenericRelation(
to='extras.ImageAttachment' to='extras.ImageAttachment'
) )
objects = NaturalOrderingManager()
tags = TaggableManager(through=TaggedItem) tags = TaggableManager(through=TaggedItem)
csv_headers = [ csv_headers = [
@ -1433,12 +1452,12 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
} }
class Meta: class Meta:
ordering = ('name', 'pk') # Name may be NULL ordering = ('_name', 'pk') # Name may be null
unique_together = [ unique_together = (
['site', 'tenant', 'name'], # See validate_unique below ('site', 'tenant', 'name'), # See validate_unique below
['rack', 'position', 'face'], ('rack', 'position', 'face'),
['virtual_chassis', 'vc_position'], ('virtual_chassis', 'vc_position'),
] )
permissions = ( permissions = (
('napalm_read', 'Read-only access to devices via NAPALM'), ('napalm_read', 'Read-only access to devices via NAPALM'),
('napalm_write', 'Read/write access to devices via NAPALM'), ('napalm_write', 'Read/write access to devices via NAPALM'),

View File

@ -4,9 +4,9 @@ from django.db import models
from dcim.choices import * from dcim.choices import *
from dcim.constants import * from dcim.constants import *
from dcim.managers import InterfaceManager
from extras.models import ObjectChange from extras.models import ObjectChange
from utilities.managers import NaturalOrderingManager from utilities.fields import NaturalOrderingField
from utilities.ordering import naturalize_interface
from utilities.utils import serialize_object from utilities.utils import serialize_object
from .device_components import ( from .device_components import (
ConsolePort, ConsoleServerPort, DeviceBay, FrontPort, Interface, PowerOutlet, PowerPort, RearPort, ConsolePort, ConsoleServerPort, DeviceBay, FrontPort, Interface, PowerOutlet, PowerPort, RearPort,
@ -58,17 +58,20 @@ class ConsolePortTemplate(ComponentTemplateModel):
name = models.CharField( name = models.CharField(
max_length=50 max_length=50
) )
_name = NaturalOrderingField(
target_field='name',
max_length=100,
blank=True
)
type = models.CharField( type = models.CharField(
max_length=50, max_length=50,
choices=ConsolePortTypeChoices, choices=ConsolePortTypeChoices,
blank=True blank=True
) )
objects = NaturalOrderingManager()
class Meta: class Meta:
ordering = ['device_type', 'name'] ordering = ('device_type', '_name')
unique_together = ['device_type', 'name'] unique_together = ('device_type', 'name')
def __str__(self): def __str__(self):
return self.name return self.name
@ -93,17 +96,20 @@ class ConsoleServerPortTemplate(ComponentTemplateModel):
name = models.CharField( name = models.CharField(
max_length=50 max_length=50
) )
_name = NaturalOrderingField(
target_field='name',
max_length=100,
blank=True
)
type = models.CharField( type = models.CharField(
max_length=50, max_length=50,
choices=ConsolePortTypeChoices, choices=ConsolePortTypeChoices,
blank=True blank=True
) )
objects = NaturalOrderingManager()
class Meta: class Meta:
ordering = ['device_type', 'name'] ordering = ('device_type', '_name')
unique_together = ['device_type', 'name'] unique_together = ('device_type', 'name')
def __str__(self): def __str__(self):
return self.name return self.name
@ -128,6 +134,11 @@ class PowerPortTemplate(ComponentTemplateModel):
name = models.CharField( name = models.CharField(
max_length=50 max_length=50
) )
_name = NaturalOrderingField(
target_field='name',
max_length=100,
blank=True
)
type = models.CharField( type = models.CharField(
max_length=50, max_length=50,
choices=PowerPortTypeChoices, choices=PowerPortTypeChoices,
@ -146,11 +157,9 @@ class PowerPortTemplate(ComponentTemplateModel):
help_text="Allocated power draw (watts)" help_text="Allocated power draw (watts)"
) )
objects = NaturalOrderingManager()
class Meta: class Meta:
ordering = ['device_type', 'name'] ordering = ('device_type', '_name')
unique_together = ['device_type', 'name'] unique_together = ('device_type', 'name')
def __str__(self): def __str__(self):
return self.name return self.name
@ -159,6 +168,7 @@ class PowerPortTemplate(ComponentTemplateModel):
return PowerPort( return PowerPort(
device=device, device=device,
name=self.name, name=self.name,
type=self.type,
maximum_draw=self.maximum_draw, maximum_draw=self.maximum_draw,
allocated_draw=self.allocated_draw allocated_draw=self.allocated_draw
) )
@ -176,6 +186,11 @@ class PowerOutletTemplate(ComponentTemplateModel):
name = models.CharField( name = models.CharField(
max_length=50 max_length=50
) )
_name = NaturalOrderingField(
target_field='name',
max_length=100,
blank=True
)
type = models.CharField( type = models.CharField(
max_length=50, max_length=50,
choices=PowerOutletTypeChoices, choices=PowerOutletTypeChoices,
@ -195,11 +210,9 @@ class PowerOutletTemplate(ComponentTemplateModel):
help_text="Phase (for three-phase feeds)" help_text="Phase (for three-phase feeds)"
) )
objects = NaturalOrderingManager()
class Meta: class Meta:
ordering = ['device_type', 'name'] ordering = ('device_type', '_name')
unique_together = ['device_type', 'name'] unique_together = ('device_type', 'name')
def __str__(self): def __str__(self):
return self.name return self.name
@ -220,6 +233,7 @@ class PowerOutletTemplate(ComponentTemplateModel):
return PowerOutlet( return PowerOutlet(
device=device, device=device,
name=self.name, name=self.name,
type=self.type,
power_port=power_port, power_port=power_port,
feed_leg=self.feed_leg feed_leg=self.feed_leg
) )
@ -237,6 +251,12 @@ class InterfaceTemplate(ComponentTemplateModel):
name = models.CharField( name = models.CharField(
max_length=64 max_length=64
) )
_name = NaturalOrderingField(
target_field='name',
naturalize_function=naturalize_interface,
max_length=100,
blank=True
)
type = models.CharField( type = models.CharField(
max_length=50, max_length=50,
choices=InterfaceTypeChoices choices=InterfaceTypeChoices
@ -246,11 +266,9 @@ class InterfaceTemplate(ComponentTemplateModel):
verbose_name='Management only' verbose_name='Management only'
) )
objects = InterfaceManager()
class Meta: class Meta:
ordering = ['device_type', 'name'] ordering = ('device_type', '_name')
unique_together = ['device_type', 'name'] unique_together = ('device_type', 'name')
def __str__(self): def __str__(self):
return self.name return self.name
@ -276,6 +294,11 @@ class FrontPortTemplate(ComponentTemplateModel):
name = models.CharField( name = models.CharField(
max_length=64 max_length=64
) )
_name = NaturalOrderingField(
target_field='name',
max_length=100,
blank=True
)
type = models.CharField( type = models.CharField(
max_length=50, max_length=50,
choices=PortTypeChoices choices=PortTypeChoices
@ -290,14 +313,12 @@ class FrontPortTemplate(ComponentTemplateModel):
validators=[MinValueValidator(1), MaxValueValidator(64)] validators=[MinValueValidator(1), MaxValueValidator(64)]
) )
objects = NaturalOrderingManager()
class Meta: class Meta:
ordering = ['device_type', 'name'] ordering = ('device_type', '_name')
unique_together = [ unique_together = (
['device_type', 'name'], ('device_type', 'name'),
['rear_port', 'rear_port_position'], ('rear_port', 'rear_port_position'),
] )
def __str__(self): def __str__(self):
return self.name return self.name
@ -344,6 +365,11 @@ class RearPortTemplate(ComponentTemplateModel):
name = models.CharField( name = models.CharField(
max_length=64 max_length=64
) )
_name = NaturalOrderingField(
target_field='name',
max_length=100,
blank=True
)
type = models.CharField( type = models.CharField(
max_length=50, max_length=50,
choices=PortTypeChoices choices=PortTypeChoices
@ -353,11 +379,9 @@ class RearPortTemplate(ComponentTemplateModel):
validators=[MinValueValidator(1), MaxValueValidator(64)] validators=[MinValueValidator(1), MaxValueValidator(64)]
) )
objects = NaturalOrderingManager()
class Meta: class Meta:
ordering = ['device_type', 'name'] ordering = ('device_type', '_name')
unique_together = ['device_type', 'name'] unique_together = ('device_type', 'name')
def __str__(self): def __str__(self):
return self.name return self.name
@ -383,12 +407,15 @@ class DeviceBayTemplate(ComponentTemplateModel):
name = models.CharField( name = models.CharField(
max_length=50 max_length=50
) )
_name = NaturalOrderingField(
objects = NaturalOrderingManager() target_field='name',
max_length=100,
blank=True
)
class Meta: class Meta:
ordering = ['device_type', 'name'] ordering = ('device_type', '_name')
unique_together = ['device_type', 'name'] unique_together = ('device_type', 'name')
def __str__(self): def __str__(self):
return self.name return self.name

View File

@ -10,9 +10,9 @@ from dcim.choices import *
from dcim.constants import * from dcim.constants import *
from dcim.exceptions import LoopDetected from dcim.exceptions import LoopDetected
from dcim.fields import MACAddressField from dcim.fields import MACAddressField
from dcim.managers import InterfaceManager
from extras.models import ObjectChange, TaggedItem from extras.models import ObjectChange, TaggedItem
from utilities.managers import NaturalOrderingManager from utilities.fields import NaturalOrderingField
from utilities.ordering import naturalize_interface
from utilities.utils import serialize_object from utilities.utils import serialize_object
from virtualization.choices import VMInterfaceTypeChoices from virtualization.choices import VMInterfaceTypeChoices
@ -181,6 +181,11 @@ class ConsolePort(CableTermination, ComponentModel):
name = models.CharField( name = models.CharField(
max_length=50 max_length=50
) )
_name = NaturalOrderingField(
target_field='name',
max_length=100,
blank=True
)
type = models.CharField( type = models.CharField(
max_length=50, max_length=50,
choices=ConsolePortTypeChoices, choices=ConsolePortTypeChoices,
@ -197,15 +202,13 @@ class ConsolePort(CableTermination, ComponentModel):
choices=CONNECTION_STATUS_CHOICES, choices=CONNECTION_STATUS_CHOICES,
blank=True blank=True
) )
objects = NaturalOrderingManager()
tags = TaggableManager(through=TaggedItem) tags = TaggableManager(through=TaggedItem)
csv_headers = ['device', 'name', 'type', 'description'] csv_headers = ['device', 'name', 'type', 'description']
class Meta: class Meta:
ordering = ['device', 'name'] ordering = ('device', '_name')
unique_together = ['device', 'name'] unique_together = ('device', 'name')
def __str__(self): def __str__(self):
return self.name return self.name
@ -238,6 +241,11 @@ class ConsoleServerPort(CableTermination, ComponentModel):
name = models.CharField( name = models.CharField(
max_length=50 max_length=50
) )
_name = NaturalOrderingField(
target_field='name',
max_length=100,
blank=True
)
type = models.CharField( type = models.CharField(
max_length=50, max_length=50,
choices=ConsolePortTypeChoices, choices=ConsolePortTypeChoices,
@ -247,14 +255,13 @@ class ConsoleServerPort(CableTermination, ComponentModel):
choices=CONNECTION_STATUS_CHOICES, choices=CONNECTION_STATUS_CHOICES,
blank=True blank=True
) )
objects = NaturalOrderingManager()
tags = TaggableManager(through=TaggedItem) tags = TaggableManager(through=TaggedItem)
csv_headers = ['device', 'name', 'type', 'description'] csv_headers = ['device', 'name', 'type', 'description']
class Meta: class Meta:
unique_together = ['device', 'name'] ordering = ('device', '_name')
unique_together = ('device', 'name')
def __str__(self): def __str__(self):
return self.name return self.name
@ -287,6 +294,11 @@ class PowerPort(CableTermination, ComponentModel):
name = models.CharField( name = models.CharField(
max_length=50 max_length=50
) )
_name = NaturalOrderingField(
target_field='name',
max_length=100,
blank=True
)
type = models.CharField( type = models.CharField(
max_length=50, max_length=50,
choices=PowerPortTypeChoices, choices=PowerPortTypeChoices,
@ -322,15 +334,13 @@ class PowerPort(CableTermination, ComponentModel):
choices=CONNECTION_STATUS_CHOICES, choices=CONNECTION_STATUS_CHOICES,
blank=True blank=True
) )
objects = NaturalOrderingManager()
tags = TaggableManager(through=TaggedItem) tags = TaggableManager(through=TaggedItem)
csv_headers = ['device', 'name', 'type', 'maximum_draw', 'allocated_draw', 'description'] csv_headers = ['device', 'name', 'type', 'maximum_draw', 'allocated_draw', 'description']
class Meta: class Meta:
ordering = ['device', 'name'] ordering = ('device', '_name')
unique_together = ['device', 'name'] unique_together = ('device', 'name')
def __str__(self): def __str__(self):
return self.name return self.name
@ -433,6 +443,11 @@ class PowerOutlet(CableTermination, ComponentModel):
name = models.CharField( name = models.CharField(
max_length=50 max_length=50
) )
_name = NaturalOrderingField(
target_field='name',
max_length=100,
blank=True
)
type = models.CharField( type = models.CharField(
max_length=50, max_length=50,
choices=PowerOutletTypeChoices, choices=PowerOutletTypeChoices,
@ -455,14 +470,13 @@ class PowerOutlet(CableTermination, ComponentModel):
choices=CONNECTION_STATUS_CHOICES, choices=CONNECTION_STATUS_CHOICES,
blank=True blank=True
) )
objects = NaturalOrderingManager()
tags = TaggableManager(through=TaggedItem) tags = TaggableManager(through=TaggedItem)
csv_headers = ['device', 'name', 'type', 'power_port', 'feed_leg', 'description'] csv_headers = ['device', 'name', 'type', 'power_port', 'feed_leg', 'description']
class Meta: class Meta:
unique_together = ['device', 'name'] ordering = ('device', '_name')
unique_together = ('device', 'name')
def __str__(self): def __str__(self):
return self.name return self.name
@ -515,6 +529,12 @@ class Interface(CableTermination, ComponentModel):
name = models.CharField( name = models.CharField(
max_length=64 max_length=64
) )
_name = NaturalOrderingField(
target_field='name',
naturalize_function=naturalize_interface,
max_length=100,
blank=True
)
_connected_interface = models.OneToOneField( _connected_interface = models.OneToOneField(
to='self', to='self',
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
@ -583,8 +603,6 @@ class Interface(CableTermination, ComponentModel):
blank=True, blank=True,
verbose_name='Tagged VLANs' verbose_name='Tagged VLANs'
) )
objects = InterfaceManager()
tags = TaggableManager(through=TaggedItem) tags = TaggableManager(through=TaggedItem)
csv_headers = [ csv_headers = [
@ -593,8 +611,9 @@ class Interface(CableTermination, ComponentModel):
] ]
class Meta: class Meta:
ordering = ['device', 'name'] # TODO: ordering and unique_together should include virtual_machine
unique_together = ['device', 'name'] ordering = ('device', '_name')
unique_together = ('device', 'name')
def __str__(self): def __str__(self):
return self.name return self.name
@ -676,7 +695,7 @@ class Interface(CableTermination, ComponentModel):
self.untagged_vlan = None self.untagged_vlan = None
# Only "tagged" interfaces may have tagged VLANs assigned. ("tagged all" implies all VLANs are assigned.) # 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: if self.pk and self.mode != InterfaceModeChoices.MODE_TAGGED:
self.tagged_vlans.clear() self.tagged_vlans.clear()
return super().save(*args, **kwargs) return super().save(*args, **kwargs)
@ -761,6 +780,11 @@ class FrontPort(CableTermination, ComponentModel):
name = models.CharField( name = models.CharField(
max_length=64 max_length=64
) )
_name = NaturalOrderingField(
target_field='name',
max_length=100,
blank=True
)
type = models.CharField( type = models.CharField(
max_length=50, max_length=50,
choices=PortTypeChoices choices=PortTypeChoices
@ -774,20 +798,17 @@ class FrontPort(CableTermination, ComponentModel):
default=1, default=1,
validators=[MinValueValidator(1), MaxValueValidator(64)] validators=[MinValueValidator(1), MaxValueValidator(64)]
) )
is_path_endpoint = False
objects = NaturalOrderingManager()
tags = TaggableManager(through=TaggedItem) tags = TaggableManager(through=TaggedItem)
csv_headers = ['device', 'name', 'type', 'rear_port', 'rear_port_position', 'description'] csv_headers = ['device', 'name', 'type', 'rear_port', 'rear_port_position', 'description']
is_path_endpoint = False
class Meta: class Meta:
ordering = ['device', 'name'] ordering = ('device', '_name')
unique_together = [ unique_together = (
['device', 'name'], ('device', 'name'),
['rear_port', 'rear_port_position'], ('rear_port', 'rear_port_position'),
] )
def __str__(self): def __str__(self):
return self.name return self.name
@ -831,6 +852,11 @@ class RearPort(CableTermination, ComponentModel):
name = models.CharField( name = models.CharField(
max_length=64 max_length=64
) )
_name = NaturalOrderingField(
target_field='name',
max_length=100,
blank=True
)
type = models.CharField( type = models.CharField(
max_length=50, max_length=50,
choices=PortTypeChoices choices=PortTypeChoices
@ -839,17 +865,14 @@ class RearPort(CableTermination, ComponentModel):
default=1, default=1,
validators=[MinValueValidator(1), MaxValueValidator(64)] validators=[MinValueValidator(1), MaxValueValidator(64)]
) )
is_path_endpoint = False
objects = NaturalOrderingManager()
tags = TaggableManager(through=TaggedItem) tags = TaggableManager(through=TaggedItem)
csv_headers = ['device', 'name', 'type', 'positions', 'description'] csv_headers = ['device', 'name', 'type', 'positions', 'description']
is_path_endpoint = False
class Meta: class Meta:
ordering = ['device', 'name'] ordering = ('device', '_name')
unique_together = ['device', 'name'] unique_together = ('device', 'name')
def __str__(self): def __str__(self):
return self.name return self.name
@ -881,6 +904,11 @@ class DeviceBay(ComponentModel):
max_length=50, max_length=50,
verbose_name='Name' verbose_name='Name'
) )
_name = NaturalOrderingField(
target_field='name',
max_length=100,
blank=True
)
installed_device = models.OneToOneField( installed_device = models.OneToOneField(
to='dcim.Device', to='dcim.Device',
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
@ -888,15 +916,13 @@ class DeviceBay(ComponentModel):
blank=True, blank=True,
null=True null=True
) )
objects = NaturalOrderingManager()
tags = TaggableManager(through=TaggedItem) tags = TaggableManager(through=TaggedItem)
csv_headers = ['device', 'name', 'installed_device', 'description'] csv_headers = ['device', 'name', 'installed_device', 'description']
class Meta: class Meta:
ordering = ['device', 'name'] ordering = ('device', '_name')
unique_together = ['device', 'name'] unique_together = ('device', 'name')
def __str__(self): def __str__(self):
return '{} - {}'.format(self.device.name, self.name) return '{} - {}'.format(self.device.name, self.name)
@ -960,6 +986,11 @@ class InventoryItem(ComponentModel):
max_length=50, max_length=50,
verbose_name='Name' verbose_name='Name'
) )
_name = NaturalOrderingField(
target_field='name',
max_length=100,
blank=True
)
manufacturer = models.ForeignKey( manufacturer = models.ForeignKey(
to='dcim.Manufacturer', to='dcim.Manufacturer',
on_delete=models.PROTECT, on_delete=models.PROTECT,
@ -997,14 +1028,14 @@ class InventoryItem(ComponentModel):
] ]
class Meta: class Meta:
ordering = ['device__id', 'parent__id', 'name'] ordering = ('device__id', 'parent__id', '_name')
unique_together = ['device', 'parent', 'name'] unique_together = ('device', 'parent', 'name')
def __str__(self): def __str__(self):
return self.name return self.name
def get_absolute_url(self): def get_absolute_url(self):
return self.device.get_absolute_url() return reverse('dcim:device_inventory', kwargs={'pk': self.device.pk})
def to_csv(self): def to_csv(self):
return ( return (

View File

@ -200,6 +200,11 @@ def get_component_template_actions(model_name):
<i class="glyphicon glyphicon-pencil" aria-hidden="true"></i> <i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
</a> </a>
{{% endif %}} {{% endif %}}
{{% if perms.dcim.delete_{model_name} %}}
<a href="{{% url 'dcim:{model_name}_delete' pk=record.pk %}}?return_url={{{{ request.path }}}}" class="btn btn-xs btn-danger">
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
</a>
{{% endif %}}
""".format(model_name=model_name).strip() """.format(model_name=model_name).strip()
@ -229,7 +234,7 @@ class RegionTable(BaseTable):
class SiteTable(BaseTable): class SiteTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
name = tables.LinkColumn(order_by=('_nat1', '_nat2', '_nat3')) name = tables.LinkColumn(order_by=('_name',))
status = tables.TemplateColumn(template_code=STATUS_LABEL, verbose_name='Status') status = tables.TemplateColumn(template_code=STATUS_LABEL, verbose_name='Status')
region = tables.TemplateColumn(template_code=SITE_REGION_LINK) region = tables.TemplateColumn(template_code=SITE_REGION_LINK)
tenant = tables.TemplateColumn(template_code=COL_TENANT) tenant = tables.TemplateColumn(template_code=COL_TENANT)
@ -291,7 +296,7 @@ class RackRoleTable(BaseTable):
class RackTable(BaseTable): class RackTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
name = tables.LinkColumn(order_by=('_nat1', '_nat2', '_nat3')) name = tables.LinkColumn(order_by=('_name',))
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')]) site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')])
group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group') group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group')
tenant = tables.TemplateColumn(template_code=COL_TENANT) tenant = tables.TemplateColumn(template_code=COL_TENANT)
@ -409,6 +414,7 @@ class DeviceTypeTable(BaseTable):
class ConsolePortTemplateTable(BaseTable): class ConsolePortTemplateTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
name = tables.Column(order_by=('_name',))
actions = tables.TemplateColumn( actions = tables.TemplateColumn(
template_code=get_component_template_actions('consoleporttemplate'), template_code=get_component_template_actions('consoleporttemplate'),
attrs={'td': {'class': 'text-right noprint'}}, attrs={'td': {'class': 'text-right noprint'}},
@ -432,6 +438,7 @@ class ConsolePortImportTable(BaseTable):
class ConsoleServerPortTemplateTable(BaseTable): class ConsoleServerPortTemplateTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
name = tables.Column(order_by=('_name',))
actions = tables.TemplateColumn( actions = tables.TemplateColumn(
template_code=get_component_template_actions('consoleserverporttemplate'), template_code=get_component_template_actions('consoleserverporttemplate'),
attrs={'td': {'class': 'text-right noprint'}}, attrs={'td': {'class': 'text-right noprint'}},
@ -440,7 +447,7 @@ class ConsoleServerPortTemplateTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = ConsoleServerPortTemplate model = ConsoleServerPortTemplate
fields = ('pk', 'name', 'actions') fields = ('pk', 'name', 'type', 'actions')
empty_text = "None" empty_text = "None"
@ -455,6 +462,7 @@ class ConsoleServerPortImportTable(BaseTable):
class PowerPortTemplateTable(BaseTable): class PowerPortTemplateTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
name = tables.Column(order_by=('_name',))
actions = tables.TemplateColumn( actions = tables.TemplateColumn(
template_code=get_component_template_actions('powerporttemplate'), template_code=get_component_template_actions('powerporttemplate'),
attrs={'td': {'class': 'text-right noprint'}}, attrs={'td': {'class': 'text-right noprint'}},
@ -478,6 +486,7 @@ class PowerPortImportTable(BaseTable):
class PowerOutletTemplateTable(BaseTable): class PowerOutletTemplateTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
name = tables.Column(order_by=('_name',))
actions = tables.TemplateColumn( actions = tables.TemplateColumn(
template_code=get_component_template_actions('poweroutlettemplate'), template_code=get_component_template_actions('poweroutlettemplate'),
attrs={'td': {'class': 'text-right noprint'}}, attrs={'td': {'class': 'text-right noprint'}},
@ -526,6 +535,7 @@ class InterfaceImportTable(BaseTable):
class FrontPortTemplateTable(BaseTable): class FrontPortTemplateTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
name = tables.Column(order_by=('_name',))
rear_port_position = tables.Column( rear_port_position = tables.Column(
verbose_name='Position' verbose_name='Position'
) )
@ -552,6 +562,7 @@ class FrontPortImportTable(BaseTable):
class RearPortTemplateTable(BaseTable): class RearPortTemplateTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
name = tables.Column(order_by=('_name',))
actions = tables.TemplateColumn( actions = tables.TemplateColumn(
template_code=get_component_template_actions('rearporttemplate'), template_code=get_component_template_actions('rearporttemplate'),
attrs={'td': {'class': 'text-right noprint'}}, attrs={'td': {'class': 'text-right noprint'}},
@ -575,6 +586,7 @@ class RearPortImportTable(BaseTable):
class DeviceBayTemplateTable(BaseTable): class DeviceBayTemplateTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
name = tables.Column(order_by=('_name',))
actions = tables.TemplateColumn( actions = tables.TemplateColumn(
template_code=get_component_template_actions('devicebaytemplate'), template_code=get_component_template_actions('devicebaytemplate'),
attrs={'td': {'class': 'text-right noprint'}}, attrs={'td': {'class': 'text-right noprint'}},
@ -654,7 +666,7 @@ class PlatformTable(BaseTable):
class DeviceTable(BaseTable): class DeviceTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
name = tables.TemplateColumn( name = tables.TemplateColumn(
order_by=('_nat1', '_nat2', '_nat3'), order_by=('_name',),
template_code=DEVICE_LINK template_code=DEVICE_LINK
) )
status = tables.TemplateColumn(template_code=STATUS_LABEL, verbose_name='Status') status = tables.TemplateColumn(template_code=STATUS_LABEL, verbose_name='Status')
@ -704,6 +716,7 @@ class DeviceImportTable(BaseTable):
class DeviceComponentDetailTable(BaseTable): class DeviceComponentDetailTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
name = tables.Column(order_by=('_name',))
cable = tables.LinkColumn() cable = tables.LinkColumn()
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
@ -713,6 +726,7 @@ class DeviceComponentDetailTable(BaseTable):
class ConsolePortTable(BaseTable): class ConsolePortTable(BaseTable):
name = tables.Column(order_by=('_name',))
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = ConsolePort model = ConsolePort
@ -727,6 +741,7 @@ class ConsolePortDetailTable(DeviceComponentDetailTable):
class ConsoleServerPortTable(BaseTable): class ConsoleServerPortTable(BaseTable):
name = tables.Column(order_by=('_name',))
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = ConsoleServerPort model = ConsoleServerPort
@ -741,6 +756,7 @@ class ConsoleServerPortDetailTable(DeviceComponentDetailTable):
class PowerPortTable(BaseTable): class PowerPortTable(BaseTable):
name = tables.Column(order_by=('_name',))
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = PowerPort model = PowerPort
@ -755,6 +771,7 @@ class PowerPortDetailTable(DeviceComponentDetailTable):
class PowerOutletTable(BaseTable): class PowerOutletTable(BaseTable):
name = tables.Column(order_by=('_name',))
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = PowerOutlet model = PowerOutlet
@ -777,6 +794,7 @@ class InterfaceTable(BaseTable):
class InterfaceDetailTable(DeviceComponentDetailTable): class InterfaceDetailTable(DeviceComponentDetailTable):
parent = tables.LinkColumn(order_by=('device', 'virtual_machine')) parent = tables.LinkColumn(order_by=('device', 'virtual_machine'))
name = tables.LinkColumn()
class Meta(InterfaceTable.Meta): class Meta(InterfaceTable.Meta):
order_by = ('parent', 'name') order_by = ('parent', 'name')
@ -785,6 +803,7 @@ class InterfaceDetailTable(DeviceComponentDetailTable):
class FrontPortTable(BaseTable): class FrontPortTable(BaseTable):
name = tables.Column(order_by=('_name',))
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = FrontPort model = FrontPort
@ -800,6 +819,7 @@ class FrontPortDetailTable(DeviceComponentDetailTable):
class RearPortTable(BaseTable): class RearPortTable(BaseTable):
name = tables.Column(order_by=('_name',))
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = RearPort model = RearPort
@ -815,6 +835,7 @@ class RearPortDetailTable(DeviceComponentDetailTable):
class DeviceBayTable(BaseTable): class DeviceBayTable(BaseTable):
name = tables.Column(order_by=('_name',))
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = DeviceBay model = DeviceBay

File diff suppressed because it is too large Load Diff

View File

@ -14,317 +14,338 @@ app_name = 'dcim'
urlpatterns = [ urlpatterns = [
# Regions # Regions
path(r'regions/', views.RegionListView.as_view(), name='region_list'), path('regions/', views.RegionListView.as_view(), name='region_list'),
path(r'regions/add/', views.RegionCreateView.as_view(), name='region_add'), path('regions/add/', views.RegionCreateView.as_view(), name='region_add'),
path(r'regions/import/', views.RegionBulkImportView.as_view(), name='region_import'), path('regions/import/', views.RegionBulkImportView.as_view(), name='region_import'),
path(r'regions/delete/', views.RegionBulkDeleteView.as_view(), name='region_bulk_delete'), path('regions/delete/', views.RegionBulkDeleteView.as_view(), name='region_bulk_delete'),
path(r'regions/<int:pk>/edit/', views.RegionEditView.as_view(), name='region_edit'), path('regions/<int:pk>/edit/', views.RegionEditView.as_view(), name='region_edit'),
path(r'regions/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='region_changelog', kwargs={'model': Region}), path('regions/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='region_changelog', kwargs={'model': Region}),
# Sites # Sites
path(r'sites/', views.SiteListView.as_view(), name='site_list'), path('sites/', views.SiteListView.as_view(), name='site_list'),
path(r'sites/add/', views.SiteCreateView.as_view(), name='site_add'), path('sites/add/', views.SiteCreateView.as_view(), name='site_add'),
path(r'sites/import/', views.SiteBulkImportView.as_view(), name='site_import'), path('sites/import/', views.SiteBulkImportView.as_view(), name='site_import'),
path(r'sites/edit/', views.SiteBulkEditView.as_view(), name='site_bulk_edit'), path('sites/edit/', views.SiteBulkEditView.as_view(), name='site_bulk_edit'),
path(r'sites/delete/', views.SiteBulkDeleteView.as_view(), name='site_bulk_delete'), path('sites/delete/', views.SiteBulkDeleteView.as_view(), name='site_bulk_delete'),
path(r'sites/<slug:slug>/', views.SiteView.as_view(), name='site'), path('sites/<slug:slug>/', views.SiteView.as_view(), name='site'),
path(r'sites/<slug:slug>/edit/', views.SiteEditView.as_view(), name='site_edit'), path('sites/<slug:slug>/edit/', views.SiteEditView.as_view(), name='site_edit'),
path(r'sites/<slug:slug>/delete/', views.SiteDeleteView.as_view(), name='site_delete'), path('sites/<slug:slug>/delete/', views.SiteDeleteView.as_view(), name='site_delete'),
path(r'sites/<slug:slug>/changelog/', ObjectChangeLogView.as_view(), name='site_changelog', kwargs={'model': Site}), path('sites/<slug:slug>/changelog/', ObjectChangeLogView.as_view(), name='site_changelog', kwargs={'model': Site}),
path(r'sites/<int:object_id>/images/add/', ImageAttachmentEditView.as_view(), name='site_add_image', kwargs={'model': Site}), path('sites/<int:object_id>/images/add/', ImageAttachmentEditView.as_view(), name='site_add_image', kwargs={'model': Site}),
# Rack groups # Rack groups
path(r'rack-groups/', views.RackGroupListView.as_view(), name='rackgroup_list'), path('rack-groups/', views.RackGroupListView.as_view(), name='rackgroup_list'),
path(r'rack-groups/add/', views.RackGroupCreateView.as_view(), name='rackgroup_add'), path('rack-groups/add/', views.RackGroupCreateView.as_view(), name='rackgroup_add'),
path(r'rack-groups/import/', views.RackGroupBulkImportView.as_view(), name='rackgroup_import'), path('rack-groups/import/', views.RackGroupBulkImportView.as_view(), name='rackgroup_import'),
path(r'rack-groups/delete/', views.RackGroupBulkDeleteView.as_view(), name='rackgroup_bulk_delete'), path('rack-groups/delete/', views.RackGroupBulkDeleteView.as_view(), name='rackgroup_bulk_delete'),
path(r'rack-groups/<int:pk>/edit/', views.RackGroupEditView.as_view(), name='rackgroup_edit'), path('rack-groups/<int:pk>/edit/', views.RackGroupEditView.as_view(), name='rackgroup_edit'),
path(r'rack-groups/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='rackgroup_changelog', kwargs={'model': RackGroup}), path('rack-groups/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='rackgroup_changelog', kwargs={'model': RackGroup}),
# Rack roles # Rack roles
path(r'rack-roles/', views.RackRoleListView.as_view(), name='rackrole_list'), path('rack-roles/', views.RackRoleListView.as_view(), name='rackrole_list'),
path(r'rack-roles/add/', views.RackRoleCreateView.as_view(), name='rackrole_add'), path('rack-roles/add/', views.RackRoleCreateView.as_view(), name='rackrole_add'),
path(r'rack-roles/import/', views.RackRoleBulkImportView.as_view(), name='rackrole_import'), path('rack-roles/import/', views.RackRoleBulkImportView.as_view(), name='rackrole_import'),
path(r'rack-roles/delete/', views.RackRoleBulkDeleteView.as_view(), name='rackrole_bulk_delete'), path('rack-roles/delete/', views.RackRoleBulkDeleteView.as_view(), name='rackrole_bulk_delete'),
path(r'rack-roles/<int:pk>/edit/', views.RackRoleEditView.as_view(), name='rackrole_edit'), path('rack-roles/<int:pk>/edit/', views.RackRoleEditView.as_view(), name='rackrole_edit'),
path(r'rack-roles/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='rackrole_changelog', kwargs={'model': RackRole}), path('rack-roles/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='rackrole_changelog', kwargs={'model': RackRole}),
# Rack reservations # Rack reservations
path(r'rack-reservations/', views.RackReservationListView.as_view(), name='rackreservation_list'), path('rack-reservations/', views.RackReservationListView.as_view(), name='rackreservation_list'),
path(r'rack-reservations/edit/', views.RackReservationBulkEditView.as_view(), name='rackreservation_bulk_edit'), path('rack-reservations/edit/', views.RackReservationBulkEditView.as_view(), name='rackreservation_bulk_edit'),
path(r'rack-reservations/delete/', views.RackReservationBulkDeleteView.as_view(), name='rackreservation_bulk_delete'), path('rack-reservations/delete/', views.RackReservationBulkDeleteView.as_view(), name='rackreservation_bulk_delete'),
path(r'rack-reservations/<int:pk>/edit/', views.RackReservationEditView.as_view(), name='rackreservation_edit'), path('rack-reservations/<int:pk>/edit/', views.RackReservationEditView.as_view(), name='rackreservation_edit'),
path(r'rack-reservations/<int:pk>/delete/', views.RackReservationDeleteView.as_view(), name='rackreservation_delete'), path('rack-reservations/<int:pk>/delete/', views.RackReservationDeleteView.as_view(), name='rackreservation_delete'),
path(r'rack-reservations/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='rackreservation_changelog', kwargs={'model': RackReservation}), path('rack-reservations/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='rackreservation_changelog', kwargs={'model': RackReservation}),
# Racks # Racks
path(r'racks/', views.RackListView.as_view(), name='rack_list'), path('racks/', views.RackListView.as_view(), name='rack_list'),
path(r'rack-elevations/', views.RackElevationListView.as_view(), name='rack_elevation_list'), path('rack-elevations/', views.RackElevationListView.as_view(), name='rack_elevation_list'),
path(r'racks/add/', views.RackEditView.as_view(), name='rack_add'), path('racks/add/', views.RackCreateView.as_view(), name='rack_add'),
path(r'racks/import/', views.RackBulkImportView.as_view(), name='rack_import'), path('racks/import/', views.RackBulkImportView.as_view(), name='rack_import'),
path(r'racks/edit/', views.RackBulkEditView.as_view(), name='rack_bulk_edit'), path('racks/edit/', views.RackBulkEditView.as_view(), name='rack_bulk_edit'),
path(r'racks/delete/', views.RackBulkDeleteView.as_view(), name='rack_bulk_delete'), path('racks/delete/', views.RackBulkDeleteView.as_view(), name='rack_bulk_delete'),
path(r'racks/<int:pk>/', views.RackView.as_view(), name='rack'), path('racks/<int:pk>/', views.RackView.as_view(), name='rack'),
path(r'racks/<int:pk>/edit/', views.RackEditView.as_view(), name='rack_edit'), path('racks/<int:pk>/edit/', views.RackEditView.as_view(), name='rack_edit'),
path(r'racks/<int:pk>/delete/', views.RackDeleteView.as_view(), name='rack_delete'), path('racks/<int:pk>/delete/', views.RackDeleteView.as_view(), name='rack_delete'),
path(r'racks/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='rack_changelog', kwargs={'model': Rack}), path('racks/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='rack_changelog', kwargs={'model': Rack}),
path(r'racks/<int:rack>/reservations/add/', views.RackReservationCreateView.as_view(), name='rack_add_reservation'), path('racks/<int:rack>/reservations/add/', views.RackReservationCreateView.as_view(), name='rack_add_reservation'),
path(r'racks/<int:object_id>/images/add/', ImageAttachmentEditView.as_view(), name='rack_add_image', kwargs={'model': Rack}), path('racks/<int:object_id>/images/add/', ImageAttachmentEditView.as_view(), name='rack_add_image', kwargs={'model': Rack}),
# Manufacturers # Manufacturers
path(r'manufacturers/', views.ManufacturerListView.as_view(), name='manufacturer_list'), path('manufacturers/', views.ManufacturerListView.as_view(), name='manufacturer_list'),
path(r'manufacturers/add/', views.ManufacturerCreateView.as_view(), name='manufacturer_add'), path('manufacturers/add/', views.ManufacturerCreateView.as_view(), name='manufacturer_add'),
path(r'manufacturers/import/', views.ManufacturerBulkImportView.as_view(), name='manufacturer_import'), path('manufacturers/import/', views.ManufacturerBulkImportView.as_view(), name='manufacturer_import'),
path(r'manufacturers/delete/', views.ManufacturerBulkDeleteView.as_view(), name='manufacturer_bulk_delete'), path('manufacturers/delete/', views.ManufacturerBulkDeleteView.as_view(), name='manufacturer_bulk_delete'),
path(r'manufacturers/<slug:slug>/edit/', views.ManufacturerEditView.as_view(), name='manufacturer_edit'), path('manufacturers/<slug:slug>/edit/', views.ManufacturerEditView.as_view(), name='manufacturer_edit'),
path(r'manufacturers/<slug:slug>/changelog/', ObjectChangeLogView.as_view(), name='manufacturer_changelog', kwargs={'model': Manufacturer}), path('manufacturers/<slug:slug>/changelog/', ObjectChangeLogView.as_view(), name='manufacturer_changelog', kwargs={'model': Manufacturer}),
# Device types # Device types
path(r'device-types/', views.DeviceTypeListView.as_view(), name='devicetype_list'), path('device-types/', views.DeviceTypeListView.as_view(), name='devicetype_list'),
path(r'device-types/add/', views.DeviceTypeCreateView.as_view(), name='devicetype_add'), path('device-types/add/', views.DeviceTypeCreateView.as_view(), name='devicetype_add'),
path(r'device-types/import/', views.DeviceTypeImportView.as_view(), name='devicetype_import'), path('device-types/import/', views.DeviceTypeImportView.as_view(), name='devicetype_import'),
path(r'device-types/edit/', views.DeviceTypeBulkEditView.as_view(), name='devicetype_bulk_edit'), path('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('device-types/delete/', views.DeviceTypeBulkDeleteView.as_view(), name='devicetype_bulk_delete'),
path(r'device-types/<int:pk>/', views.DeviceTypeView.as_view(), name='devicetype'), path('device-types/<int:pk>/', views.DeviceTypeView.as_view(), name='devicetype'),
path(r'device-types/<int:pk>/edit/', views.DeviceTypeEditView.as_view(), name='devicetype_edit'), path('device-types/<int:pk>/edit/', views.DeviceTypeEditView.as_view(), name='devicetype_edit'),
path(r'device-types/<int:pk>/delete/', views.DeviceTypeDeleteView.as_view(), name='devicetype_delete'), path('device-types/<int:pk>/delete/', views.DeviceTypeDeleteView.as_view(), name='devicetype_delete'),
path(r'device-types/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='devicetype_changelog', kwargs={'model': DeviceType}), path('device-types/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='devicetype_changelog', kwargs={'model': DeviceType}),
# Console port templates # Console port templates
path(r'device-types/<int:pk>/console-ports/add/', views.ConsolePortTemplateCreateView.as_view(), name='devicetype_add_consoleport'), path('console-port-templates/add/', views.ConsolePortTemplateCreateView.as_view(), name='consoleporttemplate_add'),
path(r'device-types/<int:pk>/console-ports/delete/', views.ConsolePortTemplateBulkDeleteView.as_view(), name='devicetype_delete_consoleport'), path('console-port-templates/edit/', views.ConsolePortTemplateBulkEditView.as_view(), name='consoleporttemplate_bulk_edit'),
path(r'console-port-templates/<int:pk>/edit/', views.ConsolePortTemplateEditView.as_view(), name='consoleporttemplate_edit'), path('console-port-templates/delete/', views.ConsolePortTemplateBulkDeleteView.as_view(), name='consoleporttemplate_bulk_delete'),
path('console-port-templates/<int:pk>/edit/', views.ConsolePortTemplateEditView.as_view(), name='consoleporttemplate_edit'),
path('console-port-templates/<int:pk>/delete/', views.ConsolePortTemplateDeleteView.as_view(), name='consoleporttemplate_delete'),
# Console server port templates # Console server port templates
path(r'device-types/<int:pk>/console-server-ports/add/', views.ConsoleServerPortTemplateCreateView.as_view(), name='devicetype_add_consoleserverport'), path('console-server-port-templates/add/', views.ConsoleServerPortTemplateCreateView.as_view(), name='consoleserverporttemplate_add'),
path(r'device-types/<int:pk>/console-server-ports/delete/', views.ConsoleServerPortTemplateBulkDeleteView.as_view(), name='devicetype_delete_consoleserverport'), path('console-server-port-templates/edit/', views.ConsoleServerPortTemplateBulkEditView.as_view(), name='consoleserverporttemplate_bulk_edit'),
path(r'console-server-port-templates/<int:pk>/edit/', views.ConsoleServerPortTemplateEditView.as_view(), name='consoleserverporttemplate_edit'), path('console-server-port-templates/delete/', views.ConsoleServerPortTemplateBulkDeleteView.as_view(), name='consoleserverporttemplate_bulk_delete'),
path('console-server-port-templates/<int:pk>/edit/', views.ConsoleServerPortTemplateEditView.as_view(), name='consoleserverporttemplate_edit'),
path('console-server-port-templates/<int:pk>/delete/', views.ConsoleServerPortTemplateDeleteView.as_view(), name='consoleserverporttemplate_delete'),
# Power port templates # Power port templates
path(r'device-types/<int:pk>/power-ports/add/', views.PowerPortTemplateCreateView.as_view(), name='devicetype_add_powerport'), path('power-port-templates/add/', views.PowerPortTemplateCreateView.as_view(), name='powerporttemplate_add'),
path(r'device-types/<int:pk>/power-ports/delete/', views.PowerPortTemplateBulkDeleteView.as_view(), name='devicetype_delete_powerport'), path('power-port-templates/edit/', views.PowerPortTemplateBulkEditView.as_view(), name='powerporttemplate_bulk_edit'),
path(r'power-port-templates/<int:pk>/edit/', views.PowerPortTemplateEditView.as_view(), name='powerporttemplate_edit'), path('power-port-templates/delete/', views.PowerPortTemplateBulkDeleteView.as_view(), name='powerporttemplate_bulk_delete'),
path('power-port-templates/<int:pk>/edit/', views.PowerPortTemplateEditView.as_view(), name='powerporttemplate_edit'),
path('power-port-templates/<int:pk>/delete/', views.PowerPortTemplateDeleteView.as_view(), name='powerporttemplate_delete'),
# Power outlet templates # Power outlet templates
path(r'device-types/<int:pk>/power-outlets/add/', views.PowerOutletTemplateCreateView.as_view(), name='devicetype_add_poweroutlet'), path('power-outlet-templates/add/', views.PowerOutletTemplateCreateView.as_view(), name='poweroutlettemplate_add'),
path(r'device-types/<int:pk>/power-outlets/delete/', views.PowerOutletTemplateBulkDeleteView.as_view(), name='devicetype_delete_poweroutlet'), path('power-outlet-templates/edit/', views.PowerOutletTemplateBulkEditView.as_view(), name='poweroutlettemplate_bulk_edit'),
path(r'power-outlet-templates/<int:pk>/edit/', views.PowerOutletTemplateEditView.as_view(), name='poweroutlettemplate_edit'), path('power-outlet-templates/delete/', views.PowerOutletTemplateBulkDeleteView.as_view(), name='poweroutlettemplate_bulk_delete'),
path('power-outlet-templates/<int:pk>/edit/', views.PowerOutletTemplateEditView.as_view(), name='poweroutlettemplate_edit'),
path('power-outlet-templates/<int:pk>/delete/', views.PowerOutletTemplateDeleteView.as_view(), name='poweroutlettemplate_delete'),
# Interface templates # Interface templates
path(r'device-types/<int:pk>/interfaces/add/', views.InterfaceTemplateCreateView.as_view(), name='devicetype_add_interface'), path('interface-templates/add/', views.InterfaceTemplateCreateView.as_view(), name='interfacetemplate_add'),
path(r'device-types/<int:pk>/interfaces/edit/', views.InterfaceTemplateBulkEditView.as_view(), name='devicetype_bulkedit_interface'), path('interface-templates/edit/', views.InterfaceTemplateBulkEditView.as_view(), name='interfacetemplate_bulk_edit'),
path(r'device-types/<int:pk>/interfaces/delete/', views.InterfaceTemplateBulkDeleteView.as_view(), name='devicetype_delete_interface'), path('interface-templates/delete/', views.InterfaceTemplateBulkDeleteView.as_view(), name='interfacetemplate_bulk_delete'),
path(r'interface-templates/<int:pk>/edit/', views.InterfaceTemplateEditView.as_view(), name='interfacetemplate_edit'), path('interface-templates/<int:pk>/edit/', views.InterfaceTemplateEditView.as_view(), name='interfacetemplate_edit'),
path('interface-templates/<int:pk>/delete/', views.InterfaceTemplateDeleteView.as_view(), name='interfacetemplate_delete'),
# Front port templates # Front port templates
path(r'device-types/<int:pk>/front-ports/add/', views.FrontPortTemplateCreateView.as_view(), name='devicetype_add_frontport'), path('front-port-templates/add/', views.FrontPortTemplateCreateView.as_view(), name='frontporttemplate_add'),
path(r'device-types/<int:pk>/front-ports/delete/', views.FrontPortTemplateBulkDeleteView.as_view(), name='devicetype_delete_frontport'), path('front-port-templates/edit/', views.FrontPortTemplateBulkEditView.as_view(), name='frontporttemplate_bulk_edit'),
path(r'front-port-templates/<int:pk>/edit/', views.FrontPortTemplateEditView.as_view(), name='frontporttemplate_edit'), path('front-port-templates/delete/', views.FrontPortTemplateBulkDeleteView.as_view(), name='frontporttemplate_bulk_delete'),
path('front-port-templates/<int:pk>/edit/', views.FrontPortTemplateEditView.as_view(), name='frontporttemplate_edit'),
path('front-port-templates/<int:pk>/delete/', views.FrontPortTemplateDeleteView.as_view(), name='frontporttemplate_delete'),
# Rear port templates # Rear port templates
path(r'device-types/<int:pk>/rear-ports/add/', views.RearPortTemplateCreateView.as_view(), name='devicetype_add_rearport'), path('rear-port-templates/add/', views.RearPortTemplateCreateView.as_view(), name='rearporttemplate_add'),
path(r'device-types/<int:pk>/rear-ports/delete/', views.RearPortTemplateBulkDeleteView.as_view(), name='devicetype_delete_rearport'), path('rear-port-templates/edit/', views.RearPortTemplateBulkEditView.as_view(), name='rearporttemplate_bulk_edit'),
path(r'rear-port-templates/<int:pk>/edit/', views.RearPortTemplateEditView.as_view(), name='rearporttemplate_edit'), path('rear-port-templates/delete/', views.RearPortTemplateBulkDeleteView.as_view(), name='rearporttemplate_bulk_delete'),
path('rear-port-templates/<int:pk>/edit/', views.RearPortTemplateEditView.as_view(), name='rearporttemplate_edit'),
path('rear-port-templates/<int:pk>/delete/', views.RearPortTemplateDeleteView.as_view(), name='rearporttemplate_delete'),
# Device bay templates # Device bay templates
path(r'device-types/<int:pk>/device-bays/add/', views.DeviceBayTemplateCreateView.as_view(), name='devicetype_add_devicebay'), path('device-bay-templates/add/', views.DeviceBayTemplateCreateView.as_view(), name='devicebaytemplate_add'),
path(r'device-types/<int:pk>/device-bays/delete/', views.DeviceBayTemplateBulkDeleteView.as_view(), name='devicetype_delete_devicebay'), # path('device-bay-templates/edit/', views.DeviceBayTemplateBulkEditView.as_view(), name='devicebaytemplate_bulk_edit'),
path(r'device-bay-templates/<int:pk>/edit/', views.DeviceBayTemplateEditView.as_view(), name='devicebaytemplate_edit'), path('device-bay-templates/delete/', views.DeviceBayTemplateBulkDeleteView.as_view(), name='devicebaytemplate_bulk_delete'),
path('device-bay-templates/<int:pk>/edit/', views.DeviceBayTemplateEditView.as_view(), name='devicebaytemplate_edit'),
path('device-bay-templates/<int:pk>/delete/', views.DeviceBayTemplateDeleteView.as_view(), name='devicebaytemplate_delete'),
# Device roles # Device roles
path(r'device-roles/', views.DeviceRoleListView.as_view(), name='devicerole_list'), path('device-roles/', views.DeviceRoleListView.as_view(), name='devicerole_list'),
path(r'device-roles/add/', views.DeviceRoleCreateView.as_view(), name='devicerole_add'), path('device-roles/add/', views.DeviceRoleCreateView.as_view(), name='devicerole_add'),
path(r'device-roles/import/', views.DeviceRoleBulkImportView.as_view(), name='devicerole_import'), path('device-roles/import/', views.DeviceRoleBulkImportView.as_view(), name='devicerole_import'),
path(r'device-roles/delete/', views.DeviceRoleBulkDeleteView.as_view(), name='devicerole_bulk_delete'), path('device-roles/delete/', views.DeviceRoleBulkDeleteView.as_view(), name='devicerole_bulk_delete'),
path(r'device-roles/<slug:slug>/edit/', views.DeviceRoleEditView.as_view(), name='devicerole_edit'), path('device-roles/<slug:slug>/edit/', views.DeviceRoleEditView.as_view(), name='devicerole_edit'),
path(r'device-roles/<slug:slug>/changelog/', ObjectChangeLogView.as_view(), name='devicerole_changelog', kwargs={'model': DeviceRole}), path('device-roles/<slug:slug>/changelog/', ObjectChangeLogView.as_view(), name='devicerole_changelog', kwargs={'model': DeviceRole}),
# Platforms # Platforms
path(r'platforms/', views.PlatformListView.as_view(), name='platform_list'), path('platforms/', views.PlatformListView.as_view(), name='platform_list'),
path(r'platforms/add/', views.PlatformCreateView.as_view(), name='platform_add'), path('platforms/add/', views.PlatformCreateView.as_view(), name='platform_add'),
path(r'platforms/import/', views.PlatformBulkImportView.as_view(), name='platform_import'), path('platforms/import/', views.PlatformBulkImportView.as_view(), name='platform_import'),
path(r'platforms/delete/', views.PlatformBulkDeleteView.as_view(), name='platform_bulk_delete'), path('platforms/delete/', views.PlatformBulkDeleteView.as_view(), name='platform_bulk_delete'),
path(r'platforms/<slug:slug>/edit/', views.PlatformEditView.as_view(), name='platform_edit'), path('platforms/<slug:slug>/edit/', views.PlatformEditView.as_view(), name='platform_edit'),
path(r'platforms/<slug:slug>/changelog/', ObjectChangeLogView.as_view(), name='platform_changelog', kwargs={'model': Platform}), path('platforms/<slug:slug>/changelog/', ObjectChangeLogView.as_view(), name='platform_changelog', kwargs={'model': Platform}),
# Devices # Devices
path(r'devices/', views.DeviceListView.as_view(), name='device_list'), path('devices/', views.DeviceListView.as_view(), name='device_list'),
path(r'devices/add/', views.DeviceCreateView.as_view(), name='device_add'), path('devices/add/', views.DeviceCreateView.as_view(), name='device_add'),
path(r'devices/import/', views.DeviceBulkImportView.as_view(), name='device_import'), path('devices/import/', views.DeviceBulkImportView.as_view(), name='device_import'),
path(r'devices/import/child-devices/', views.ChildDeviceBulkImportView.as_view(), name='device_import_child'), path('devices/import/child-devices/', views.ChildDeviceBulkImportView.as_view(), name='device_import_child'),
path(r'devices/edit/', views.DeviceBulkEditView.as_view(), name='device_bulk_edit'), path('devices/edit/', views.DeviceBulkEditView.as_view(), name='device_bulk_edit'),
path(r'devices/delete/', views.DeviceBulkDeleteView.as_view(), name='device_bulk_delete'), path('devices/delete/', views.DeviceBulkDeleteView.as_view(), name='device_bulk_delete'),
path(r'devices/<int:pk>/', views.DeviceView.as_view(), name='device'), path('devices/<int:pk>/', views.DeviceView.as_view(), name='device'),
path(r'devices/<int:pk>/edit/', views.DeviceEditView.as_view(), name='device_edit'), path('devices/<int:pk>/edit/', views.DeviceEditView.as_view(), name='device_edit'),
path(r'devices/<int:pk>/delete/', views.DeviceDeleteView.as_view(), name='device_delete'), path('devices/<int:pk>/delete/', views.DeviceDeleteView.as_view(), name='device_delete'),
path(r'devices/<int:pk>/config-context/', views.DeviceConfigContextView.as_view(), name='device_configcontext'), path('devices/<int:pk>/config-context/', views.DeviceConfigContextView.as_view(), name='device_configcontext'),
path(r'devices/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='device_changelog', kwargs={'model': Device}), path('devices/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='device_changelog', kwargs={'model': Device}),
path(r'devices/<int:pk>/inventory/', views.DeviceInventoryView.as_view(), name='device_inventory'), path('devices/<int:pk>/inventory/', views.DeviceInventoryView.as_view(), name='device_inventory'),
path(r'devices/<int:pk>/status/', views.DeviceStatusView.as_view(), name='device_status'), path('devices/<int:pk>/status/', views.DeviceStatusView.as_view(), name='device_status'),
path(r'devices/<int:pk>/lldp-neighbors/', views.DeviceLLDPNeighborsView.as_view(), name='device_lldp_neighbors'), path('devices/<int:pk>/lldp-neighbors/', views.DeviceLLDPNeighborsView.as_view(), name='device_lldp_neighbors'),
path(r'devices/<int:pk>/config/', views.DeviceConfigView.as_view(), name='device_config'), path('devices/<int:pk>/config/', views.DeviceConfigView.as_view(), name='device_config'),
path(r'devices/<int:pk>/add-secret/', secret_add, name='device_addsecret'), path('devices/<int:pk>/add-secret/', secret_add, name='device_addsecret'),
path(r'devices/<int:device>/services/assign/', ServiceCreateView.as_view(), name='device_service_assign'), path('devices/<int:device>/services/assign/', ServiceCreateView.as_view(), name='device_service_assign'),
path(r'devices/<int:object_id>/images/add/', ImageAttachmentEditView.as_view(), name='device_add_image', kwargs={'model': Device}), path('devices/<int:object_id>/images/add/', ImageAttachmentEditView.as_view(), name='device_add_image', kwargs={'model': Device}),
# Console ports # Console ports
path(r'devices/console-ports/add/', views.DeviceBulkAddConsolePortView.as_view(), name='device_bulk_add_consoleport'), path('console-ports/', views.ConsolePortListView.as_view(), name='consoleport_list'),
path(r'devices/<int:pk>/console-ports/add/', views.ConsolePortCreateView.as_view(), name='consoleport_add'), path('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('console-ports/import/', views.ConsolePortBulkImportView.as_view(), name='consoleport_import'),
path(r'console-ports/', views.ConsolePortListView.as_view(), name='consoleport_list'), path('console-ports/edit/', views.ConsolePortBulkEditView.as_view(), name='consoleport_bulk_edit'),
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}), # TODO: Bulk rename, disconnect views for ConsolePorts
path(r'console-ports/<int:pk>/edit/', views.ConsolePortEditView.as_view(), name='consoleport_edit'), path('console-ports/delete/', views.ConsolePortBulkDeleteView.as_view(), name='consoleport_bulk_delete'),
path(r'console-ports/<int:pk>/delete/', views.ConsolePortDeleteView.as_view(), name='consoleport_delete'), path('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>/trace/', views.CableTraceView.as_view(), name='consoleport_trace', kwargs={'model': ConsolePort}), path('console-ports/<int:pk>/edit/', views.ConsolePortEditView.as_view(), name='consoleport_edit'),
path(r'console-ports/import/', views.ConsolePortBulkImportView.as_view(), name='consoleport_import'), path('console-ports/<int:pk>/delete/', views.ConsolePortDeleteView.as_view(), name='consoleport_delete'),
path('console-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='consoleport_trace', kwargs={'model': ConsolePort}),
path('devices/console-ports/add/', views.DeviceBulkAddConsolePortView.as_view(), name='device_bulk_add_consoleport'),
# Console server ports # Console server ports
path(r'devices/console-server-ports/add/', views.DeviceBulkAddConsoleServerPortView.as_view(), name='device_bulk_add_consoleserverport'), path('console-server-ports/', views.ConsoleServerPortListView.as_view(), name='consoleserverport_list'),
path(r'devices/<int:pk>/console-server-ports/add/', views.ConsoleServerPortCreateView.as_view(), name='consoleserverport_add'), path('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('console-server-ports/import/', views.ConsoleServerPortBulkImportView.as_view(), name='consoleserverport_import'),
path(r'devices/<int:pk>/console-server-ports/delete/', views.ConsoleServerPortBulkDeleteView.as_view(), name='consoleserverport_bulk_delete'), path('console-server-ports/edit/', views.ConsoleServerPortBulkEditView.as_view(), name='consoleserverport_bulk_edit'),
path(r'console-server-ports/', views.ConsoleServerPortListView.as_view(), name='consoleserverport_list'), path('console-server-ports/rename/', views.ConsoleServerPortBulkRenameView.as_view(), name='consoleserverport_bulk_rename'),
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('console-server-ports/disconnect/', views.ConsoleServerPortBulkDisconnectView.as_view(), name='consoleserverport_bulk_disconnect'),
path(r'console-server-ports/<int:pk>/edit/', views.ConsoleServerPortEditView.as_view(), name='consoleserverport_edit'), path('console-server-ports/delete/', views.ConsoleServerPortBulkDeleteView.as_view(), name='consoleserverport_bulk_delete'),
path(r'console-server-ports/<int:pk>/delete/', views.ConsoleServerPortDeleteView.as_view(), name='consoleserverport_delete'), path('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>/trace/', views.CableTraceView.as_view(), name='consoleserverport_trace', kwargs={'model': ConsoleServerPort}), path('console-server-ports/<int:pk>/edit/', views.ConsoleServerPortEditView.as_view(), name='consoleserverport_edit'),
path(r'console-server-ports/rename/', views.ConsoleServerPortBulkRenameView.as_view(), name='consoleserverport_bulk_rename'), path('console-server-ports/<int:pk>/delete/', views.ConsoleServerPortDeleteView.as_view(), name='consoleserverport_delete'),
path(r'console-server-ports/disconnect/', views.ConsoleServerPortBulkDisconnectView.as_view(), name='consoleserverport_bulk_disconnect'), path('console-server-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='consoleserverport_trace', kwargs={'model': ConsoleServerPort}),
path(r'console-server-ports/import/', views.ConsoleServerPortBulkImportView.as_view(), name='consoleserverport_import'), path('devices/console-server-ports/add/', views.DeviceBulkAddConsoleServerPortView.as_view(), name='device_bulk_add_consoleserverport'),
# Power ports # Power ports
path(r'devices/power-ports/add/', views.DeviceBulkAddPowerPortView.as_view(), name='device_bulk_add_powerport'), path('power-ports/', views.PowerPortListView.as_view(), name='powerport_list'),
path(r'devices/<int:pk>/power-ports/add/', views.PowerPortCreateView.as_view(), name='powerport_add'), path('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('power-ports/import/', views.PowerPortBulkImportView.as_view(), name='powerport_import'),
path(r'power-ports/', views.PowerPortListView.as_view(), name='powerport_list'), path('power-ports/edit/', views.PowerPortBulkEditView.as_view(), name='powerport_bulk_edit'),
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}), # TODO: Bulk rename, disconnect views for PowerPorts
path(r'power-ports/<int:pk>/edit/', views.PowerPortEditView.as_view(), name='powerport_edit'), path('power-ports/delete/', views.PowerPortBulkDeleteView.as_view(), name='powerport_bulk_delete'),
path(r'power-ports/<int:pk>/delete/', views.PowerPortDeleteView.as_view(), name='powerport_delete'), path('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>/trace/', views.CableTraceView.as_view(), name='powerport_trace', kwargs={'model': PowerPort}), path('power-ports/<int:pk>/edit/', views.PowerPortEditView.as_view(), name='powerport_edit'),
path(r'power-ports/import/', views.PowerPortBulkImportView.as_view(), name='powerport_import'), path('power-ports/<int:pk>/delete/', views.PowerPortDeleteView.as_view(), name='powerport_delete'),
path('power-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='powerport_trace', kwargs={'model': PowerPort}),
path('devices/power-ports/add/', views.DeviceBulkAddPowerPortView.as_view(), name='device_bulk_add_powerport'),
# Power outlets # Power outlets
path(r'devices/power-outlets/add/', views.DeviceBulkAddPowerOutletView.as_view(), name='device_bulk_add_poweroutlet'), path('power-outlets/', views.PowerOutletListView.as_view(), name='poweroutlet_list'),
path(r'devices/<int:pk>/power-outlets/add/', views.PowerOutletCreateView.as_view(), name='poweroutlet_add'), path('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('power-outlets/import/', views.PowerOutletBulkImportView.as_view(), name='poweroutlet_import'),
path(r'devices/<int:pk>/power-outlets/delete/', views.PowerOutletBulkDeleteView.as_view(), name='poweroutlet_bulk_delete'), path('power-outlets/edit/', views.PowerOutletBulkEditView.as_view(), name='poweroutlet_bulk_edit'),
path(r'power-outlets/', views.PowerOutletListView.as_view(), name='poweroutlet_list'), path('power-outlets/rename/', views.PowerOutletBulkRenameView.as_view(), name='poweroutlet_bulk_rename'),
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('power-outlets/disconnect/', views.PowerOutletBulkDisconnectView.as_view(), name='poweroutlet_bulk_disconnect'),
path(r'power-outlets/<int:pk>/edit/', views.PowerOutletEditView.as_view(), name='poweroutlet_edit'), path('power-outlets/delete/', views.PowerOutletBulkDeleteView.as_view(), name='poweroutlet_bulk_delete'),
path(r'power-outlets/<int:pk>/delete/', views.PowerOutletDeleteView.as_view(), name='poweroutlet_delete'), path('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>/trace/', views.CableTraceView.as_view(), name='poweroutlet_trace', kwargs={'model': PowerOutlet}), path('power-outlets/<int:pk>/edit/', views.PowerOutletEditView.as_view(), name='poweroutlet_edit'),
path(r'power-outlets/rename/', views.PowerOutletBulkRenameView.as_view(), name='poweroutlet_bulk_rename'), path('power-outlets/<int:pk>/delete/', views.PowerOutletDeleteView.as_view(), name='poweroutlet_delete'),
path(r'power-outlets/disconnect/', views.PowerOutletBulkDisconnectView.as_view(), name='poweroutlet_bulk_disconnect'), path('power-outlets/<int:pk>/trace/', views.CableTraceView.as_view(), name='poweroutlet_trace', kwargs={'model': PowerOutlet}),
path(r'power-outlets/import/', views.PowerOutletBulkImportView.as_view(), name='poweroutlet_import'), path('devices/power-outlets/add/', views.DeviceBulkAddPowerOutletView.as_view(), name='device_bulk_add_poweroutlet'),
# Interfaces # Interfaces
path(r'devices/interfaces/add/', views.DeviceBulkAddInterfaceView.as_view(), name='device_bulk_add_interface'), path('interfaces/', views.InterfaceListView.as_view(), name='interface_list'),
path(r'devices/<int:pk>/interfaces/add/', views.InterfaceCreateView.as_view(), name='interface_add'), path('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('interfaces/import/', views.InterfaceBulkImportView.as_view(), name='interface_import'),
path(r'devices/<int:pk>/interfaces/delete/', views.InterfaceBulkDeleteView.as_view(), name='interface_bulk_delete'), path('interfaces/edit/', views.InterfaceBulkEditView.as_view(), name='interface_bulk_edit'),
path(r'interfaces/', views.InterfaceListView.as_view(), name='interface_list'), path('interfaces/rename/', views.InterfaceBulkRenameView.as_view(), name='interface_bulk_rename'),
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('interfaces/disconnect/', views.InterfaceBulkDisconnectView.as_view(), name='interface_bulk_disconnect'),
path(r'interfaces/<int:pk>/', views.InterfaceView.as_view(), name='interface'), path('interfaces/delete/', views.InterfaceBulkDeleteView.as_view(), name='interface_bulk_delete'),
path(r'interfaces/<int:pk>/edit/', views.InterfaceEditView.as_view(), name='interface_edit'), path('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>/delete/', views.InterfaceDeleteView.as_view(), name='interface_delete'), path('interfaces/<int:pk>/', views.InterfaceView.as_view(), name='interface'),
path(r'interfaces/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='interface_changelog', kwargs={'model': Interface}), path('interfaces/<int:pk>/edit/', views.InterfaceEditView.as_view(), name='interface_edit'),
path(r'interfaces/<int:pk>/trace/', views.CableTraceView.as_view(), name='interface_trace', kwargs={'model': Interface}), path('interfaces/<int:pk>/delete/', views.InterfaceDeleteView.as_view(), name='interface_delete'),
path(r'interfaces/rename/', views.InterfaceBulkRenameView.as_view(), name='interface_bulk_rename'), path('interfaces/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='interface_changelog', kwargs={'model': Interface}),
path(r'interfaces/disconnect/', views.InterfaceBulkDisconnectView.as_view(), name='interface_bulk_disconnect'), path('interfaces/<int:pk>/trace/', views.CableTraceView.as_view(), name='interface_trace', kwargs={'model': Interface}),
path(r'interfaces/import/', views.InterfaceBulkImportView.as_view(), name='interface_import'), path('devices/interfaces/add/', views.DeviceBulkAddInterfaceView.as_view(), name='device_bulk_add_interface'),
# Front ports # Front ports
# path(r'devices/front-ports/add/', views.DeviceBulkAddFrontPortView.as_view(), name='device_bulk_add_frontport'), path('front-ports/', views.FrontPortListView.as_view(), name='frontport_list'),
path(r'devices/<int:pk>/front-ports/add/', views.FrontPortCreateView.as_view(), name='frontport_add'), path('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('front-ports/import/', views.FrontPortBulkImportView.as_view(), name='frontport_import'),
path(r'devices/<int:pk>/front-ports/delete/', views.FrontPortBulkDeleteView.as_view(), name='frontport_bulk_delete'), path('front-ports/edit/', views.FrontPortBulkEditView.as_view(), name='frontport_bulk_edit'),
path(r'front-ports/', views.FrontPortListView.as_view(), name='frontport_list'), path('front-ports/rename/', views.FrontPortBulkRenameView.as_view(), name='frontport_bulk_rename'),
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('front-ports/disconnect/', views.FrontPortBulkDisconnectView.as_view(), name='frontport_bulk_disconnect'),
path(r'front-ports/<int:pk>/edit/', views.FrontPortEditView.as_view(), name='frontport_edit'), path('front-ports/delete/', views.FrontPortBulkDeleteView.as_view(), name='frontport_bulk_delete'),
path(r'front-ports/<int:pk>/delete/', views.FrontPortDeleteView.as_view(), name='frontport_delete'), path('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>/trace/', views.CableTraceView.as_view(), name='frontport_trace', kwargs={'model': FrontPort}), path('front-ports/<int:pk>/edit/', views.FrontPortEditView.as_view(), name='frontport_edit'),
path(r'front-ports/rename/', views.FrontPortBulkRenameView.as_view(), name='frontport_bulk_rename'), path('front-ports/<int:pk>/delete/', views.FrontPortDeleteView.as_view(), name='frontport_delete'),
path(r'front-ports/disconnect/', views.FrontPortBulkDisconnectView.as_view(), name='frontport_bulk_disconnect'), path('front-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='frontport_trace', kwargs={'model': FrontPort}),
path(r'front-ports/import/', views.FrontPortBulkImportView.as_view(), name='frontport_import'), # path('devices/front-ports/add/', views.DeviceBulkAddFrontPortView.as_view(), name='device_bulk_add_frontport'),
# Rear ports # Rear ports
# path(r'devices/rear-ports/add/', views.DeviceBulkAddRearPortView.as_view(), name='device_bulk_add_rearport'), path('rear-ports/', views.RearPortListView.as_view(), name='rearport_list'),
path(r'devices/<int:pk>/rear-ports/add/', views.RearPortCreateView.as_view(), name='rearport_add'), path('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('rear-ports/import/', views.RearPortBulkImportView.as_view(), name='rearport_import'),
path(r'devices/<int:pk>/rear-ports/delete/', views.RearPortBulkDeleteView.as_view(), name='rearport_bulk_delete'), path('rear-ports/edit/', views.RearPortBulkEditView.as_view(), name='rearport_bulk_edit'),
path(r'rear-ports/', views.RearPortListView.as_view(), name='rearport_list'), path('rear-ports/rename/', views.RearPortBulkRenameView.as_view(), name='rearport_bulk_rename'),
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('rear-ports/disconnect/', views.RearPortBulkDisconnectView.as_view(), name='rearport_bulk_disconnect'),
path(r'rear-ports/<int:pk>/edit/', views.RearPortEditView.as_view(), name='rearport_edit'), path('rear-ports/delete/', views.RearPortBulkDeleteView.as_view(), name='rearport_bulk_delete'),
path(r'rear-ports/<int:pk>/delete/', views.RearPortDeleteView.as_view(), name='rearport_delete'), path('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>/trace/', views.CableTraceView.as_view(), name='rearport_trace', kwargs={'model': RearPort}), path('rear-ports/<int:pk>/edit/', views.RearPortEditView.as_view(), name='rearport_edit'),
path(r'rear-ports/rename/', views.RearPortBulkRenameView.as_view(), name='rearport_bulk_rename'), path('rear-ports/<int:pk>/delete/', views.RearPortDeleteView.as_view(), name='rearport_delete'),
path(r'rear-ports/disconnect/', views.RearPortBulkDisconnectView.as_view(), name='rearport_bulk_disconnect'), path('rear-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='rearport_trace', kwargs={'model': RearPort}),
path(r'rear-ports/import/', views.RearPortBulkImportView.as_view(), name='rearport_import'), # path('devices/rear-ports/add/', views.DeviceBulkAddRearPortView.as_view(), name='device_bulk_add_rearport'),
# Device bays # Device bays
path(r'devices/device-bays/add/', views.DeviceBulkAddDeviceBayView.as_view(), name='device_bulk_add_devicebay'), path('device-bays/', views.DeviceBayListView.as_view(), name='devicebay_list'),
path(r'devices/<int:pk>/bays/add/', views.DeviceBayCreateView.as_view(), name='devicebay_add'), path('device-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('device-bays/import/', views.DeviceBayBulkImportView.as_view(), name='devicebay_import'),
path(r'device-bays/', views.DeviceBayListView.as_view(), name='devicebay_list'), # TODO: Bulk edit view for DeviceBays
path(r'device-bays/<int:pk>/edit/', views.DeviceBayEditView.as_view(), name='devicebay_edit'), path('device-bays/rename/', views.DeviceBayBulkRenameView.as_view(), name='devicebay_bulk_rename'),
path(r'device-bays/<int:pk>/delete/', views.DeviceBayDeleteView.as_view(), name='devicebay_delete'), path('device-bays/delete/', views.DeviceBayBulkDeleteView.as_view(), name='devicebay_bulk_delete'),
path(r'device-bays/<int:pk>/populate/', views.DeviceBayPopulateView.as_view(), name='devicebay_populate'), path('device-bays/<int:pk>/edit/', views.DeviceBayEditView.as_view(), name='devicebay_edit'),
path(r'device-bays/<int:pk>/depopulate/', views.DeviceBayDepopulateView.as_view(), name='devicebay_depopulate'), path('device-bays/<int:pk>/delete/', views.DeviceBayDeleteView.as_view(), name='devicebay_delete'),
path(r'device-bays/rename/', views.DeviceBayBulkRenameView.as_view(), name='devicebay_bulk_rename'), path('device-bays/<int:pk>/populate/', views.DeviceBayPopulateView.as_view(), name='devicebay_populate'),
path(r'device-bays/import/', views.DeviceBayBulkImportView.as_view(), name='devicebay_import'), path('device-bays/<int:pk>/depopulate/', views.DeviceBayDepopulateView.as_view(), name='devicebay_depopulate'),
path('devices/device-bays/add/', views.DeviceBulkAddDeviceBayView.as_view(), name='device_bulk_add_devicebay'),
# Inventory items # Inventory items
path(r'inventory-items/', views.InventoryItemListView.as_view(), name='inventoryitem_list'), path('inventory-items/', views.InventoryItemListView.as_view(), name='inventoryitem_list'),
path(r'inventory-items/import/', views.InventoryItemBulkImportView.as_view(), name='inventoryitem_import'), path('inventory-items/add/', views.InventoryItemCreateView.as_view(), name='inventoryitem_add'),
path(r'inventory-items/edit/', views.InventoryItemBulkEditView.as_view(), name='inventoryitem_bulk_edit'), path('inventory-items/import/', views.InventoryItemBulkImportView.as_view(), name='inventoryitem_import'),
path(r'inventory-items/delete/', views.InventoryItemBulkDeleteView.as_view(), name='inventoryitem_bulk_delete'), path('inventory-items/edit/', views.InventoryItemBulkEditView.as_view(), name='inventoryitem_bulk_edit'),
path(r'inventory-items/<int:pk>/edit/', views.InventoryItemEditView.as_view(), name='inventoryitem_edit'), # TODO: Bulk rename view for InventoryItems
path(r'inventory-items/<int:pk>/delete/', views.InventoryItemDeleteView.as_view(), name='inventoryitem_delete'), path('inventory-items/delete/', views.InventoryItemBulkDeleteView.as_view(), name='inventoryitem_bulk_delete'),
path(r'devices/<int:device>/inventory-items/add/', views.InventoryItemEditView.as_view(), name='inventoryitem_add'), path('inventory-items/<int:pk>/edit/', views.InventoryItemEditView.as_view(), name='inventoryitem_edit'),
path('inventory-items/<int:pk>/delete/', views.InventoryItemDeleteView.as_view(), name='inventoryitem_delete'),
# Cables # Cables
path(r'cables/', views.CableListView.as_view(), name='cable_list'), path('cables/', views.CableListView.as_view(), name='cable_list'),
path(r'cables/import/', views.CableBulkImportView.as_view(), name='cable_import'), path('cables/import/', views.CableBulkImportView.as_view(), name='cable_import'),
path(r'cables/edit/', views.CableBulkEditView.as_view(), name='cable_bulk_edit'), path('cables/edit/', views.CableBulkEditView.as_view(), name='cable_bulk_edit'),
path(r'cables/delete/', views.CableBulkDeleteView.as_view(), name='cable_bulk_delete'), path('cables/delete/', views.CableBulkDeleteView.as_view(), name='cable_bulk_delete'),
path(r'cables/<int:pk>/', views.CableView.as_view(), name='cable'), path('cables/<int:pk>/', views.CableView.as_view(), name='cable'),
path(r'cables/<int:pk>/edit/', views.CableEditView.as_view(), name='cable_edit'), path('cables/<int:pk>/edit/', views.CableEditView.as_view(), name='cable_edit'),
path(r'cables/<int:pk>/delete/', views.CableDeleteView.as_view(), name='cable_delete'), path('cables/<int:pk>/delete/', views.CableDeleteView.as_view(), name='cable_delete'),
path(r'cables/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='cable_changelog', kwargs={'model': Cable}), path('cables/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='cable_changelog', kwargs={'model': Cable}),
# Console/power/interface connections (read-only) # Console/power/interface connections (read-only)
path(r'console-connections/', views.ConsoleConnectionsListView.as_view(), name='console_connections_list'), path('console-connections/', views.ConsoleConnectionsListView.as_view(), name='console_connections_list'),
path(r'power-connections/', views.PowerConnectionsListView.as_view(), name='power_connections_list'), path('power-connections/', views.PowerConnectionsListView.as_view(), name='power_connections_list'),
path(r'interface-connections/', views.InterfaceConnectionsListView.as_view(), name='interface_connections_list'), path('interface-connections/', views.InterfaceConnectionsListView.as_view(), name='interface_connections_list'),
# Virtual chassis # Virtual chassis
path(r'virtual-chassis/', views.VirtualChassisListView.as_view(), name='virtualchassis_list'), path('virtual-chassis/', views.VirtualChassisListView.as_view(), name='virtualchassis_list'),
path(r'virtual-chassis/add/', views.VirtualChassisCreateView.as_view(), name='virtualchassis_add'), path('virtual-chassis/add/', views.VirtualChassisCreateView.as_view(), name='virtualchassis_add'),
path(r'virtual-chassis/<int:pk>/edit/', views.VirtualChassisEditView.as_view(), name='virtualchassis_edit'), path('virtual-chassis/<int:pk>/edit/', views.VirtualChassisEditView.as_view(), name='virtualchassis_edit'),
path(r'virtual-chassis/<int:pk>/delete/', views.VirtualChassisDeleteView.as_view(), name='virtualchassis_delete'), path('virtual-chassis/<int:pk>/delete/', views.VirtualChassisDeleteView.as_view(), name='virtualchassis_delete'),
path(r'virtual-chassis/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='virtualchassis_changelog', kwargs={'model': VirtualChassis}), path('virtual-chassis/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='virtualchassis_changelog', kwargs={'model': VirtualChassis}),
path(r'virtual-chassis/<int:pk>/add-member/', views.VirtualChassisAddMemberView.as_view(), name='virtualchassis_add_member'), path('virtual-chassis/<int:pk>/add-member/', views.VirtualChassisAddMemberView.as_view(), name='virtualchassis_add_member'),
path(r'virtual-chassis-members/<int:pk>/delete/', views.VirtualChassisRemoveMemberView.as_view(), name='virtualchassis_remove_member'), path('virtual-chassis-members/<int:pk>/delete/', views.VirtualChassisRemoveMemberView.as_view(), name='virtualchassis_remove_member'),
# Power panels # Power panels
path(r'power-panels/', views.PowerPanelListView.as_view(), name='powerpanel_list'), path('power-panels/', views.PowerPanelListView.as_view(), name='powerpanel_list'),
path(r'power-panels/add/', views.PowerPanelCreateView.as_view(), name='powerpanel_add'), path('power-panels/add/', views.PowerPanelCreateView.as_view(), name='powerpanel_add'),
path(r'power-panels/import/', views.PowerPanelBulkImportView.as_view(), name='powerpanel_import'), path('power-panels/import/', views.PowerPanelBulkImportView.as_view(), name='powerpanel_import'),
path(r'power-panels/delete/', views.PowerPanelBulkDeleteView.as_view(), name='powerpanel_bulk_delete'), path('power-panels/delete/', views.PowerPanelBulkDeleteView.as_view(), name='powerpanel_bulk_delete'),
path(r'power-panels/<int:pk>/', views.PowerPanelView.as_view(), name='powerpanel'), path('power-panels/<int:pk>/', views.PowerPanelView.as_view(), name='powerpanel'),
path(r'power-panels/<int:pk>/edit/', views.PowerPanelEditView.as_view(), name='powerpanel_edit'), path('power-panels/<int:pk>/edit/', views.PowerPanelEditView.as_view(), name='powerpanel_edit'),
path(r'power-panels/<int:pk>/delete/', views.PowerPanelDeleteView.as_view(), name='powerpanel_delete'), path('power-panels/<int:pk>/delete/', views.PowerPanelDeleteView.as_view(), name='powerpanel_delete'),
path(r'power-panels/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='powerpanel_changelog', kwargs={'model': PowerPanel}), path('power-panels/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='powerpanel_changelog', kwargs={'model': PowerPanel}),
# Power feeds # Power feeds
path(r'power-feeds/', views.PowerFeedListView.as_view(), name='powerfeed_list'), path('power-feeds/', views.PowerFeedListView.as_view(), name='powerfeed_list'),
path(r'power-feeds/add/', views.PowerFeedEditView.as_view(), name='powerfeed_add'), path('power-feeds/add/', views.PowerFeedCreateView.as_view(), name='powerfeed_add'),
path(r'power-feeds/import/', views.PowerFeedBulkImportView.as_view(), name='powerfeed_import'), path('power-feeds/import/', views.PowerFeedBulkImportView.as_view(), name='powerfeed_import'),
path(r'power-feeds/edit/', views.PowerFeedBulkEditView.as_view(), name='powerfeed_bulk_edit'), path('power-feeds/edit/', views.PowerFeedBulkEditView.as_view(), name='powerfeed_bulk_edit'),
path(r'power-feeds/delete/', views.PowerFeedBulkDeleteView.as_view(), name='powerfeed_bulk_delete'), path('power-feeds/delete/', views.PowerFeedBulkDeleteView.as_view(), name='powerfeed_bulk_delete'),
path(r'power-feeds/<int:pk>/', views.PowerFeedView.as_view(), name='powerfeed'), path('power-feeds/<int:pk>/', views.PowerFeedView.as_view(), name='powerfeed'),
path(r'power-feeds/<int:pk>/edit/', views.PowerFeedEditView.as_view(), name='powerfeed_edit'), path('power-feeds/<int:pk>/edit/', views.PowerFeedEditView.as_view(), name='powerfeed_edit'),
path(r'power-feeds/<int:pk>/delete/', views.PowerFeedDeleteView.as_view(), name='powerfeed_delete'), path('power-feeds/<int:pk>/delete/', views.PowerFeedDeleteView.as_view(), name='powerfeed_delete'),
path(r'power-feeds/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='powerfeed_changelog', kwargs={'model': PowerFeed}), path('power-feeds/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='powerfeed_changelog', kwargs={'model': PowerFeed}),
] ]

View File

@ -152,7 +152,6 @@ class RegionListView(PermissionRequiredMixin, ObjectListView):
filterset = filters.RegionFilterSet filterset = filters.RegionFilterSet
filterset_form = forms.RegionFilterForm filterset_form = forms.RegionFilterForm
table = tables.RegionTable table = tables.RegionTable
template_name = 'dcim/region_list.html'
class RegionCreateView(PermissionRequiredMixin, ObjectEditView): class RegionCreateView(PermissionRequiredMixin, ObjectEditView):
@ -191,7 +190,6 @@ class SiteListView(PermissionRequiredMixin, ObjectListView):
filterset = filters.SiteFilterSet filterset = filters.SiteFilterSet
filterset_form = forms.SiteFilterForm filterset_form = forms.SiteFilterForm
table = tables.SiteTable table = tables.SiteTable
template_name = 'dcim/site_list.html'
class SiteView(PermissionRequiredMixin, View): class SiteView(PermissionRequiredMixin, View):
@ -271,7 +269,6 @@ class RackGroupListView(PermissionRequiredMixin, ObjectListView):
filterset = filters.RackGroupFilterSet filterset = filters.RackGroupFilterSet
filterset_form = forms.RackGroupFilterForm filterset_form = forms.RackGroupFilterForm
table = tables.RackGroupTable table = tables.RackGroupTable
template_name = 'dcim/rackgroup_list.html'
class RackGroupCreateView(PermissionRequiredMixin, ObjectEditView): class RackGroupCreateView(PermissionRequiredMixin, ObjectEditView):
@ -308,7 +305,6 @@ class RackRoleListView(PermissionRequiredMixin, ObjectListView):
permission_required = 'dcim.view_rackrole' permission_required = 'dcim.view_rackrole'
queryset = RackRole.objects.annotate(rack_count=Count('racks')) queryset = RackRole.objects.annotate(rack_count=Count('racks'))
table = tables.RackRoleTable table = tables.RackRoleTable
template_name = 'dcim/rackrole_list.html'
class RackRoleCreateView(PermissionRequiredMixin, ObjectEditView): class RackRoleCreateView(PermissionRequiredMixin, ObjectEditView):
@ -350,7 +346,6 @@ class RackListView(PermissionRequiredMixin, ObjectListView):
filterset = filters.RackFilterSet filterset = filters.RackFilterSet
filterset_form = forms.RackFilterForm filterset_form = forms.RackFilterForm
table = tables.RackDetailTable table = tables.RackDetailTable
template_name = 'dcim/rack_list.html'
class RackElevationListView(PermissionRequiredMixin, View): class RackElevationListView(PermissionRequiredMixin, View):
@ -474,7 +469,7 @@ class RackReservationListView(PermissionRequiredMixin, ObjectListView):
filterset = filters.RackReservationFilterSet filterset = filters.RackReservationFilterSet
filterset_form = forms.RackReservationFilterForm filterset_form = forms.RackReservationFilterForm
table = tables.RackReservationTable table = tables.RackReservationTable
template_name = 'dcim/rackreservation_list.html' action_buttons = ()
class RackReservationCreateView(PermissionRequiredMixin, ObjectEditView): class RackReservationCreateView(PermissionRequiredMixin, ObjectEditView):
@ -533,7 +528,6 @@ class ManufacturerListView(PermissionRequiredMixin, ObjectListView):
platform_count=Count('platforms', distinct=True), platform_count=Count('platforms', distinct=True),
) )
table = tables.ManufacturerTable table = tables.ManufacturerTable
template_name = 'dcim/manufacturer_list.html'
class ManufacturerCreateView(PermissionRequiredMixin, ObjectEditView): class ManufacturerCreateView(PermissionRequiredMixin, ObjectEditView):
@ -571,7 +565,6 @@ class DeviceTypeListView(PermissionRequiredMixin, ObjectListView):
filterset = filters.DeviceTypeFilterSet filterset = filters.DeviceTypeFilterSet
filterset_form = forms.DeviceTypeFilterForm filterset_form = forms.DeviceTypeFilterForm
table = tables.DeviceTypeTable table = tables.DeviceTypeTable
template_name = 'dcim/devicetype_list.html'
class DeviceTypeView(PermissionRequiredMixin, View): class DeviceTypeView(PermissionRequiredMixin, View):
@ -700,13 +693,11 @@ class DeviceTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
# #
# Device type components # Console port templates
# #
class ConsolePortTemplateCreateView(PermissionRequiredMixin, ComponentCreateView): class ConsolePortTemplateCreateView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_consoleporttemplate' permission_required = 'dcim.add_consoleporttemplate'
parent_model = DeviceType
parent_field = 'device_type'
model = ConsolePortTemplate model = ConsolePortTemplate
form = forms.ConsolePortTemplateCreateForm form = forms.ConsolePortTemplateCreateForm
model_form = forms.ConsolePortTemplateForm model_form = forms.ConsolePortTemplateForm
@ -719,17 +710,30 @@ class ConsolePortTemplateEditView(PermissionRequiredMixin, ObjectEditView):
model_form = forms.ConsolePortTemplateForm model_form = forms.ConsolePortTemplateForm
class ConsolePortTemplateDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'dcim.delete_consoleporttemplate'
model = ConsolePortTemplate
class ConsolePortTemplateBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.change_consoleporttemplate'
queryset = ConsolePortTemplate.objects.all()
table = tables.ConsolePortTemplateTable
form = forms.ConsolePortTemplateBulkEditForm
class ConsolePortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class ConsolePortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_consoleporttemplate' permission_required = 'dcim.delete_consoleporttemplate'
queryset = ConsolePortTemplate.objects.all() queryset = ConsolePortTemplate.objects.all()
parent_model = DeviceType
table = tables.ConsolePortTemplateTable table = tables.ConsolePortTemplateTable
#
# Console server port templates
#
class ConsoleServerPortTemplateCreateView(PermissionRequiredMixin, ComponentCreateView): class ConsoleServerPortTemplateCreateView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_consoleserverporttemplate' permission_required = 'dcim.add_consoleserverporttemplate'
parent_model = DeviceType
parent_field = 'device_type'
model = ConsoleServerPortTemplate model = ConsoleServerPortTemplate
form = forms.ConsoleServerPortTemplateCreateForm form = forms.ConsoleServerPortTemplateCreateForm
model_form = forms.ConsoleServerPortTemplateForm model_form = forms.ConsoleServerPortTemplateForm
@ -742,17 +746,30 @@ class ConsoleServerPortTemplateEditView(PermissionRequiredMixin, ObjectEditView)
model_form = forms.ConsoleServerPortTemplateForm model_form = forms.ConsoleServerPortTemplateForm
class ConsoleServerPortTemplateDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'dcim.delete_consoleserverporttemplate'
model = ConsoleServerPortTemplate
class ConsoleServerPortTemplateBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.change_consoleserverporttemplate'
queryset = ConsoleServerPortTemplate.objects.all()
table = tables.ConsoleServerPortTemplateTable
form = forms.ConsoleServerPortTemplateBulkEditForm
class ConsoleServerPortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class ConsoleServerPortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_consoleserverporttemplate' permission_required = 'dcim.delete_consoleserverporttemplate'
queryset = ConsoleServerPortTemplate.objects.all() queryset = ConsoleServerPortTemplate.objects.all()
parent_model = DeviceType
table = tables.ConsoleServerPortTemplateTable table = tables.ConsoleServerPortTemplateTable
#
# Power port templates
#
class PowerPortTemplateCreateView(PermissionRequiredMixin, ComponentCreateView): class PowerPortTemplateCreateView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_powerporttemplate' permission_required = 'dcim.add_powerporttemplate'
parent_model = DeviceType
parent_field = 'device_type'
model = PowerPortTemplate model = PowerPortTemplate
form = forms.PowerPortTemplateCreateForm form = forms.PowerPortTemplateCreateForm
model_form = forms.PowerPortTemplateForm model_form = forms.PowerPortTemplateForm
@ -765,17 +782,30 @@ class PowerPortTemplateEditView(PermissionRequiredMixin, ObjectEditView):
model_form = forms.PowerPortTemplateForm model_form = forms.PowerPortTemplateForm
class PowerPortTemplateDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'dcim.delete_powerporttemplate'
model = PowerPortTemplate
class PowerPortTemplateBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.change_powerporttemplate'
queryset = PowerPortTemplate.objects.all()
table = tables.PowerPortTemplateTable
form = forms.PowerPortTemplateBulkEditForm
class PowerPortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class PowerPortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_powerporttemplate' permission_required = 'dcim.delete_powerporttemplate'
queryset = PowerPortTemplate.objects.all() queryset = PowerPortTemplate.objects.all()
parent_model = DeviceType
table = tables.PowerPortTemplateTable table = tables.PowerPortTemplateTable
#
# Power outlet templates
#
class PowerOutletTemplateCreateView(PermissionRequiredMixin, ComponentCreateView): class PowerOutletTemplateCreateView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_poweroutlettemplate' permission_required = 'dcim.add_poweroutlettemplate'
parent_model = DeviceType
parent_field = 'device_type'
model = PowerOutletTemplate model = PowerOutletTemplate
form = forms.PowerOutletTemplateCreateForm form = forms.PowerOutletTemplateCreateForm
model_form = forms.PowerOutletTemplateForm model_form = forms.PowerOutletTemplateForm
@ -788,17 +818,30 @@ class PowerOutletTemplateEditView(PermissionRequiredMixin, ObjectEditView):
model_form = forms.PowerOutletTemplateForm model_form = forms.PowerOutletTemplateForm
class PowerOutletTemplateDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'dcim.delete_poweroutlettemplate'
model = PowerOutletTemplate
class PowerOutletTemplateBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.change_poweroutlettemplate'
queryset = PowerOutletTemplate.objects.all()
table = tables.PowerOutletTemplateTable
form = forms.PowerOutletTemplateBulkEditForm
class PowerOutletTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class PowerOutletTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_poweroutlettemplate' permission_required = 'dcim.delete_poweroutlettemplate'
queryset = PowerOutletTemplate.objects.all() queryset = PowerOutletTemplate.objects.all()
parent_model = DeviceType
table = tables.PowerOutletTemplateTable table = tables.PowerOutletTemplateTable
#
# Interface templates
#
class InterfaceTemplateCreateView(PermissionRequiredMixin, ComponentCreateView): class InterfaceTemplateCreateView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_interfacetemplate' permission_required = 'dcim.add_interfacetemplate'
parent_model = DeviceType
parent_field = 'device_type'
model = InterfaceTemplate model = InterfaceTemplate
form = forms.InterfaceTemplateCreateForm form = forms.InterfaceTemplateCreateForm
model_form = forms.InterfaceTemplateForm model_form = forms.InterfaceTemplateForm
@ -811,10 +854,14 @@ class InterfaceTemplateEditView(PermissionRequiredMixin, ObjectEditView):
model_form = forms.InterfaceTemplateForm model_form = forms.InterfaceTemplateForm
class InterfaceTemplateDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'dcim.delete_interfacetemplate'
model = InterfaceTemplate
class InterfaceTemplateBulkEditView(PermissionRequiredMixin, BulkEditView): class InterfaceTemplateBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.change_interfacetemplate' permission_required = 'dcim.change_interfacetemplate'
queryset = InterfaceTemplate.objects.all() queryset = InterfaceTemplate.objects.all()
parent_model = DeviceType
table = tables.InterfaceTemplateTable table = tables.InterfaceTemplateTable
form = forms.InterfaceTemplateBulkEditForm form = forms.InterfaceTemplateBulkEditForm
@ -822,14 +869,15 @@ class InterfaceTemplateBulkEditView(PermissionRequiredMixin, BulkEditView):
class InterfaceTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class InterfaceTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_interfacetemplate' permission_required = 'dcim.delete_interfacetemplate'
queryset = InterfaceTemplate.objects.all() queryset = InterfaceTemplate.objects.all()
parent_model = DeviceType
table = tables.InterfaceTemplateTable table = tables.InterfaceTemplateTable
#
# Front port templates
#
class FrontPortTemplateCreateView(PermissionRequiredMixin, ComponentCreateView): class FrontPortTemplateCreateView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_frontporttemplate' permission_required = 'dcim.add_frontporttemplate'
parent_model = DeviceType
parent_field = 'device_type'
model = FrontPortTemplate model = FrontPortTemplate
form = forms.FrontPortTemplateCreateForm form = forms.FrontPortTemplateCreateForm
model_form = forms.FrontPortTemplateForm model_form = forms.FrontPortTemplateForm
@ -842,17 +890,30 @@ class FrontPortTemplateEditView(PermissionRequiredMixin, ObjectEditView):
model_form = forms.FrontPortTemplateForm model_form = forms.FrontPortTemplateForm
class FrontPortTemplateDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'dcim.delete_frontporttemplate'
model = FrontPortTemplate
class FrontPortTemplateBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.change_frontporttemplate'
queryset = FrontPortTemplate.objects.all()
table = tables.FrontPortTemplateTable
form = forms.FrontPortTemplateBulkEditForm
class FrontPortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class FrontPortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_frontporttemplate' permission_required = 'dcim.delete_frontporttemplate'
queryset = FrontPortTemplate.objects.all() queryset = FrontPortTemplate.objects.all()
parent_model = DeviceType
table = tables.FrontPortTemplateTable table = tables.FrontPortTemplateTable
#
# Rear port templates
#
class RearPortTemplateCreateView(PermissionRequiredMixin, ComponentCreateView): class RearPortTemplateCreateView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_rearporttemplate' permission_required = 'dcim.add_rearporttemplate'
parent_model = DeviceType
parent_field = 'device_type'
model = RearPortTemplate model = RearPortTemplate
form = forms.RearPortTemplateCreateForm form = forms.RearPortTemplateCreateForm
model_form = forms.RearPortTemplateForm model_form = forms.RearPortTemplateForm
@ -865,17 +926,30 @@ class RearPortTemplateEditView(PermissionRequiredMixin, ObjectEditView):
model_form = forms.RearPortTemplateForm model_form = forms.RearPortTemplateForm
class RearPortTemplateDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'dcim.delete_rearporttemplate'
model = RearPortTemplate
class RearPortTemplateBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.change_rearporttemplate'
queryset = RearPortTemplate.objects.all()
table = tables.RearPortTemplateTable
form = forms.RearPortTemplateBulkEditForm
class RearPortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class RearPortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_rearporttemplate' permission_required = 'dcim.delete_rearporttemplate'
queryset = RearPortTemplate.objects.all() queryset = RearPortTemplate.objects.all()
parent_model = DeviceType
table = tables.RearPortTemplateTable table = tables.RearPortTemplateTable
#
# Device bay templates
#
class DeviceBayTemplateCreateView(PermissionRequiredMixin, ComponentCreateView): class DeviceBayTemplateCreateView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_devicebaytemplate' permission_required = 'dcim.add_devicebaytemplate'
parent_model = DeviceType
parent_field = 'device_type'
model = DeviceBayTemplate model = DeviceBayTemplate
form = forms.DeviceBayTemplateCreateForm form = forms.DeviceBayTemplateCreateForm
model_form = forms.DeviceBayTemplateForm model_form = forms.DeviceBayTemplateForm
@ -888,10 +962,21 @@ class DeviceBayTemplateEditView(PermissionRequiredMixin, ObjectEditView):
model_form = forms.DeviceBayTemplateForm model_form = forms.DeviceBayTemplateForm
class DeviceBayTemplateDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'dcim.delete_devicebaytemplate'
model = DeviceBayTemplate
# class DeviceBayTemplateBulkEditView(PermissionRequiredMixin, BulkEditView):
# permission_required = 'dcim.change_devicebaytemplate'
# queryset = DeviceBayTemplate.objects.all()
# table = tables.DeviceBayTemplateTable
# form = forms.DeviceBayTemplateBulkEditForm
class DeviceBayTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class DeviceBayTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_devicebaytemplate' permission_required = 'dcim.delete_devicebaytemplate'
queryset = DeviceBayTemplate.objects.all() queryset = DeviceBayTemplate.objects.all()
parent_model = DeviceType
table = tables.DeviceBayTemplateTable table = tables.DeviceBayTemplateTable
@ -903,7 +988,6 @@ class DeviceRoleListView(PermissionRequiredMixin, ObjectListView):
permission_required = 'dcim.view_devicerole' permission_required = 'dcim.view_devicerole'
queryset = DeviceRole.objects.all() queryset = DeviceRole.objects.all()
table = tables.DeviceRoleTable table = tables.DeviceRoleTable
template_name = 'dcim/devicerole_list.html'
class DeviceRoleCreateView(PermissionRequiredMixin, ObjectEditView): class DeviceRoleCreateView(PermissionRequiredMixin, ObjectEditView):
@ -939,7 +1023,6 @@ class PlatformListView(PermissionRequiredMixin, ObjectListView):
permission_required = 'dcim.view_platform' permission_required = 'dcim.view_platform'
queryset = Platform.objects.all() queryset = Platform.objects.all()
table = tables.PlatformTable table = tables.PlatformTable
template_name = 'dcim/platform_list.html'
class PlatformCreateView(PermissionRequiredMixin, ObjectEditView): class PlatformCreateView(PermissionRequiredMixin, ObjectEditView):
@ -1200,13 +1283,11 @@ class ConsolePortListView(PermissionRequiredMixin, ObjectListView):
filterset = filters.ConsolePortFilterSet filterset = filters.ConsolePortFilterSet
filterset_form = forms.ConsolePortFilterForm filterset_form = forms.ConsolePortFilterForm
table = tables.ConsolePortDetailTable table = tables.ConsolePortDetailTable
template_name = 'dcim/device_component_list.html' action_buttons = ('import', 'export')
class ConsolePortCreateView(PermissionRequiredMixin, ComponentCreateView): class ConsolePortCreateView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_consoleport' permission_required = 'dcim.add_consoleport'
parent_model = Device
parent_field = 'device'
model = ConsolePort model = ConsolePort
form = forms.ConsolePortCreateForm form = forms.ConsolePortCreateForm
model_form = forms.ConsolePortForm model_form = forms.ConsolePortForm
@ -1231,11 +1312,18 @@ class ConsolePortBulkImportView(PermissionRequiredMixin, BulkImportView):
default_return_url = 'dcim:consoleport_list' default_return_url = 'dcim:consoleport_list'
class ConsolePortBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.change_consoleport'
queryset = ConsolePort.objects.all()
table = tables.ConsolePortTable
form = forms.ConsolePortBulkEditForm
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()
parent_model = Device
table = tables.ConsolePortTable table = tables.ConsolePortTable
default_return_url = 'dcim:consoleport_list'
# #
@ -1248,13 +1336,11 @@ class ConsoleServerPortListView(PermissionRequiredMixin, ObjectListView):
filterset = filters.ConsoleServerPortFilterSet filterset = filters.ConsoleServerPortFilterSet
filterset_form = forms.ConsoleServerPortFilterForm filterset_form = forms.ConsoleServerPortFilterForm
table = tables.ConsoleServerPortDetailTable table = tables.ConsoleServerPortDetailTable
template_name = 'dcim/device_component_list.html' action_buttons = ('import', 'export')
class ConsoleServerPortCreateView(PermissionRequiredMixin, ComponentCreateView): class ConsoleServerPortCreateView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_consoleserverport' permission_required = 'dcim.add_consoleserverport'
parent_model = Device
parent_field = 'device'
model = ConsoleServerPort model = ConsoleServerPort
form = forms.ConsoleServerPortCreateForm form = forms.ConsoleServerPortCreateForm
model_form = forms.ConsoleServerPortForm model_form = forms.ConsoleServerPortForm
@ -1282,7 +1368,6 @@ class ConsoleServerPortBulkImportView(PermissionRequiredMixin, BulkImportView):
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()
parent_model = Device
table = tables.ConsoleServerPortTable table = tables.ConsoleServerPortTable
form = forms.ConsoleServerPortBulkEditForm form = forms.ConsoleServerPortBulkEditForm
@ -1302,8 +1387,8 @@ class ConsoleServerPortBulkDisconnectView(PermissionRequiredMixin, BulkDisconnec
class ConsoleServerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class ConsoleServerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_consoleserverport' permission_required = 'dcim.delete_consoleserverport'
queryset = ConsoleServerPort.objects.all() queryset = ConsoleServerPort.objects.all()
parent_model = Device
table = tables.ConsoleServerPortTable table = tables.ConsoleServerPortTable
default_return_url = 'dcim:consoleserverport_list'
# #
@ -1316,13 +1401,11 @@ class PowerPortListView(PermissionRequiredMixin, ObjectListView):
filterset = filters.PowerPortFilterSet filterset = filters.PowerPortFilterSet
filterset_form = forms.PowerPortFilterForm filterset_form = forms.PowerPortFilterForm
table = tables.PowerPortDetailTable table = tables.PowerPortDetailTable
template_name = 'dcim/device_component_list.html' action_buttons = ('import', 'export')
class PowerPortCreateView(PermissionRequiredMixin, ComponentCreateView): class PowerPortCreateView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_powerport' permission_required = 'dcim.add_powerport'
parent_model = Device
parent_field = 'device'
model = PowerPort model = PowerPort
form = forms.PowerPortCreateForm form = forms.PowerPortCreateForm
model_form = forms.PowerPortForm model_form = forms.PowerPortForm
@ -1347,11 +1430,18 @@ class PowerPortBulkImportView(PermissionRequiredMixin, BulkImportView):
default_return_url = 'dcim:powerport_list' default_return_url = 'dcim:powerport_list'
class PowerPortBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.change_powerport'
queryset = PowerPort.objects.all()
table = tables.PowerPortTable
form = forms.PowerPortBulkEditForm
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()
parent_model = Device
table = tables.PowerPortTable table = tables.PowerPortTable
default_return_url = 'dcim:powerport_list'
# #
@ -1364,13 +1454,11 @@ class PowerOutletListView(PermissionRequiredMixin, ObjectListView):
filterset = filters.PowerOutletFilterSet filterset = filters.PowerOutletFilterSet
filterset_form = forms.PowerOutletFilterForm filterset_form = forms.PowerOutletFilterForm
table = tables.PowerOutletDetailTable table = tables.PowerOutletDetailTable
template_name = 'dcim/device_component_list.html' action_buttons = ('import', 'export')
class PowerOutletCreateView(PermissionRequiredMixin, ComponentCreateView): class PowerOutletCreateView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_poweroutlet' permission_required = 'dcim.add_poweroutlet'
parent_model = Device
parent_field = 'device'
model = PowerOutlet model = PowerOutlet
form = forms.PowerOutletCreateForm form = forms.PowerOutletCreateForm
model_form = forms.PowerOutletForm model_form = forms.PowerOutletForm
@ -1398,7 +1486,6 @@ class PowerOutletBulkImportView(PermissionRequiredMixin, BulkImportView):
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()
parent_model = Device
table = tables.PowerOutletTable table = tables.PowerOutletTable
form = forms.PowerOutletBulkEditForm form = forms.PowerOutletBulkEditForm
@ -1418,8 +1505,8 @@ class PowerOutletBulkDisconnectView(PermissionRequiredMixin, BulkDisconnectView)
class PowerOutletBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class PowerOutletBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_poweroutlet' permission_required = 'dcim.delete_poweroutlet'
queryset = PowerOutlet.objects.all() queryset = PowerOutlet.objects.all()
parent_model = Device
table = tables.PowerOutletTable table = tables.PowerOutletTable
default_return_url = 'dcim:poweroutlet_list'
# #
@ -1432,7 +1519,7 @@ class InterfaceListView(PermissionRequiredMixin, ObjectListView):
filterset = filters.InterfaceFilterSet filterset = filters.InterfaceFilterSet
filterset_form = forms.InterfaceFilterForm filterset_form = forms.InterfaceFilterForm
table = tables.InterfaceDetailTable table = tables.InterfaceDetailTable
template_name = 'dcim/device_component_list.html' action_buttons = ('import', 'export')
class InterfaceView(PermissionRequiredMixin, View): class InterfaceView(PermissionRequiredMixin, View):
@ -1473,8 +1560,6 @@ class InterfaceView(PermissionRequiredMixin, View):
class InterfaceCreateView(PermissionRequiredMixin, ComponentCreateView): class InterfaceCreateView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_interface' permission_required = 'dcim.add_interface'
parent_model = Device
parent_field = 'device'
model = Interface model = Interface
form = forms.InterfaceCreateForm form = forms.InterfaceCreateForm
model_form = forms.InterfaceForm model_form = forms.InterfaceForm
@ -1503,7 +1588,6 @@ class InterfaceBulkImportView(PermissionRequiredMixin, BulkImportView):
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()
parent_model = Device
table = tables.InterfaceTable table = tables.InterfaceTable
form = forms.InterfaceBulkEditForm form = forms.InterfaceBulkEditForm
@ -1523,8 +1607,8 @@ class InterfaceBulkDisconnectView(PermissionRequiredMixin, BulkDisconnectView):
class InterfaceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class InterfaceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_interface' permission_required = 'dcim.delete_interface'
queryset = Interface.objects.all() queryset = Interface.objects.all()
parent_model = Device
table = tables.InterfaceTable table = tables.InterfaceTable
default_return_url = 'dcim:interface_list'
# #
@ -1537,13 +1621,11 @@ class FrontPortListView(PermissionRequiredMixin, ObjectListView):
filterset = filters.FrontPortFilterSet filterset = filters.FrontPortFilterSet
filterset_form = forms.FrontPortFilterForm filterset_form = forms.FrontPortFilterForm
table = tables.FrontPortDetailTable table = tables.FrontPortDetailTable
template_name = 'dcim/device_component_list.html' action_buttons = ('import', 'export')
class FrontPortCreateView(PermissionRequiredMixin, ComponentCreateView): class FrontPortCreateView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_frontport' permission_required = 'dcim.add_frontport'
parent_model = Device
parent_field = 'device'
model = FrontPort model = FrontPort
form = forms.FrontPortCreateForm form = forms.FrontPortCreateForm
model_form = forms.FrontPortForm model_form = forms.FrontPortForm
@ -1571,7 +1653,6 @@ class FrontPortBulkImportView(PermissionRequiredMixin, BulkImportView):
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()
parent_model = Device
table = tables.FrontPortTable table = tables.FrontPortTable
form = forms.FrontPortBulkEditForm form = forms.FrontPortBulkEditForm
@ -1591,8 +1672,8 @@ class FrontPortBulkDisconnectView(PermissionRequiredMixin, BulkDisconnectView):
class FrontPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class FrontPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_frontport' permission_required = 'dcim.delete_frontport'
queryset = FrontPort.objects.all() queryset = FrontPort.objects.all()
parent_model = Device
table = tables.FrontPortTable table = tables.FrontPortTable
default_return_url = 'dcim:frontport_list'
# #
@ -1605,13 +1686,11 @@ class RearPortListView(PermissionRequiredMixin, ObjectListView):
filterset = filters.RearPortFilterSet filterset = filters.RearPortFilterSet
filterset_form = forms.RearPortFilterForm filterset_form = forms.RearPortFilterForm
table = tables.RearPortDetailTable table = tables.RearPortDetailTable
template_name = 'dcim/device_component_list.html' action_buttons = ('import', 'export')
class RearPortCreateView(PermissionRequiredMixin, ComponentCreateView): class RearPortCreateView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_rearport' permission_required = 'dcim.add_rearport'
parent_model = Device
parent_field = 'device'
model = RearPort model = RearPort
form = forms.RearPortCreateForm form = forms.RearPortCreateForm
model_form = forms.RearPortForm model_form = forms.RearPortForm
@ -1639,7 +1718,6 @@ class RearPortBulkImportView(PermissionRequiredMixin, BulkImportView):
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()
parent_model = Device
table = tables.RearPortTable table = tables.RearPortTable
form = forms.RearPortBulkEditForm form = forms.RearPortBulkEditForm
@ -1659,8 +1737,8 @@ class RearPortBulkDisconnectView(PermissionRequiredMixin, BulkDisconnectView):
class RearPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class RearPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_rearport' permission_required = 'dcim.delete_rearport'
queryset = RearPort.objects.all() queryset = RearPort.objects.all()
parent_model = Device
table = tables.RearPortTable table = tables.RearPortTable
default_return_url = 'dcim:rearport_list'
# #
@ -1675,13 +1753,11 @@ class DeviceBayListView(PermissionRequiredMixin, ObjectListView):
filterset = filters.DeviceBayFilterSet filterset = filters.DeviceBayFilterSet
filterset_form = forms.DeviceBayFilterForm filterset_form = forms.DeviceBayFilterForm
table = tables.DeviceBayDetailTable table = tables.DeviceBayDetailTable
template_name = 'dcim/device_component_list.html' action_buttons = ('import', 'export')
class DeviceBayCreateView(PermissionRequiredMixin, ComponentCreateView): class DeviceBayCreateView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_devicebay' permission_required = 'dcim.add_devicebay'
parent_model = Device
parent_field = 'device'
model = DeviceBay model = DeviceBay
form = forms.DeviceBayCreateForm form = forms.DeviceBayCreateForm
model_form = forms.DeviceBayForm model_form = forms.DeviceBayForm
@ -1784,8 +1860,8 @@ class DeviceBayBulkRenameView(PermissionRequiredMixin, BulkRenameView):
class DeviceBayBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class DeviceBayBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_devicebay' permission_required = 'dcim.delete_devicebay'
queryset = DeviceBay.objects.all() queryset = DeviceBay.objects.all()
parent_model = Device
table = tables.DeviceBayTable table = tables.DeviceBayTable
default_return_url = 'dcim:devicebay_list'
# #
@ -1876,7 +1952,7 @@ class CableListView(PermissionRequiredMixin, ObjectListView):
filterset = filters.CableFilterSet filterset = filters.CableFilterSet
filterset_form = forms.CableFilterForm filterset_form = forms.CableFilterForm
table = tables.CableTable table = tables.CableTable
template_name = 'dcim/cable_list.html' action_buttons = ('import', 'export')
class CableView(PermissionRequiredMixin, View): class CableView(PermissionRequiredMixin, View):
@ -2148,7 +2224,7 @@ class InventoryItemListView(PermissionRequiredMixin, ObjectListView):
filterset = filters.InventoryItemFilterSet filterset = filters.InventoryItemFilterSet
filterset_form = forms.InventoryItemFilterForm filterset_form = forms.InventoryItemFilterForm
table = tables.InventoryItemTable table = tables.InventoryItemTable
template_name = 'dcim/inventoryitem_list.html' action_buttons = ('import', 'export')
class InventoryItemEditView(PermissionRequiredMixin, ObjectEditView): class InventoryItemEditView(PermissionRequiredMixin, ObjectEditView):
@ -2156,13 +2232,13 @@ class InventoryItemEditView(PermissionRequiredMixin, ObjectEditView):
model = InventoryItem model = InventoryItem
model_form = forms.InventoryItemForm model_form = forms.InventoryItemForm
def alter_obj(self, obj, request, url_args, url_kwargs):
if 'device' in url_kwargs:
obj.device = get_object_or_404(Device, pk=url_kwargs['device'])
return obj
def get_return_url(self, request, obj): class InventoryItemCreateView(PermissionRequiredMixin, ComponentCreateView):
return reverse('dcim:device_inventory', kwargs={'pk': obj.device.pk}) permission_required = 'dcim.add_inventoryitem'
model = InventoryItem
form = forms.InventoryItemCreateForm
model_form = forms.InventoryItemForm
template_name = 'dcim/device_component_add.html'
class InventoryItemDeleteView(PermissionRequiredMixin, ObjectDeleteView): class InventoryItemDeleteView(PermissionRequiredMixin, ObjectDeleteView):
@ -2204,7 +2280,7 @@ class VirtualChassisListView(PermissionRequiredMixin, ObjectListView):
table = tables.VirtualChassisTable table = tables.VirtualChassisTable
filterset = filters.VirtualChassisFilterSet filterset = filters.VirtualChassisFilterSet
filterset_form = forms.VirtualChassisFilterForm filterset_form = forms.VirtualChassisFilterForm
template_name = 'dcim/virtualchassis_list.html' action_buttons = ('export',)
class VirtualChassisCreateView(PermissionRequiredMixin, View): class VirtualChassisCreateView(PermissionRequiredMixin, View):
@ -2448,7 +2524,6 @@ class PowerPanelListView(PermissionRequiredMixin, ObjectListView):
filterset = filters.PowerPanelFilterSet filterset = filters.PowerPanelFilterSet
filterset_form = forms.PowerPanelFilterForm filterset_form = forms.PowerPanelFilterForm
table = tables.PowerPanelTable table = tables.PowerPanelTable
template_name = 'dcim/powerpanel_list.html'
class PowerPanelView(PermissionRequiredMixin, View): class PowerPanelView(PermissionRequiredMixin, View):
@ -2517,7 +2592,6 @@ class PowerFeedListView(PermissionRequiredMixin, ObjectListView):
filterset = filters.PowerFeedFilterSet filterset = filters.PowerFeedFilterSet
filterset_form = forms.PowerFeedFilterForm filterset_form = forms.PowerFeedFilterForm
table = tables.PowerFeedTable table = tables.PowerFeedTable
template_name = 'dcim/powerfeed_list.html'
class PowerFeedView(PermissionRequiredMixin, View): class PowerFeedView(PermissionRequiredMixin, View):

View File

@ -20,6 +20,8 @@ from utilities.api import (
ChoiceField, ContentTypeField, get_serializer_for_model, SerializerNotFound, SerializedPKRelatedField, ChoiceField, ContentTypeField, get_serializer_for_model, SerializerNotFound, SerializedPKRelatedField,
ValidatedModelSerializer, ValidatedModelSerializer,
) )
from virtualization.api.nested_serializers import NestedClusterGroupSerializer, NestedClusterSerializer
from virtualization.models import Cluster, ClusterGroup
from .nested_serializers import * from .nested_serializers import *
@ -161,6 +163,18 @@ class ConfigContextSerializer(ValidatedModelSerializer):
required=False, required=False,
many=True many=True
) )
cluster_groups = SerializedPKRelatedField(
queryset=ClusterGroup.objects.all(),
serializer=NestedClusterGroupSerializer,
required=False,
many=True
)
clusters = SerializedPKRelatedField(
queryset=Cluster.objects.all(),
serializer=NestedClusterSerializer,
required=False,
many=True
)
tenant_groups = SerializedPKRelatedField( tenant_groups = SerializedPKRelatedField(
queryset=TenantGroup.objects.all(), queryset=TenantGroup.objects.all(),
serializer=NestedTenantGroupSerializer, serializer=NestedTenantGroupSerializer,
@ -184,7 +198,7 @@ class ConfigContextSerializer(ValidatedModelSerializer):
model = ConfigContext model = ConfigContext
fields = [ fields = [
'id', 'name', 'weight', 'description', 'is_active', 'regions', 'sites', 'roles', 'platforms', 'id', 'name', 'weight', 'description', 'is_active', 'regions', 'sites', 'roles', 'platforms',
'tenant_groups', 'tenants', 'tags', 'data', 'cluster_groups', 'clusters', 'tenant_groups', 'tenants', 'tags', 'data',
] ]

View File

@ -15,34 +15,34 @@ router = routers.DefaultRouter()
router.APIRootView = ExtrasRootView router.APIRootView = ExtrasRootView
# Field choices # Field choices
router.register(r'_choices', views.ExtrasFieldChoicesViewSet, basename='field-choice') router.register('_choices', views.ExtrasFieldChoicesViewSet, basename='field-choice')
# Custom field choices # Custom field choices
router.register(r'_custom_field_choices', views.CustomFieldChoicesViewSet, basename='custom-field-choice') router.register('_custom_field_choices', views.CustomFieldChoicesViewSet, basename='custom-field-choice')
# Graphs # Graphs
router.register(r'graphs', views.GraphViewSet) router.register('graphs', views.GraphViewSet)
# Export templates # Export templates
router.register(r'export-templates', views.ExportTemplateViewSet) router.register('export-templates', views.ExportTemplateViewSet)
# Tags # Tags
router.register(r'tags', views.TagViewSet) router.register('tags', views.TagViewSet)
# Image attachments # Image attachments
router.register(r'image-attachments', views.ImageAttachmentViewSet) router.register('image-attachments', views.ImageAttachmentViewSet)
# Config contexts # Config contexts
router.register(r'config-contexts', views.ConfigContextViewSet) router.register('config-contexts', views.ConfigContextViewSet)
# Reports # Reports
router.register(r'reports', views.ReportViewSet, basename='report') router.register('reports', views.ReportViewSet, basename='report')
# Scripts # Scripts
router.register(r'scripts', views.ScriptViewSet, basename='script') router.register('scripts', views.ScriptViewSet, basename='script')
# Change logging # Change logging
router.register(r'object-changes', views.ObjectChangeViewSet) router.register('object-changes', views.ObjectChangeViewSet)
app_name = 'extras-api' app_name = 'extras-api'
urlpatterns = router.urls urlpatterns = router.urls

View File

@ -1,28 +1,8 @@
from django.apps import AppConfig from django.apps import AppConfig
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
import redis
class ExtrasConfig(AppConfig): class ExtrasConfig(AppConfig):
name = "extras" name = "extras"
def ready(self): def ready(self):
import extras.signals import extras.signals
# 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."
)

View File

@ -4,6 +4,7 @@ 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 virtualization.models import Cluster, ClusterGroup
from .choices import * from .choices import *
from .models import ConfigContext, CustomField, Graph, ExportTemplate, ObjectChange, Tag from .models import ConfigContext, CustomField, Graph, ExportTemplate, ObjectChange, Tag
@ -170,6 +171,22 @@ class ConfigContextFilterSet(django_filters.FilterSet):
to_field_name='slug', to_field_name='slug',
label='Platform (slug)', label='Platform (slug)',
) )
cluster_group_id = django_filters.ModelMultipleChoiceFilter(
field_name='cluster_groups',
queryset=ClusterGroup.objects.all(),
label='Cluster group',
)
cluster_group = django_filters.ModelMultipleChoiceFilter(
field_name='cluster_groups__slug',
queryset=ClusterGroup.objects.all(),
to_field_name='slug',
label='Cluster group (slug)',
)
cluster_id = django_filters.ModelMultipleChoiceFilter(
field_name='clusters',
queryset=Cluster.objects.all(),
label='Cluster',
)
tenant_group_id = django_filters.ModelMultipleChoiceFilter( tenant_group_id = django_filters.ModelMultipleChoiceFilter(
field_name='tenant_groups', field_name='tenant_groups',
queryset=TenantGroup.objects.all(), queryset=TenantGroup.objects.all(),

View File

@ -1,18 +1,17 @@
from collections import OrderedDict
from django import forms from django import forms
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist from mptt.forms import TreeNodeMultipleChoiceField
from taggit.forms import TagField from taggit.forms import TagField
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 utilities.forms import ( from utilities.forms import (
add_blank_choice, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ColorSelect, add_blank_choice, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ColorSelect,
CommentField, ContentTypeSelect, DatePicker, DateTimePicker, FilterChoiceField, LaxURLField, JSONField, CommentField, ContentTypeSelect, DateTimePicker, DynamicModelMultipleChoiceField, JSONField, SlugField,
SlugField, StaticSelect2, BOOLEAN_WITH_BLANK_CHOICES, StaticSelect2, StaticSelect2Multiple, BOOLEAN_WITH_BLANK_CHOICES,
) )
from virtualization.models import Cluster, ClusterGroup
from .choices import * from .choices import *
from .models import ConfigContext, CustomField, CustomFieldValue, ImageAttachment, ObjectChange, Tag from .models import ConfigContext, CustomField, CustomFieldValue, ImageAttachment, ObjectChange, Tag
@ -21,102 +20,41 @@ from .models import ConfigContext, CustomField, CustomFieldValue, ImageAttachmen
# Custom fields # Custom fields
# #
def get_custom_fields_for_model(content_type, filterable_only=False, bulk_edit=False): class CustomFieldModelForm(forms.ModelForm):
"""
Retrieve all CustomFields applicable to the given ContentType
"""
field_dict = OrderedDict()
custom_fields = CustomField.objects.filter(obj_type=content_type)
if filterable_only:
custom_fields = custom_fields.exclude(filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED)
for cf in custom_fields:
field_name = 'cf_{}'.format(str(cf.name))
initial = cf.default if not bulk_edit else None
# Integer
if cf.type == CustomFieldTypeChoices.TYPE_INTEGER:
field = forms.IntegerField(required=cf.required, initial=initial)
# Boolean
elif cf.type == CustomFieldTypeChoices.TYPE_BOOLEAN:
choices = (
(None, '---------'),
(1, 'True'),
(0, 'False'),
)
if initial is not None and initial.lower() in ['true', 'yes', '1']:
initial = 1
elif initial is not None and initial.lower() in ['false', 'no', '0']:
initial = 0
else:
initial = None
field = forms.NullBooleanField(
required=cf.required, initial=initial, widget=StaticSelect2(choices=choices)
)
# Date
elif cf.type == CustomFieldTypeChoices.TYPE_DATE:
field = forms.DateField(required=cf.required, initial=initial, widget=DatePicker())
# Select
elif cf.type == CustomFieldTypeChoices.TYPE_SELECT:
choices = [(cfc.pk, cfc) for cfc in cf.choices.all()]
if not cf.required or bulk_edit or filterable_only:
choices = [(None, '---------')] + choices
# Check for a default choice
default_choice = None
if initial:
try:
default_choice = cf.choices.get(value=initial).pk
except ObjectDoesNotExist:
pass
field = forms.TypedChoiceField(
choices=choices, coerce=int, required=cf.required, initial=default_choice, widget=StaticSelect2()
)
# URL
elif cf.type == CustomFieldTypeChoices.TYPE_URL:
field = LaxURLField(required=cf.required, initial=initial)
# Text
else:
field = forms.CharField(max_length=255, required=cf.required, initial=initial)
field.model = cf
field.label = cf.label if cf.label else cf.name.replace('_', ' ').capitalize()
if cf.description:
field.help_text = cf.description
field_dict[field_name] = field
return field_dict
class CustomFieldForm(forms.ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.custom_fields = []
self.obj_type = ContentType.objects.get_for_model(self._meta.model) self.obj_type = ContentType.objects.get_for_model(self._meta.model)
self.custom_fields = []
self.custom_field_values = {}
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
# Add all applicable CustomFields to the form self._append_customfield_fields()
custom_fields = []
for name, field in get_custom_fields_for_model(self.obj_type).items():
self.fields[name] = field
custom_fields.append(name)
self.custom_fields = custom_fields
# If editing an existing object, initialize values for all custom fields def _append_customfield_fields(self):
"""
Append form fields for all CustomFields assigned to this model.
"""
# Retrieve initial CustomField values for the instance
if self.instance.pk: if self.instance.pk:
existing_values = CustomFieldValue.objects.filter( for cfv in CustomFieldValue.objects.filter(
obj_type=self.obj_type, obj_type=self.obj_type,
obj_id=self.instance.pk obj_id=self.instance.pk
).prefetch_related('field') ).prefetch_related('field'):
for cfv in existing_values: self.custom_field_values[cfv.field.name] = cfv.serialized_value
self.initial['cf_{}'.format(str(cfv.field.name))] = cfv.serialized_value
# Append form fields; assign initial values if modifying and existing object
for cf in CustomField.objects.filter(obj_type=self.obj_type):
field_name = 'cf_{}'.format(cf.name)
if self.instance.pk:
self.fields[field_name] = cf.to_form_field(set_initial=False)
self.fields[field_name].initial = self.custom_field_values.get(cf.name)
else:
self.fields[field_name] = cf.to_form_field()
# Annotate the field in the list of CustomField form fields
self.custom_fields.append(field_name)
def _save_custom_fields(self): def _save_custom_fields(self):
@ -151,6 +89,19 @@ class CustomFieldForm(forms.ModelForm):
return obj return obj
class CustomFieldModelCSVForm(CustomFieldModelForm):
def _append_customfield_fields(self):
# Append form fields
for cf in CustomField.objects.filter(obj_type=self.obj_type):
field_name = 'cf_{}'.format(cf.name)
self.fields[field_name] = cf.to_form_field(for_csv_import=True)
# Annotate the field in the list of CustomField form fields
self.custom_fields.append(field_name)
class CustomFieldBulkEditForm(BulkEditForm): class CustomFieldBulkEditForm(BulkEditForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -160,15 +111,14 @@ class CustomFieldBulkEditForm(BulkEditForm):
self.obj_type = ContentType.objects.get_for_model(self.model) self.obj_type = ContentType.objects.get_for_model(self.model)
# Add all applicable CustomFields to the form # Add all applicable CustomFields to the form
custom_fields = get_custom_fields_for_model(self.obj_type, bulk_edit=True).items() custom_fields = CustomField.objects.filter(obj_type=self.obj_type)
for name, field in custom_fields: for cf in custom_fields:
# Annotate non-required custom fields as nullable # Annotate non-required custom fields as nullable
if not field.required: if not cf.required:
self.nullable_fields.append(name) self.nullable_fields.append(cf.name)
field.required = False self.fields[cf.name] = cf.to_form_field(set_initial=False, enforce_required=False)
self.fields[name] = field
# Annotate this as a custom field # Annotate this as a custom field
self.custom_fields.append(name) self.custom_fields.append(cf.name)
class CustomFieldFilterForm(forms.Form): class CustomFieldFilterForm(forms.Form):
@ -180,10 +130,12 @@ class CustomFieldFilterForm(forms.Form):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
# Add all applicable CustomFields to the form # Add all applicable CustomFields to the form
custom_fields = get_custom_fields_for_model(self.obj_type, filterable_only=True).items() custom_fields = CustomField.objects.filter(obj_type=self.obj_type).exclude(
for name, field in custom_fields: filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED
field.required = False )
self.fields[name] = field for cf in custom_fields:
field_name = 'cf_{}'.format(cf.name)
self.fields[field_name] = cf.to_form_field(set_initial=True, enforce_required=False)
# #
@ -239,7 +191,61 @@ class TagBulkEditForm(BootstrapMixin, BulkEditForm):
# #
class ConfigContextForm(BootstrapMixin, forms.ModelForm): class ConfigContextForm(BootstrapMixin, forms.ModelForm):
tags = forms.ModelMultipleChoiceField( regions = TreeNodeMultipleChoiceField(
queryset=Region.objects.all(),
required=False,
widget=StaticSelect2Multiple()
)
sites = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(),
required=False,
widget=APISelectMultiple(
api_url="/api/dcim/sites/"
)
)
roles = DynamicModelMultipleChoiceField(
queryset=DeviceRole.objects.all(),
required=False,
widget=APISelectMultiple(
api_url="/api/dcim/device-roles/"
)
)
platforms = DynamicModelMultipleChoiceField(
queryset=Platform.objects.all(),
required=False,
widget=APISelectMultiple(
api_url="/api/dcim/platforms/"
)
)
cluster_groups = DynamicModelMultipleChoiceField(
queryset=ClusterGroup.objects.all(),
required=False,
widget=APISelectMultiple(
api_url="/api/virtualization/cluster-groups/"
)
)
clusters = DynamicModelMultipleChoiceField(
queryset=Cluster.objects.all(),
required=False,
widget=APISelectMultiple(
api_url="/api/virtualization/clusters/"
)
)
tenant_groups = DynamicModelMultipleChoiceField(
queryset=TenantGroup.objects.all(),
required=False,
widget=APISelectMultiple(
api_url="/api/tenancy/tenant-groups/"
)
)
tenants = DynamicModelMultipleChoiceField(
queryset=Tenant.objects.all(),
required=False,
widget=APISelectMultiple(
api_url="/api/tenancy/tenants/"
)
)
tags = DynamicModelMultipleChoiceField(
queryset=Tag.objects.all(), queryset=Tag.objects.all(),
to_field_name='slug', to_field_name='slug',
required=False, required=False,
@ -253,30 +259,10 @@ class ConfigContextForm(BootstrapMixin, forms.ModelForm):
class Meta: class Meta:
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', 'cluster_groups',
'tenants', 'tags', 'data', 'clusters', 'tenant_groups', 'tenants', 'tags', 'data',
] )
widgets = {
'regions': APISelectMultiple(
api_url="/api/dcim/regions/"
),
'sites': APISelectMultiple(
api_url="/api/dcim/sites/"
),
'roles': APISelectMultiple(
api_url="/api/dcim/device-roles/"
),
'platforms': APISelectMultiple(
api_url="/api/dcim/platforms/"
),
'tenant_groups': APISelectMultiple(
api_url="/api/tenancy/tenant-groups/"
),
'tenants': APISelectMultiple(
api_url="/api/tenancy/tenants/"
),
}
class ConfigContextBulkEditForm(BootstrapMixin, BulkEditForm): class ConfigContextBulkEditForm(BootstrapMixin, BulkEditForm):
@ -308,57 +294,81 @@ class ConfigContextFilterForm(BootstrapMixin, forms.Form):
required=False, required=False,
label='Search' label='Search'
) )
region = FilterChoiceField( region = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(), queryset=Region.objects.all(),
to_field_name='slug', to_field_name='slug',
required=False,
widget=APISelectMultiple( widget=APISelectMultiple(
api_url="/api/dcim/regions/", api_url="/api/dcim/regions/",
value_field="slug", value_field="slug",
) )
) )
site = FilterChoiceField( site = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(), queryset=Site.objects.all(),
to_field_name='slug', to_field_name='slug',
required=False,
widget=APISelectMultiple( widget=APISelectMultiple(
api_url="/api/dcim/sites/", api_url="/api/dcim/sites/",
value_field="slug", value_field="slug",
) )
) )
role = FilterChoiceField( role = DynamicModelMultipleChoiceField(
queryset=DeviceRole.objects.all(), queryset=DeviceRole.objects.all(),
to_field_name='slug', to_field_name='slug',
required=False,
widget=APISelectMultiple( widget=APISelectMultiple(
api_url="/api/dcim/device-roles/", api_url="/api/dcim/device-roles/",
value_field="slug", value_field="slug",
) )
) )
platform = FilterChoiceField( platform = DynamicModelMultipleChoiceField(
queryset=Platform.objects.all(), queryset=Platform.objects.all(),
to_field_name='slug', to_field_name='slug',
required=False,
widget=APISelectMultiple( widget=APISelectMultiple(
api_url="/api/dcim/platforms/", api_url="/api/dcim/platforms/",
value_field="slug", value_field="slug",
) )
) )
tenant_group = FilterChoiceField( cluster_group = DynamicModelMultipleChoiceField(
queryset=ClusterGroup.objects.all(),
to_field_name='slug',
required=False,
widget=APISelectMultiple(
api_url="/api/virtualization/cluster-groups/",
value_field="slug",
)
)
cluster_id = DynamicModelMultipleChoiceField(
queryset=Cluster.objects.all(),
required=False,
label='Cluster',
widget=APISelectMultiple(
api_url="/api/virtualization/clusters/",
)
)
tenant_group = DynamicModelMultipleChoiceField(
queryset=TenantGroup.objects.all(), queryset=TenantGroup.objects.all(),
to_field_name='slug', to_field_name='slug',
required=False,
widget=APISelectMultiple( widget=APISelectMultiple(
api_url="/api/tenancy/tenant-groups/", api_url="/api/tenancy/tenant-groups/",
value_field="slug", value_field="slug",
) )
) )
tenant = FilterChoiceField( tenant = DynamicModelMultipleChoiceField(
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
to_field_name='slug', to_field_name='slug',
required=False,
widget=APISelectMultiple( widget=APISelectMultiple(
api_url="/api/tenancy/tenants/", api_url="/api/tenancy/tenants/",
value_field="slug", value_field="slug",
) )
) )
tag = FilterChoiceField( tag = DynamicModelMultipleChoiceField(
queryset=Tag.objects.all(), queryset=Tag.objects.all(),
to_field_name='slug', to_field_name='slug',
required=False,
widget=APISelectMultiple( widget=APISelectMultiple(
api_url="/api/extras/tags/", api_url="/api/extras/tags/",
value_field="slug", value_field="slug",
@ -415,11 +425,14 @@ class ObjectChangeFilterForm(BootstrapMixin, forms.Form):
) )
action = forms.ChoiceField( action = forms.ChoiceField(
choices=add_blank_choice(ObjectChangeActionChoices), choices=add_blank_choice(ObjectChangeActionChoices),
required=False required=False,
widget=StaticSelect2()
) )
# TODO: Convert to DynamicModelMultipleChoiceField once we have an API endpoint for users
user = forms.ModelChoiceField( user = forms.ModelChoiceField(
queryset=User.objects.order_by('username'), queryset=User.objects.order_by('username'),
required=False required=False,
widget=StaticSelect2()
) )
changed_object_type = forms.ModelChoiceField( changed_object_type = forms.ModelChoiceField(
queryset=ContentType.objects.order_by('model'), queryset=ContentType.objects.order_by('model'),

View File

@ -0,0 +1,111 @@
from django.apps import apps
from django.core.management.base import BaseCommand, CommandError
from utilities.fields import NaturalOrderingField
class Command(BaseCommand):
help = "Recalculate natural ordering values for the specified models"
def add_arguments(self, parser):
parser.add_argument(
'args', metavar='app_label.ModelName', nargs='*',
help='One or more specific models (each prefixed with its app_label) to renaturalize',
)
def _get_models(self, names):
"""
Compile a list of models to be renaturalized. If no names are specified, all models which have one or more
NaturalOrderingFields will be included.
"""
models = []
if names:
# Collect all NaturalOrderingFields present on the specified models
for name in names:
try:
app_label, model_name = name.split('.')
except ValueError:
raise CommandError(
"Invalid format: {}. Models must be specified in the form app_label.ModelName.".format(name)
)
try:
app_config = apps.get_app_config(app_label)
except LookupError as e:
raise CommandError(str(e))
try:
model = app_config.get_model(model_name)
except LookupError:
raise CommandError("Unknown model: {}.{}".format(app_label, model_name))
fields = [
field for field in model._meta.concrete_fields if type(field) is NaturalOrderingField
]
if not fields:
raise CommandError(
"Invalid model: {}.{} does not employ natural ordering".format(app_label, model_name)
)
models.append(
(model, fields)
)
else:
# Find *all* models with NaturalOrderingFields
for app_config in apps.get_app_configs():
for model in app_config.models.values():
fields = [
field for field in model._meta.concrete_fields if type(field) is NaturalOrderingField
]
if fields:
models.append(
(model, fields)
)
return models
def handle(self, *args, **options):
models = self._get_models(args)
if options['verbosity']:
self.stdout.write("Renaturalizing {} models.".format(len(models)))
for model, fields in models:
for field in fields:
target_field = field.target_field
naturalize = field.naturalize_function
count = 0
# Print the model and field name
if options['verbosity']:
self.stdout.write(
"{}.{} ({})... ".format(model._meta.label, field.target_field, field.name),
ending='\n' if options['verbosity'] >= 2 else ''
)
self.stdout.flush()
# Find all unique values for the field
queryset = model.objects.values_list(target_field, flat=True).order_by(target_field).distinct()
for value in queryset:
naturalized_value = naturalize(value, max_length=field.max_length)
if options['verbosity'] >= 2:
self.stdout.write(" {} -> {}".format(value, naturalized_value), ending='')
self.stdout.flush()
# Update each unique field value in bulk
changed = model.objects.filter(name=value).update(**{field.name: naturalized_value})
if options['verbosity'] >= 2:
self.stdout.write(" ({})".format(changed))
count += changed
# Print the total count of alterations for the field
if options['verbosity'] >= 2:
self.stdout.write(self.style.SUCCESS("{} {} updated ({} unique values)".format(
count, model._meta.verbose_name_plural, queryset.count()
)))
elif options['verbosity']:
self.stdout.write(self.style.SUCCESS(str(count)))
if options['verbosity']:
self.stdout.write(self.style.SUCCESS("Done."))

View File

@ -0,0 +1,24 @@
# Generated by Django 2.2.8 on 2020-01-17 18:11
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('virtualization', '0013_deterministic_ordering'),
('extras', '0036_contenttype_filters_to_q_objects'),
]
operations = [
migrations.AddField(
model_name='configcontext',
name='cluster_groups',
field=models.ManyToManyField(blank=True, related_name='_configcontext_cluster_groups_+', to='virtualization.ClusterGroup'),
),
migrations.AddField(
model_name='configcontext',
name='clusters',
field=models.ManyToManyField(blank=True, related_name='_configcontext_clusters_+', to='virtualization.Cluster'),
),
]

View File

@ -1,6 +1,7 @@
from collections import OrderedDict from collections import OrderedDict
from datetime import date from datetime import date
from django import forms
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
@ -14,6 +15,7 @@ from django.utils.text import slugify
from taggit.models import TagBase, GenericTaggedItemBase from taggit.models import TagBase, GenericTaggedItemBase
from utilities.fields import ColorField from utilities.fields import ColorField
from utilities.forms import CSVChoiceField, DatePicker, LaxURLField, StaticSelect2, add_blank_choice
from utilities.utils import deepmerge, render_jinja2 from utilities.utils import deepmerge, render_jinja2
from .choices import * from .choices import *
from .constants import * from .constants import *
@ -280,6 +282,75 @@ class CustomField(models.Model):
return self.choices.get(pk=int(serialized_value)) return self.choices.get(pk=int(serialized_value))
return serialized_value return serialized_value
def to_form_field(self, set_initial=True, enforce_required=True, for_csv_import=False):
"""
Return a form field suitable for setting a CustomField's value for an object.
set_initial: Set initial date for the field. This should be False when generating a field for bulk editing.
enforce_required: Honor the value of CustomField.required. Set to False for filtering/bulk editing.
for_csv_import: Return a form field suitable for bulk import of objects in CSV format.
"""
initial = self.default if set_initial else None
required = self.required if enforce_required else False
# Integer
if self.type == CustomFieldTypeChoices.TYPE_INTEGER:
field = forms.IntegerField(required=required, initial=initial)
# Boolean
elif self.type == CustomFieldTypeChoices.TYPE_BOOLEAN:
choices = (
(None, '---------'),
(1, 'True'),
(0, 'False'),
)
if initial is not None and initial.lower() in ['true', 'yes', '1']:
initial = 1
elif initial is not None and initial.lower() in ['false', 'no', '0']:
initial = 0
else:
initial = None
field = forms.NullBooleanField(
required=required, initial=initial, widget=StaticSelect2(choices=choices)
)
# Date
elif self.type == CustomFieldTypeChoices.TYPE_DATE:
field = forms.DateField(required=required, initial=initial, widget=DatePicker())
# Select
elif self.type == CustomFieldTypeChoices.TYPE_SELECT:
choices = [(cfc.pk, cfc.value) for cfc in self.choices.all()]
if not required:
choices = add_blank_choice(choices)
# Set the initial value to the PK of the default choice, if any
if set_initial:
default_choice = self.choices.filter(value=self.default).first()
if default_choice:
initial = default_choice.pk
field_class = CSVChoiceField if for_csv_import else forms.ChoiceField
field = field_class(
choices=choices, required=required, initial=initial, widget=StaticSelect2()
)
# URL
elif self.type == CustomFieldTypeChoices.TYPE_URL:
field = LaxURLField(required=required, initial=initial)
# Text
else:
field = forms.CharField(max_length=255, required=required, initial=initial)
field.model = self
field.label = self.label if self.label else self.name.replace('_', ' ').capitalize()
if self.description:
field.help_text = self.description
return field
class CustomFieldValue(models.Model): class CustomFieldValue(models.Model):
field = models.ForeignKey( field = models.ForeignKey(
@ -694,6 +765,16 @@ class ConfigContext(models.Model):
related_name='+', related_name='+',
blank=True blank=True
) )
cluster_groups = models.ManyToManyField(
to='virtualization.ClusterGroup',
related_name='+',
blank=True
)
clusters = models.ManyToManyField(
to='virtualization.Cluster',
related_name='+',
blank=True
)
tenant_groups = models.ManyToManyField( tenant_groups = models.ManyToManyField(
to='tenancy.TenantGroup', to='tenancy.TenantGroup',
related_name='+', related_name='+',

View File

@ -29,6 +29,10 @@ class ConfigContextQuerySet(QuerySet):
# `device_role` for Device; `role` for VirtualMachine # `device_role` for Device; `role` for VirtualMachine
role = getattr(obj, 'device_role', None) or obj.role role = getattr(obj, 'device_role', None) or obj.role
# Virtualization cluster for VirtualMachine
cluster = getattr(obj, 'cluster', None)
cluster_group = getattr(cluster, 'group', None)
# Get the group of the assigned tenant, if any # Get the group of the assigned tenant, if any
tenant_group = obj.tenant.group if obj.tenant else None tenant_group = obj.tenant.group if obj.tenant else None
@ -44,6 +48,8 @@ class ConfigContextQuerySet(QuerySet):
Q(sites=obj.site) | Q(sites=None), Q(sites=obj.site) | Q(sites=None),
Q(roles=role) | Q(roles=None), Q(roles=role) | Q(roles=None),
Q(platforms=obj.platform) | Q(platforms=None), Q(platforms=obj.platform) | Q(platforms=None),
Q(cluster_groups=cluster_group) | Q(cluster_groups=None),
Q(clusters=cluster) | Q(clusters=None),
Q(tenant_groups=tenant_group) | Q(tenant_groups=None), Q(tenant_groups=tenant_group) | Q(tenant_groups=None),
Q(tenants=obj.tenant) | Q(tenants=None), Q(tenants=obj.tenant) | Q(tenants=None),
Q(tags__slug__in=obj.tags.slugs()) | Q(tags=None), Q(tags__slug__in=obj.tags.slugs()) | Q(tags=None),

View File

@ -48,7 +48,7 @@ class ScriptVariable:
""" """
form_field = forms.CharField form_field = forms.CharField
def __init__(self, label='', description='', default=None, required=True): def __init__(self, label='', description='', default=None, required=True, widget=None):
# Initialize field attributes # Initialize field attributes
if not hasattr(self, 'field_attrs'): if not hasattr(self, 'field_attrs'):
@ -59,6 +59,8 @@ class ScriptVariable:
self.field_attrs['help_text'] = description self.field_attrs['help_text'] = description
if default: if default:
self.field_attrs['initial'] = default self.field_attrs['initial'] = default
if widget:
self.field_attrs['widget'] = widget
self.field_attrs['required'] = required self.field_attrs['required'] = required
# Initialize the list of optional validators if none have already been defined # Initialize the list of optional validators if none have already been defined
@ -71,6 +73,9 @@ class ScriptVariable:
""" """
form_field = self.form_field(**self.field_attrs) form_field = self.form_field(**self.field_attrs)
if not isinstance(form_field.widget, forms.CheckboxInput): if not isinstance(form_field.widget, forms.CheckboxInput):
if form_field.widget.attrs and 'class' in form_field.widget.attrs.keys():
form_field.widget.attrs['class'] += ' form-control'
else:
form_field.widget.attrs['class'] = 'form-control' form_field.widget.attrs['class'] = 'form-control'
return form_field return form_field

View File

@ -1,14 +1,15 @@
from datetime import date from datetime import date
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.test import TestCase from django.test import Client, TestCase
from django.urls import reverse from django.urls import reverse
from rest_framework import status from rest_framework import status
from dcim.forms import SiteCSVForm
from dcim.models import Site from dcim.models import Site
from extras.choices import * from extras.choices import *
from extras.models import CustomField, CustomFieldValue, CustomFieldChoice from extras.models import CustomField, CustomFieldValue, CustomFieldChoice
from utilities.testing import APITestCase from utilities.testing import APITestCase, create_test_user
from virtualization.models import VirtualMachine from virtualization.models import VirtualMachine
@ -364,3 +365,113 @@ class CustomFieldChoiceAPITest(APITestCase):
self.assertEqual(self.cf_choice_1.pk, response.data[self.cf_1.name][self.cf_choice_1.value]) self.assertEqual(self.cf_choice_1.pk, response.data[self.cf_1.name][self.cf_choice_1.value])
self.assertEqual(self.cf_choice_2.pk, response.data[self.cf_1.name][self.cf_choice_2.value]) self.assertEqual(self.cf_choice_2.pk, response.data[self.cf_1.name][self.cf_choice_2.value])
self.assertEqual(self.cf_choice_3.pk, response.data[self.cf_2.name][self.cf_choice_3.value]) self.assertEqual(self.cf_choice_3.pk, response.data[self.cf_2.name][self.cf_choice_3.value])
class CustomFieldImportTest(TestCase):
def setUp(self):
user = create_test_user(
permissions=[
'dcim.view_site',
'dcim.add_site',
]
)
self.client = Client()
self.client.force_login(user)
@classmethod
def setUpTestData(cls):
custom_fields = (
CustomField(name='text', type=CustomFieldTypeChoices.TYPE_TEXT),
CustomField(name='integer', type=CustomFieldTypeChoices.TYPE_INTEGER),
CustomField(name='boolean', type=CustomFieldTypeChoices.TYPE_BOOLEAN),
CustomField(name='date', type=CustomFieldTypeChoices.TYPE_DATE),
CustomField(name='url', type=CustomFieldTypeChoices.TYPE_URL),
CustomField(name='select', type=CustomFieldTypeChoices.TYPE_SELECT),
)
for cf in custom_fields:
cf.save()
cf.obj_type.set([ContentType.objects.get_for_model(Site)])
CustomFieldChoice.objects.bulk_create((
CustomFieldChoice(field=custom_fields[5], value='Choice A'),
CustomFieldChoice(field=custom_fields[5], value='Choice B'),
CustomFieldChoice(field=custom_fields[5], value='Choice C'),
))
def test_import(self):
"""
Import a Site in CSV format, including a value for each CustomField.
"""
data = (
('name', 'slug', 'cf_text', 'cf_integer', 'cf_boolean', 'cf_date', 'cf_url', 'cf_select'),
('Site 1', 'site-1', 'ABC', '123', 'True', '2020-01-01', 'http://example.com/1', 'Choice A'),
('Site 2', 'site-2', 'DEF', '456', 'False', '2020-01-02', 'http://example.com/2', 'Choice B'),
('Site 3', 'site-3', '', '', '', '', '', ''),
)
csv_data = '\n'.join(','.join(row) for row in data)
response = self.client.post(reverse('dcim:site_import'), {'csv': csv_data})
self.assertEqual(response.status_code, 200)
# Validate data for site 1
custom_field_values = {
cf.name: value for cf, value in Site.objects.get(name='Site 1').get_custom_fields().items()
}
self.assertEqual(len(custom_field_values), 6)
self.assertEqual(custom_field_values['text'], 'ABC')
self.assertEqual(custom_field_values['integer'], 123)
self.assertEqual(custom_field_values['boolean'], True)
self.assertEqual(custom_field_values['date'], date(2020, 1, 1))
self.assertEqual(custom_field_values['url'], 'http://example.com/1')
self.assertEqual(custom_field_values['select'].value, 'Choice A')
# Validate data for site 2
custom_field_values = {
cf.name: value for cf, value in Site.objects.get(name='Site 2').get_custom_fields().items()
}
self.assertEqual(len(custom_field_values), 6)
self.assertEqual(custom_field_values['text'], 'DEF')
self.assertEqual(custom_field_values['integer'], 456)
self.assertEqual(custom_field_values['boolean'], False)
self.assertEqual(custom_field_values['date'], date(2020, 1, 2))
self.assertEqual(custom_field_values['url'], 'http://example.com/2')
self.assertEqual(custom_field_values['select'].value, 'Choice B')
# No CustomFieldValues should be created for site 3
obj_type = ContentType.objects.get_for_model(Site)
site3 = Site.objects.get(name='Site 3')
self.assertFalse(CustomFieldValue.objects.filter(obj_type=obj_type, obj_id=site3.pk).exists())
self.assertEqual(CustomFieldValue.objects.count(), 12) # Sanity check
def test_import_missing_required(self):
"""
Attempt to import an object missing a required custom field.
"""
# Set one of our CustomFields to required
CustomField.objects.filter(name='text').update(required=True)
form_data = {
'name': 'Site 1',
'slug': 'site-1',
}
form = SiteCSVForm(data=form_data)
self.assertFalse(form.is_valid())
self.assertIn('cf_text', form.errors)
def test_import_invalid_choice(self):
"""
Attempt to import an object with an invalid choice selection.
"""
form_data = {
'name': 'Site 1',
'slug': 'site-1',
'cf_select': 'Choice X'
}
form = SiteCSVForm(data=form_data)
self.assertFalse(form.is_valid())
self.assertIn('cf_select', form.errors)

View File

@ -7,6 +7,7 @@ from extras.constants import GRAPH_MODELS
from extras.filters import * from extras.filters import *
from extras.models import ConfigContext, ExportTemplate, Graph from extras.models import ConfigContext, ExportTemplate, Graph
from tenancy.models import Tenant, TenantGroup from tenancy.models import Tenant, TenantGroup
from virtualization.models import Cluster, ClusterGroup, ClusterType
class GraphTestCase(TestCase): class GraphTestCase(TestCase):
@ -107,6 +108,21 @@ class ConfigContextTestCase(TestCase):
) )
Platform.objects.bulk_create(platforms) Platform.objects.bulk_create(platforms)
cluster_groups = (
ClusterGroup(name='Cluster Group 1', slug='cluster-group-1'),
ClusterGroup(name='Cluster Group 2', slug='cluster-group-2'),
ClusterGroup(name='Cluster Group 3', slug='cluster-group-3'),
)
ClusterGroup.objects.bulk_create(cluster_groups)
cluster_type = ClusterType.objects.create(name='Cluster Type 1', slug='cluster-type-1')
clusters = (
Cluster(name='Cluster 1', type=cluster_type),
Cluster(name='Cluster 2', type=cluster_type),
Cluster(name='Cluster 3', type=cluster_type),
)
Cluster.objects.bulk_create(clusters)
tenant_groups = ( tenant_groups = (
TenantGroup(name='Tenant Group 1', slug='tenant-group-1'), TenantGroup(name='Tenant Group 1', slug='tenant-group-1'),
TenantGroup(name='Tenant Group 2', slug='tenant-group-2'), TenantGroup(name='Tenant Group 2', slug='tenant-group-2'),
@ -132,6 +148,8 @@ class ConfigContextTestCase(TestCase):
c.sites.set([sites[i]]) c.sites.set([sites[i]])
c.roles.set([device_roles[i]]) c.roles.set([device_roles[i]])
c.platforms.set([platforms[i]]) c.platforms.set([platforms[i]])
c.cluster_groups.set([cluster_groups[i]])
c.clusters.set([clusters[i]])
c.tenant_groups.set([tenant_groups[i]]) c.tenant_groups.set([tenant_groups[i]])
c.tenants.set([tenants[i]]) c.tenants.set([tenants[i]])
@ -173,6 +191,18 @@ class ConfigContextTestCase(TestCase):
params = {'platform': [platforms[0].slug, platforms[1].slug]} params = {'platform': [platforms[0].slug, platforms[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_cluster_group(self):
cluster_groups = ClusterGroup.objects.all()[:2]
params = {'cluster_group_id': [cluster_groups[0].pk, cluster_groups[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'cluster_group': [cluster_groups[0].slug, cluster_groups[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_cluster(self):
clusters = Cluster.objects.all()[:2]
params = {'cluster_id': [clusters[0].pk, clusters[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_tenant_group(self): def test_tenant_group(self):
tenant_groups = TenantGroup.objects.all()[:2] tenant_groups = TenantGroup.objects.all()[:2]
params = {'tenant_group_id': [tenant_groups[0].pk, tenant_groups[1].pk]} params = {'tenant_group_id': [tenant_groups[0].pk, tenant_groups[1].pk]}

View File

@ -2,86 +2,102 @@ import urllib.parse
import uuid import uuid
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.test import Client, TestCase
from django.urls import reverse from django.urls import reverse
from dcim.models import Site from dcim.models import Site
from extras.choices import ObjectChangeActionChoices from extras.choices import ObjectChangeActionChoices
from extras.models import ConfigContext, ObjectChange, Tag from extras.models import ConfigContext, ObjectChange, Tag
from utilities.testing import create_test_user from utilities.testing import ViewTestCases, TestCase
class TagTestCase(TestCase): class TagTestCase(ViewTestCases.PrimaryObjectViewTestCase):
model = Tag
def setUp(self): # Disable inapplicable tests
user = create_test_user(permissions=['extras.view_tag']) test_create_object = None
self.client = Client() test_import_objects = None
self.client.force_login(user)
Tag.objects.bulk_create([ @classmethod
def setUpTestData(cls):
Tag.objects.bulk_create((
Tag(name='Tag 1', slug='tag-1'), Tag(name='Tag 1', slug='tag-1'),
Tag(name='Tag 2', slug='tag-2'), Tag(name='Tag 2', slug='tag-2'),
Tag(name='Tag 3', slug='tag-3'), Tag(name='Tag 3', slug='tag-3'),
]) ))
def test_tag_list(self): cls.form_data = {
'name': 'Tag X',
url = reverse('extras:tag_list') 'slug': 'tag-x',
params = { 'color': 'c0c0c0',
"q": "tag", 'comments': 'Some comments',
} }
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params))) cls.bulk_edit_data = {
self.assertEqual(response.status_code, 200) 'color': '00ff00',
}
class ConfigContextTestCase(TestCase): class ConfigContextTestCase(ViewTestCases.PrimaryObjectViewTestCase):
model = ConfigContext
def setUp(self): # Disable inapplicable tests
user = create_test_user(permissions=['extras.view_configcontext']) test_import_objects = None
self.client = Client()
self.client.force_login(user)
site = Site(name='Site 1', slug='site-1') # TODO: Resolve model discrepancies when creating/editing ConfigContexts
site.save() test_create_object = None
test_edit_object = None
@classmethod
def setUpTestData(cls):
site = Site.objects.create(name='Site 1', slug='site-1')
# Create three ConfigContexts # Create three ConfigContexts
for i in range(1, 4): for i in range(1, 4):
configcontext = ConfigContext( configcontext = ConfigContext(
name='Config Context {}'.format(i), name='Config Context {}'.format(i),
data='{{"foo": {}}}'.format(i) data={'foo': i}
) )
configcontext.save() configcontext.save()
configcontext.sites.add(site) configcontext.sites.add(site)
def test_configcontext_list(self): cls.form_data = {
'name': 'Config Context X',
url = reverse('extras:configcontext_list') 'weight': 200,
params = { 'description': 'A new config context',
"q": "foo", 'is_active': True,
'regions': [],
'sites': [site.pk],
'roles': [],
'platforms': [],
'tenant_groups': [],
'tenants': [],
'tags': [],
'data': '{"foo": 123}',
} }
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params))) cls.bulk_edit_data = {
self.assertEqual(response.status_code, 200) 'weight': 300,
'is_active': False,
def test_configcontext(self): 'description': 'New description',
}
configcontext = ConfigContext.objects.first()
response = self.client.get(configcontext.get_absolute_url())
self.assertEqual(response.status_code, 200)
# TODO: Convert to StandardTestCases.Views
class ObjectChangeTestCase(TestCase): class ObjectChangeTestCase(TestCase):
user_permissions = (
'extras.view_objectchange',
)
def setUp(self): @classmethod
user = create_test_user(permissions=['extras.view_objectchange']) def setUpTestData(cls):
self.client = Client()
self.client.force_login(user)
site = Site(name='Site 1', slug='site-1') site = Site(name='Site 1', slug='site-1')
site.save() site.save()
# Create three ObjectChanges # Create three ObjectChanges
user = User.objects.create_user(username='testuser2')
for i in range(1, 4): for i in range(1, 4):
oc = site.to_objectchange(action=ObjectChangeActionChoices.ACTION_UPDATE) oc = site.to_objectchange(action=ObjectChangeActionChoices.ACTION_UPDATE)
oc.user = user oc.user = user
@ -96,10 +112,10 @@ class ObjectChangeTestCase(TestCase):
} }
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params))) response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
self.assertEqual(response.status_code, 200) self.assertHttpStatus(response, 200)
def test_objectchange(self): def test_objectchange(self):
objectchange = ObjectChange.objects.first() objectchange = ObjectChange.objects.first()
response = self.client.get(objectchange.get_absolute_url()) response = self.client.get(objectchange.get_absolute_url())
self.assertEqual(response.status_code, 200) self.assertHttpStatus(response, 200)

View File

@ -8,38 +8,38 @@ app_name = 'extras'
urlpatterns = [ urlpatterns = [
# Tags # Tags
path(r'tags/', views.TagListView.as_view(), name='tag_list'), path('tags/', views.TagListView.as_view(), name='tag_list'),
path(r'tags/edit/', views.TagBulkEditView.as_view(), name='tag_bulk_edit'), path('tags/edit/', views.TagBulkEditView.as_view(), name='tag_bulk_edit'),
path(r'tags/delete/', views.TagBulkDeleteView.as_view(), name='tag_bulk_delete'), path('tags/delete/', views.TagBulkDeleteView.as_view(), name='tag_bulk_delete'),
path(r'tags/<str:slug>/', views.TagView.as_view(), name='tag'), path('tags/<str:slug>/', views.TagView.as_view(), name='tag'),
path(r'tags/<str:slug>/edit/', views.TagEditView.as_view(), name='tag_edit'), path('tags/<str:slug>/edit/', views.TagEditView.as_view(), name='tag_edit'),
path(r'tags/<str:slug>/delete/', views.TagDeleteView.as_view(), name='tag_delete'), path('tags/<str:slug>/delete/', views.TagDeleteView.as_view(), name='tag_delete'),
path(r'tags/<str:slug>/changelog/', views.ObjectChangeLogView.as_view(), name='tag_changelog', kwargs={'model': Tag}), path('tags/<str:slug>/changelog/', views.ObjectChangeLogView.as_view(), name='tag_changelog', kwargs={'model': Tag}),
# Config contexts # Config contexts
path(r'config-contexts/', views.ConfigContextListView.as_view(), name='configcontext_list'), path('config-contexts/', views.ConfigContextListView.as_view(), name='configcontext_list'),
path(r'config-contexts/add/', views.ConfigContextCreateView.as_view(), name='configcontext_add'), path('config-contexts/add/', views.ConfigContextCreateView.as_view(), name='configcontext_add'),
path(r'config-contexts/edit/', views.ConfigContextBulkEditView.as_view(), name='configcontext_bulk_edit'), path('config-contexts/edit/', views.ConfigContextBulkEditView.as_view(), name='configcontext_bulk_edit'),
path(r'config-contexts/delete/', views.ConfigContextBulkDeleteView.as_view(), name='configcontext_bulk_delete'), path('config-contexts/delete/', views.ConfigContextBulkDeleteView.as_view(), name='configcontext_bulk_delete'),
path(r'config-contexts/<int:pk>/', views.ConfigContextView.as_view(), name='configcontext'), path('config-contexts/<int:pk>/', views.ConfigContextView.as_view(), name='configcontext'),
path(r'config-contexts/<int:pk>/edit/', views.ConfigContextEditView.as_view(), name='configcontext_edit'), path('config-contexts/<int:pk>/edit/', views.ConfigContextEditView.as_view(), name='configcontext_edit'),
path(r'config-contexts/<int:pk>/delete/', views.ConfigContextDeleteView.as_view(), name='configcontext_delete'), path('config-contexts/<int:pk>/delete/', views.ConfigContextDeleteView.as_view(), name='configcontext_delete'),
# Image attachments # Image attachments
path(r'image-attachments/<int:pk>/edit/', views.ImageAttachmentEditView.as_view(), name='imageattachment_edit'), path('image-attachments/<int:pk>/edit/', views.ImageAttachmentEditView.as_view(), name='imageattachment_edit'),
path(r'image-attachments/<int:pk>/delete/', views.ImageAttachmentDeleteView.as_view(), name='imageattachment_delete'), path('image-attachments/<int:pk>/delete/', views.ImageAttachmentDeleteView.as_view(), name='imageattachment_delete'),
# Change logging # Change logging
path(r'changelog/', views.ObjectChangeListView.as_view(), name='objectchange_list'), path('changelog/', views.ObjectChangeListView.as_view(), name='objectchange_list'),
path(r'changelog/<int:pk>/', views.ObjectChangeView.as_view(), name='objectchange'), path('changelog/<int:pk>/', views.ObjectChangeView.as_view(), name='objectchange'),
# Reports # Reports
path(r'reports/', views.ReportListView.as_view(), name='report_list'), path('reports/', views.ReportListView.as_view(), name='report_list'),
path(r'reports/<str:name>/', views.ReportView.as_view(), name='report'), path('reports/<str:name>/', views.ReportView.as_view(), name='report'),
path(r'reports/<str:name>/run/', views.ReportRunView.as_view(), name='report_run'), path('reports/<str:name>/run/', views.ReportRunView.as_view(), name='report_run'),
# Scripts # Scripts
path(r'scripts/', views.ScriptListView.as_view(), name='script_list'), path('scripts/', views.ScriptListView.as_view(), name='script_list'),
path(r'scripts/<str:module>/<str:name>/', views.ScriptView.as_view(), name='script'), path('scripts/<str:module>/<str:name>/', views.ScriptView.as_view(), name='script'),
] ]

View File

@ -34,10 +34,11 @@ class TagListView(PermissionRequiredMixin, ObjectListView):
filterset = filters.TagFilterSet filterset = filters.TagFilterSet
filterset_form = forms.TagFilterForm filterset_form = forms.TagFilterForm
table = TagTable table = TagTable
template_name = 'extras/tag_list.html' action_buttons = ()
class TagView(View): class TagView(PermissionRequiredMixin, View):
permission_required = 'extras.view_tag'
def get(self, request, slug): def get(self, request, slug):
@ -84,10 +85,9 @@ class TagBulkEditView(PermissionRequiredMixin, BulkEditView):
).order_by( ).order_by(
'name' 'name'
) )
# filter = filters.ProviderFilter
table = TagTable table = TagTable
form = forms.TagBulkEditForm form = forms.TagBulkEditForm
default_return_url = 'circuits:provider_list' default_return_url = 'extras:tag_list'
class TagBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class TagBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
@ -111,7 +111,7 @@ class ConfigContextListView(PermissionRequiredMixin, ObjectListView):
filterset = filters.ConfigContextFilterSet filterset = filters.ConfigContextFilterSet
filterset_form = forms.ConfigContextFilterForm filterset_form = forms.ConfigContextFilterForm
table = ConfigContextTable table = ConfigContextTable
template_name = 'extras/configcontext_list.html' action_buttons = ('add',)
class ConfigContextView(PermissionRequiredMixin, View): class ConfigContextView(PermissionRequiredMixin, View):
@ -191,6 +191,7 @@ class ObjectChangeListView(PermissionRequiredMixin, ObjectListView):
filterset_form = forms.ObjectChangeFilterForm filterset_form = forms.ObjectChangeFilterForm
table = ObjectChangeTable table = ObjectChangeTable
template_name = 'extras/objectchange_list.html' template_name = 'extras/objectchange_list.html'
action_buttons = ('export',)
class ObjectChangeView(PermissionRequiredMixin, View): class ObjectChangeView(PermissionRequiredMixin, View):

View File

@ -202,7 +202,7 @@ class IPAddressSerializer(TaggitSerializer, CustomFieldModelSerializer):
vrf = NestedVRFSerializer(required=False, allow_null=True) vrf = NestedVRFSerializer(required=False, allow_null=True)
tenant = NestedTenantSerializer(required=False, allow_null=True) tenant = NestedTenantSerializer(required=False, allow_null=True)
status = ChoiceField(choices=IPAddressStatusChoices, required=False) status = ChoiceField(choices=IPAddressStatusChoices, required=False)
role = ChoiceField(choices=IPAddressRoleChoices, required=False, allow_null=True) role = ChoiceField(choices=IPAddressRoleChoices, allow_blank=True, required=False)
interface = IPAddressInterfaceSerializer(required=False, allow_null=True) interface = IPAddressInterfaceSerializer(required=False, allow_null=True)
nat_inside = NestedIPAddressSerializer(required=False, allow_null=True) nat_inside = NestedIPAddressSerializer(required=False, allow_null=True)
nat_outside = NestedIPAddressSerializer(read_only=True) nat_outside = NestedIPAddressSerializer(read_only=True)
@ -240,7 +240,7 @@ class AvailableIPSerializer(serializers.Serializer):
class ServiceSerializer(TaggitSerializer, CustomFieldModelSerializer): class ServiceSerializer(TaggitSerializer, CustomFieldModelSerializer):
device = NestedDeviceSerializer(required=False, allow_null=True) device = NestedDeviceSerializer(required=False, allow_null=True)
virtual_machine = NestedVirtualMachineSerializer(required=False, allow_null=True) virtual_machine = NestedVirtualMachineSerializer(required=False, allow_null=True)
protocol = ChoiceField(choices=ServiceProtocolChoices) protocol = ChoiceField(choices=ServiceProtocolChoices, required=False)
ipaddresses = SerializedPKRelatedField( ipaddresses = SerializedPKRelatedField(
queryset=IPAddress.objects.all(), queryset=IPAddress.objects.all(),
serializer=NestedIPAddressSerializer, serializer=NestedIPAddressSerializer,

View File

@ -15,30 +15,30 @@ router = routers.DefaultRouter()
router.APIRootView = IPAMRootView router.APIRootView = IPAMRootView
# Field choices # Field choices
router.register(r'_choices', views.IPAMFieldChoicesViewSet, basename='field-choice') router.register('_choices', views.IPAMFieldChoicesViewSet, basename='field-choice')
# VRFs # VRFs
router.register(r'vrfs', views.VRFViewSet) router.register('vrfs', views.VRFViewSet)
# RIRs # RIRs
router.register(r'rirs', views.RIRViewSet) router.register('rirs', views.RIRViewSet)
# Aggregates # Aggregates
router.register(r'aggregates', views.AggregateViewSet) router.register('aggregates', views.AggregateViewSet)
# Prefixes # Prefixes
router.register(r'roles', views.RoleViewSet) router.register('roles', views.RoleViewSet)
router.register(r'prefixes', views.PrefixViewSet) router.register('prefixes', views.PrefixViewSet)
# IP addresses # IP addresses
router.register(r'ip-addresses', views.IPAddressViewSet) router.register('ip-addresses', views.IPAddressViewSet)
# VLANs # VLANs
router.register(r'vlan-groups', views.VLANGroupViewSet) router.register('vlan-groups', views.VLANGroupViewSet)
router.register(r'vlans', views.VLANViewSet) router.register('vlans', views.VLANViewSet)
# Services # Services
router.register(r'services', views.ServiceViewSet) router.register('services', views.ServiceViewSet)
app_name = 'ipam-api' app_name = 'ipam-api'
urlpatterns = router.urls urlpatterns = router.urls

View File

@ -1,6 +1,7 @@
from django.conf import settings from django.conf import settings
from django.db.models import Count from django.db.models import Count
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django_pglocks import advisory_lock
from rest_framework import status 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
@ -10,6 +11,7 @@ from extras.api.views import CustomFieldModelViewSet
from ipam import filters from ipam import filters
from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
from utilities.api import FieldChoicesViewSet, ModelViewSet from utilities.api import FieldChoicesViewSet, ModelViewSet
from utilities.constants import ADVISORY_LOCK_KEYS
from utilities.utils import get_subquery from utilities.utils import get_subquery
from . import serializers from . import serializers
@ -86,9 +88,13 @@ class PrefixViewSet(CustomFieldModelViewSet):
filterset_class = filters.PrefixFilterSet filterset_class = filters.PrefixFilterSet
@action(detail=True, url_path='available-prefixes', methods=['get', 'post']) @action(detail=True, url_path='available-prefixes', methods=['get', 'post'])
@advisory_lock(ADVISORY_LOCK_KEYS['available-prefixes'])
def available_prefixes(self, request, pk=None): def available_prefixes(self, request, pk=None):
""" """
A convenience method for returning available child prefixes within a parent. A convenience method for returning available child prefixes within a parent.
The advisory lock decorator uses a PostgreSQL advisory lock to prevent this API from being
invoked in parallel, which results in a race condition where multiple insertions can occur.
""" """
prefix = get_object_or_404(Prefix, pk=pk) prefix = get_object_or_404(Prefix, pk=pk)
available_prefixes = prefix.get_available_prefixes() available_prefixes = prefix.get_available_prefixes()
@ -180,11 +186,15 @@ class PrefixViewSet(CustomFieldModelViewSet):
return Response(serializer.data) return Response(serializer.data)
@action(detail=True, url_path='available-ips', methods=['get', 'post']) @action(detail=True, url_path='available-ips', methods=['get', 'post'])
@advisory_lock(ADVISORY_LOCK_KEYS['available-ips'])
def available_ips(self, request, pk=None): def available_ips(self, request, pk=None):
""" """
A convenience method for returning available IP addresses within a prefix. By default, the number of IPs A convenience method for returning available IP addresses within a prefix. By default, the number of IPs
returned will be equivalent to PAGINATE_COUNT. An arbitrary limit (up to MAX_PAGE_SIZE, if set) may be passed, returned will be equivalent to PAGINATE_COUNT. An arbitrary limit (up to MAX_PAGE_SIZE, if set) may be passed,
however results will not be paginated. however results will not be paginated.
The advisory lock decorator uses a PostgreSQL advisory lock to prevent this API from being
invoked in parallel, which results in a race condition where multiple insertions can occur.
""" """
prefix = get_object_or_404(Prefix, pk=pk) prefix = get_object_or_404(Prefix, pk=pk)

View File

@ -8,7 +8,7 @@ from dcim.models import Device, Interface, Region, Site
from extras.filters import CustomFieldFilterSet, CreatedUpdatedFilterSet from extras.filters import CustomFieldFilterSet, CreatedUpdatedFilterSet
from tenancy.filters import TenancyFilterSet from tenancy.filters import TenancyFilterSet
from utilities.filters import ( from utilities.filters import (
MultiValueCharFilter, NameSlugSearchFilterSet, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter, MultiValueCharFilter, MultiValueNumberFilter, NameSlugSearchFilterSet, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter,
) )
from virtualization.models import VirtualMachine from virtualization.models import VirtualMachine
from .choices import * from .choices import *
@ -304,12 +304,12 @@ class IPAddressFilterSet(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedF
to_field_name='rd', to_field_name='rd',
label='VRF (RD)', label='VRF (RD)',
) )
device = django_filters.CharFilter( device = MultiValueCharFilter(
method='filter_device', method='filter_device',
field_name='name', field_name='name',
label='Device', label='Device (name)',
) )
device_id = django_filters.NumberFilter( device_id = MultiValueNumberFilter(
method='filter_device', method='filter_device',
field_name='pk', field_name='pk',
label='Device (ID)', label='Device (ID)',
@ -385,8 +385,10 @@ class IPAddressFilterSet(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedF
def filter_device(self, queryset, name, value): def filter_device(self, queryset, name, value):
try: try:
device = Device.objects.prefetch_related('device_type').get(**{name: value}) devices = Device.objects.prefetch_related('device_type').filter(**{'{}__in'.format(name): value})
vc_interface_ids = [i['id'] for i in device.vc_interfaces.values('id')] vc_interface_ids = []
for device in devices:
vc_interface_ids.extend([i['id'] for i in device.vc_interfaces.values('id')])
return queryset.filter(interface_id__in=vc_interface_ids) return queryset.filter(interface_id__in=vc_interface_ids)
except Device.DoesNotExist: except Device.DoesNotExist:
return queryset.none() return queryset.none()

View File

@ -4,13 +4,16 @@ from django.core.validators import MaxValueValidator, MinValueValidator
from taggit.forms import TagField from taggit.forms import TagField
from dcim.models import Device, Interface, Rack, Region, Site from dcim.models import Device, Interface, Rack, Region, Site
from extras.forms import AddRemoveTagsForm, CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm from extras.forms import (
AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldModelCSVForm, CustomFieldModelForm, CustomFieldFilterForm,
)
from tenancy.forms import TenancyFilterForm, TenancyForm from tenancy.forms import TenancyFilterForm, TenancyForm
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.forms import ( from utilities.forms import (
add_blank_choice, APISelect, APISelectMultiple, BootstrapMixin, BulkEditNullBooleanSelect, ChainedModelChoiceField, add_blank_choice, APISelect, APISelectMultiple, BootstrapMixin, BulkEditNullBooleanSelect, CSVChoiceField,
CSVChoiceField, DatePicker, ExpandableIPAddressField, FilterChoiceField, FlexibleModelChoiceField, ReturnURLForm, DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableIPAddressField,
SlugField, StaticSelect2, StaticSelect2Multiple, BOOLEAN_WITH_BLANK_CHOICES FlexibleModelChoiceField, ReturnURLForm, SlugField, StaticSelect2, StaticSelect2Multiple, TagFilterField,
BOOLEAN_WITH_BLANK_CHOICES,
) )
from virtualization.models import VirtualMachine from virtualization.models import VirtualMachine
from .constants import * from .constants import *
@ -31,7 +34,7 @@ IPADDRESS_MASK_LENGTH_CHOICES = add_blank_choice([
# VRFs # VRFs
# #
class VRFForm(BootstrapMixin, TenancyForm, CustomFieldForm): class VRFForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
tags = TagField( tags = TagField(
required=False required=False
) )
@ -49,7 +52,7 @@ class VRFForm(BootstrapMixin, TenancyForm, CustomFieldForm):
} }
class VRFCSVForm(forms.ModelForm): class VRFCSVForm(CustomFieldModelCSVForm):
tenant = forms.ModelChoiceField( tenant = forms.ModelChoiceField(
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
required=False, required=False,
@ -73,7 +76,7 @@ class VRFBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm
queryset=VRF.objects.all(), queryset=VRF.objects.all(),
widget=forms.MultipleHiddenInput() widget=forms.MultipleHiddenInput()
) )
tenant = forms.ModelChoiceField( tenant = DynamicModelChoiceField(
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
required=False, required=False,
widget=APISelect( widget=APISelect(
@ -103,6 +106,7 @@ class VRFFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
required=False, required=False,
label='Search' label='Search'
) )
tag = TagFilterField(model)
# #
@ -144,7 +148,13 @@ class RIRFilterForm(BootstrapMixin, forms.Form):
# Aggregates # Aggregates
# #
class AggregateForm(BootstrapMixin, CustomFieldForm): class AggregateForm(BootstrapMixin, CustomFieldModelForm):
rir = DynamicModelChoiceField(
queryset=RIR.objects.all(),
widget=APISelect(
api_url="/api/ipam/rirs/"
)
)
tags = TagField( tags = TagField(
required=False required=False
) )
@ -159,14 +169,11 @@ class AggregateForm(BootstrapMixin, CustomFieldForm):
'rir': "Regional Internet Registry responsible for this prefix", 'rir': "Regional Internet Registry responsible for this prefix",
} }
widgets = { widgets = {
'rir': APISelect(
api_url="/api/ipam/rirs/"
),
'date_added': DatePicker(), 'date_added': DatePicker(),
} }
class AggregateCSVForm(forms.ModelForm): class AggregateCSVForm(CustomFieldModelCSVForm):
rir = forms.ModelChoiceField( rir = forms.ModelChoiceField(
queryset=RIR.objects.all(), queryset=RIR.objects.all(),
to_field_name='name', to_field_name='name',
@ -186,7 +193,7 @@ class AggregateBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEd
queryset=Aggregate.objects.all(), queryset=Aggregate.objects.all(),
widget=forms.MultipleHiddenInput() widget=forms.MultipleHiddenInput()
) )
rir = forms.ModelChoiceField( rir = DynamicModelChoiceField(
queryset=RIR.objects.all(), queryset=RIR.objects.all(),
required=False, required=False,
label='RIR', label='RIR',
@ -223,15 +230,17 @@ class AggregateFilterForm(BootstrapMixin, CustomFieldFilterForm):
label='Address family', label='Address family',
widget=StaticSelect2() widget=StaticSelect2()
) )
rir = FilterChoiceField( rir = DynamicModelMultipleChoiceField(
queryset=RIR.objects.all(), queryset=RIR.objects.all(),
to_field_name='slug', to_field_name='slug',
required=False,
label='RIR', label='RIR',
widget=APISelectMultiple( widget=APISelectMultiple(
api_url="/api/ipam/rirs/", api_url="/api/ipam/rirs/",
value_field="slug", value_field="slug",
) )
) )
tag = TagFilterField(model)
# #
@ -263,11 +272,17 @@ class RoleCSVForm(forms.ModelForm):
# Prefixes # Prefixes
# #
class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldForm): class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
site = forms.ModelChoiceField( vrf = DynamicModelChoiceField(
queryset=VRF.objects.all(),
required=False,
widget=APISelect(
api_url="/api/ipam/vrfs/",
)
)
site = DynamicModelChoiceField(
queryset=Site.objects.all(), queryset=Site.objects.all(),
required=False, required=False,
label='Site',
widget=APISelect( widget=APISelect(
api_url="/api/dcim/sites/", api_url="/api/dcim/sites/",
filter_for={ filter_for={
@ -279,11 +294,8 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldForm):
} }
) )
) )
vlan_group = ChainedModelChoiceField( vlan_group = DynamicModelChoiceField(
queryset=VLANGroup.objects.all(), queryset=VLANGroup.objects.all(),
chains=(
('site', 'site'),
),
required=False, required=False,
label='VLAN group', label='VLAN group',
widget=APISelect( widget=APISelect(
@ -296,12 +308,8 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldForm):
} }
) )
) )
vlan = ChainedModelChoiceField( vlan = DynamicModelChoiceField(
queryset=VLAN.objects.all(), queryset=VLAN.objects.all(),
chains=(
('site', 'site'),
('group', 'vlan_group'),
),
required=False, required=False,
label='VLAN', label='VLAN',
widget=APISelect( widget=APISelect(
@ -309,6 +317,13 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldForm):
display_field='display_name' display_field='display_name'
) )
) )
role = DynamicModelChoiceField(
queryset=Role.objects.all(),
required=False,
widget=APISelect(
api_url="/api/ipam/roles/"
)
)
tags = TagField(required=False) tags = TagField(required=False)
class Meta: class Meta:
@ -318,13 +333,7 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldForm):
'tags', 'tags',
] ]
widgets = { widgets = {
'vrf': APISelect(
api_url="/api/ipam/vrfs/"
),
'status': StaticSelect2(), 'status': StaticSelect2(),
'role': APISelect(
api_url="/api/ipam/roles/"
)
} }
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -341,7 +350,7 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldForm):
self.fields['vrf'].empty_label = 'Global' self.fields['vrf'].empty_label = 'Global'
class PrefixCSVForm(forms.ModelForm): class PrefixCSVForm(CustomFieldModelCSVForm):
vrf = FlexibleModelChoiceField( vrf = FlexibleModelChoiceField(
queryset=VRF.objects.all(), queryset=VRF.objects.all(),
to_field_name='rd', to_field_name='rd',
@ -435,14 +444,14 @@ class PrefixBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF
queryset=Prefix.objects.all(), queryset=Prefix.objects.all(),
widget=forms.MultipleHiddenInput() widget=forms.MultipleHiddenInput()
) )
site = forms.ModelChoiceField( site = DynamicModelChoiceField(
queryset=Site.objects.all(), queryset=Site.objects.all(),
required=False, required=False,
widget=APISelect( widget=APISelect(
api_url="/api/dcim/sites/" api_url="/api/dcim/sites/"
) )
) )
vrf = forms.ModelChoiceField( vrf = DynamicModelChoiceField(
queryset=VRF.objects.all(), queryset=VRF.objects.all(),
required=False, required=False,
label='VRF', label='VRF',
@ -455,7 +464,7 @@ class PrefixBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF
max_value=PREFIX_LENGTH_MAX, max_value=PREFIX_LENGTH_MAX,
required=False required=False
) )
tenant = forms.ModelChoiceField( tenant = DynamicModelChoiceField(
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
required=False, required=False,
widget=APISelect( widget=APISelect(
@ -467,7 +476,7 @@ class PrefixBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF
required=False, required=False,
widget=StaticSelect2() widget=StaticSelect2()
) )
role = forms.ModelChoiceField( role = DynamicModelChoiceField(
queryset=Role.objects.all(), queryset=Role.objects.all(),
required=False, required=False,
widget=APISelect( widget=APISelect(
@ -521,10 +530,10 @@ class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm)
label='Mask length', label='Mask length',
widget=StaticSelect2() widget=StaticSelect2()
) )
vrf_id = FilterChoiceField( vrf_id = DynamicModelMultipleChoiceField(
queryset=VRF.objects.all(), queryset=VRF.objects.all(),
required=False,
label='VRF', label='VRF',
null_label='-- Global --',
widget=APISelectMultiple( widget=APISelectMultiple(
api_url="/api/ipam/vrfs/", api_url="/api/ipam/vrfs/",
null_option=True, null_option=True,
@ -535,7 +544,7 @@ class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm)
required=False, required=False,
widget=StaticSelect2Multiple() widget=StaticSelect2Multiple()
) )
region = FilterChoiceField( region = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(), queryset=Region.objects.all(),
to_field_name='slug', to_field_name='slug',
required=False, required=False,
@ -547,20 +556,20 @@ class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm)
} }
) )
) )
site = FilterChoiceField( site = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(), queryset=Site.objects.all(),
to_field_name='slug', to_field_name='slug',
null_label='-- None --', required=False,
widget=APISelectMultiple( widget=APISelectMultiple(
api_url="/api/dcim/sites/", api_url="/api/dcim/sites/",
value_field="slug", value_field="slug",
null_option=True, null_option=True,
) )
) )
role = FilterChoiceField( role = DynamicModelMultipleChoiceField(
queryset=Role.objects.all(), queryset=Role.objects.all(),
to_field_name='slug', to_field_name='slug',
null_label='-- None --', required=False,
widget=APISelectMultiple( widget=APISelectMultiple(
api_url="/api/ipam/roles/", api_url="/api/ipam/roles/",
value_field="slug", value_field="slug",
@ -578,18 +587,27 @@ class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm)
required=False, required=False,
label='Expand prefix hierarchy' label='Expand prefix hierarchy'
) )
tag = TagFilterField(model)
# #
# IP addresses # IP addresses
# #
class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm): class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldModelForm):
interface = forms.ModelChoiceField( interface = forms.ModelChoiceField(
queryset=Interface.objects.all(), queryset=Interface.objects.all(),
required=False required=False
) )
nat_site = forms.ModelChoiceField( vrf = DynamicModelChoiceField(
queryset=VRF.objects.all(),
required=False,
label='VRF',
widget=APISelect(
api_url="/api/ipam/vrfs/"
)
)
nat_site = DynamicModelChoiceField(
queryset=Site.objects.all(), queryset=Site.objects.all(),
required=False, required=False,
label='Site', label='Site',
@ -601,11 +619,8 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm)
} }
) )
) )
nat_rack = ChainedModelChoiceField( nat_rack = DynamicModelChoiceField(
queryset=Rack.objects.all(), queryset=Rack.objects.all(),
chains=(
('site', 'nat_site'),
),
required=False, required=False,
label='Rack', label='Rack',
widget=APISelect( widget=APISelect(
@ -619,12 +634,8 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm)
} }
) )
) )
nat_device = ChainedModelChoiceField( nat_device = DynamicModelChoiceField(
queryset=Device.objects.all(), queryset=Device.objects.all(),
chains=(
('site', 'nat_site'),
('rack', 'nat_rack'),
),
required=False, required=False,
label='Device', label='Device',
widget=APISelect( widget=APISelect(
@ -646,11 +657,8 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm)
} }
) )
) )
nat_inside = ChainedModelChoiceField( nat_inside = DynamicModelChoiceField(
queryset=IPAddress.objects.all(), queryset=IPAddress.objects.all(),
chains=(
('interface__device', 'nat_device'),
),
required=False, required=False,
label='IP Address', label='IP Address',
widget=APISelect( widget=APISelect(
@ -675,9 +683,6 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm)
widgets = { widgets = {
'status': StaticSelect2(), 'status': StaticSelect2(),
'role': StaticSelect2(), 'role': StaticSelect2(),
'vrf': APISelect(
api_url="/api/ipam/vrfs/"
)
} }
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -751,7 +756,15 @@ class IPAddressBulkCreateForm(BootstrapMixin, forms.Form):
) )
class IPAddressBulkAddForm(BootstrapMixin, TenancyForm, CustomFieldForm): class IPAddressBulkAddForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
vrf = DynamicModelChoiceField(
queryset=VRF.objects.all(),
required=False,
label='VRF',
widget=APISelect(
api_url="/api/ipam/vrfs/"
)
)
class Meta: class Meta:
model = IPAddress model = IPAddress
@ -761,9 +774,6 @@ class IPAddressBulkAddForm(BootstrapMixin, TenancyForm, CustomFieldForm):
widgets = { widgets = {
'status': StaticSelect2(), 'status': StaticSelect2(),
'role': StaticSelect2(), 'role': StaticSelect2(),
'vrf': APISelect(
api_url="/api/ipam/vrfs/"
)
} }
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -771,7 +781,7 @@ class IPAddressBulkAddForm(BootstrapMixin, TenancyForm, CustomFieldForm):
self.fields['vrf'].empty_label = 'Global' self.fields['vrf'].empty_label = 'Global'
class IPAddressCSVForm(forms.ModelForm): class IPAddressCSVForm(CustomFieldModelCSVForm):
vrf = FlexibleModelChoiceField( vrf = FlexibleModelChoiceField(
queryset=VRF.objects.all(), queryset=VRF.objects.all(),
to_field_name='rd', to_field_name='rd',
@ -899,7 +909,7 @@ class IPAddressBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEd
queryset=IPAddress.objects.all(), queryset=IPAddress.objects.all(),
widget=forms.MultipleHiddenInput() widget=forms.MultipleHiddenInput()
) )
vrf = forms.ModelChoiceField( vrf = DynamicModelChoiceField(
queryset=VRF.objects.all(), queryset=VRF.objects.all(),
required=False, required=False,
label='VRF', label='VRF',
@ -912,7 +922,7 @@ class IPAddressBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEd
max_value=IPADDRESS_MASK_LENGTH_MAX, max_value=IPADDRESS_MASK_LENGTH_MAX,
required=False required=False
) )
tenant = forms.ModelChoiceField( tenant = DynamicModelChoiceField(
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
required=False, required=False,
widget=APISelect( widget=APISelect(
@ -945,7 +955,7 @@ class IPAddressBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEd
class IPAddressAssignForm(BootstrapMixin, forms.Form): class IPAddressAssignForm(BootstrapMixin, forms.Form):
vrf_id = forms.ModelChoiceField( vrf_id = DynamicModelChoiceField(
queryset=VRF.objects.all(), queryset=VRF.objects.all(),
required=False, required=False,
label='VRF', label='VRF',
@ -991,10 +1001,10 @@ class IPAddressFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterFo
label='Mask length', label='Mask length',
widget=StaticSelect2() widget=StaticSelect2()
) )
vrf_id = FilterChoiceField( vrf_id = DynamicModelMultipleChoiceField(
queryset=VRF.objects.all(), queryset=VRF.objects.all(),
required=False,
label='VRF', label='VRF',
null_label='-- Global --',
widget=APISelectMultiple( widget=APISelectMultiple(
api_url="/api/ipam/vrfs/", api_url="/api/ipam/vrfs/",
null_option=True, null_option=True,
@ -1017,6 +1027,7 @@ class IPAddressFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterFo
choices=BOOLEAN_WITH_BLANK_CHOICES choices=BOOLEAN_WITH_BLANK_CHOICES
) )
) )
tag = TagFilterField(model)
# #
@ -1024,6 +1035,13 @@ class IPAddressFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterFo
# #
class VLANGroupForm(BootstrapMixin, forms.ModelForm): class VLANGroupForm(BootstrapMixin, forms.ModelForm):
site = DynamicModelChoiceField(
queryset=Site.objects.all(),
required=False,
widget=APISelect(
api_url="/api/dcim/sites/"
)
)
slug = SlugField() slug = SlugField()
class Meta: class Meta:
@ -1031,11 +1049,6 @@ class VLANGroupForm(BootstrapMixin, forms.ModelForm):
fields = [ fields = [
'site', 'name', 'slug', 'site', 'name', 'slug',
] ]
widgets = {
'site': APISelect(
api_url="/api/dcim/sites/"
)
}
class VLANGroupCSVForm(forms.ModelForm): class VLANGroupCSVForm(forms.ModelForm):
@ -1059,7 +1072,7 @@ class VLANGroupCSVForm(forms.ModelForm):
class VLANGroupFilterForm(BootstrapMixin, forms.Form): class VLANGroupFilterForm(BootstrapMixin, forms.Form):
region = FilterChoiceField( region = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(), queryset=Region.objects.all(),
to_field_name='slug', to_field_name='slug',
required=False, required=False,
@ -1071,10 +1084,10 @@ class VLANGroupFilterForm(BootstrapMixin, forms.Form):
} }
) )
) )
site = FilterChoiceField( site = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(), queryset=Site.objects.all(),
to_field_name='slug', to_field_name='slug',
null_label='-- Global --', required=False,
widget=APISelectMultiple( widget=APISelectMultiple(
api_url="/api/dcim/sites/", api_url="/api/dcim/sites/",
value_field="slug", value_field="slug",
@ -1087,8 +1100,8 @@ class VLANGroupFilterForm(BootstrapMixin, forms.Form):
# VLANs # VLANs
# #
class VLANForm(BootstrapMixin, TenancyForm, CustomFieldForm): class VLANForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
site = forms.ModelChoiceField( site = DynamicModelChoiceField(
queryset=Site.objects.all(), queryset=Site.objects.all(),
required=False, required=False,
widget=APISelect( widget=APISelect(
@ -1101,17 +1114,20 @@ class VLANForm(BootstrapMixin, TenancyForm, CustomFieldForm):
} }
) )
) )
group = ChainedModelChoiceField( group = DynamicModelChoiceField(
queryset=VLANGroup.objects.all(), queryset=VLANGroup.objects.all(),
chains=(
('site', 'site'),
),
required=False, required=False,
label='Group',
widget=APISelect( widget=APISelect(
api_url='/api/ipam/vlan-groups/', api_url='/api/ipam/vlan-groups/',
) )
) )
role = DynamicModelChoiceField(
queryset=Role.objects.all(),
required=False,
widget=APISelect(
api_url="/api/ipam/roles/"
)
)
tags = TagField(required=False) tags = TagField(required=False)
class Meta: class Meta:
@ -1129,13 +1145,10 @@ class VLANForm(BootstrapMixin, TenancyForm, CustomFieldForm):
} }
widgets = { widgets = {
'status': StaticSelect2(), 'status': StaticSelect2(),
'role': APISelect(
api_url="/api/ipam/roles/"
)
} }
class VLANCSVForm(forms.ModelForm): class VLANCSVForm(CustomFieldModelCSVForm):
site = forms.ModelChoiceField( site = forms.ModelChoiceField(
queryset=Site.objects.all(), queryset=Site.objects.all(),
required=False, required=False,
@ -1206,21 +1219,21 @@ class VLANBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor
queryset=VLAN.objects.all(), queryset=VLAN.objects.all(),
widget=forms.MultipleHiddenInput() widget=forms.MultipleHiddenInput()
) )
site = forms.ModelChoiceField( site = DynamicModelChoiceField(
queryset=Site.objects.all(), queryset=Site.objects.all(),
required=False, required=False,
widget=APISelect( widget=APISelect(
api_url="/api/dcim/sites/" api_url="/api/dcim/sites/"
) )
) )
group = forms.ModelChoiceField( group = DynamicModelChoiceField(
queryset=VLANGroup.objects.all(), queryset=VLANGroup.objects.all(),
required=False, required=False,
widget=APISelect( widget=APISelect(
api_url="/api/ipam/vlan-groups/" api_url="/api/ipam/vlan-groups/"
) )
) )
tenant = forms.ModelChoiceField( tenant = DynamicModelChoiceField(
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
required=False, required=False,
widget=APISelect( widget=APISelect(
@ -1232,7 +1245,7 @@ class VLANBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor
required=False, required=False,
widget=StaticSelect2() widget=StaticSelect2()
) )
role = forms.ModelChoiceField( role = DynamicModelChoiceField(
queryset=Role.objects.all(), queryset=Role.objects.all(),
required=False, required=False,
widget=APISelect( widget=APISelect(
@ -1257,7 +1270,7 @@ class VLANFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
required=False, required=False,
label='Search' label='Search'
) )
region = FilterChoiceField( region = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(), queryset=Region.objects.all(),
to_field_name='slug', to_field_name='slug',
required=False, required=False,
@ -1270,20 +1283,20 @@ class VLANFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
} }
) )
) )
site = FilterChoiceField( site = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(), queryset=Site.objects.all(),
to_field_name='slug', to_field_name='slug',
null_label='-- Global --', required=False,
widget=APISelectMultiple( widget=APISelectMultiple(
api_url="/api/dcim/sites/", api_url="/api/dcim/sites/",
value_field="slug", value_field="slug",
null_option=True, null_option=True,
) )
) )
group_id = FilterChoiceField( group_id = DynamicModelMultipleChoiceField(
queryset=VLANGroup.objects.all(), queryset=VLANGroup.objects.all(),
required=False,
label='VLAN group', label='VLAN group',
null_label='-- None --',
widget=APISelectMultiple( widget=APISelectMultiple(
api_url="/api/ipam/vlan-groups/", api_url="/api/ipam/vlan-groups/",
null_option=True, null_option=True,
@ -1294,23 +1307,24 @@ class VLANFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
required=False, required=False,
widget=StaticSelect2Multiple() widget=StaticSelect2Multiple()
) )
role = FilterChoiceField( role = DynamicModelMultipleChoiceField(
queryset=Role.objects.all(), queryset=Role.objects.all(),
to_field_name='slug', to_field_name='slug',
null_label='-- None --', required=False,
widget=APISelectMultiple( widget=APISelectMultiple(
api_url="/api/ipam/roles/", api_url="/api/ipam/roles/",
value_field="slug", value_field="slug",
null_option=True, null_option=True,
) )
) )
tag = TagFilterField(model)
# #
# Services # Services
# #
class ServiceForm(BootstrapMixin, CustomFieldForm): class ServiceForm(BootstrapMixin, CustomFieldModelForm):
port = forms.IntegerField( port = forms.IntegerField(
min_value=SERVICE_PORT_MIN, min_value=SERVICE_PORT_MIN,
max_value=SERVICE_PORT_MAX max_value=SERVICE_PORT_MAX
@ -1364,6 +1378,7 @@ class ServiceFilterForm(BootstrapMixin, CustomFieldFilterForm):
port = forms.IntegerField( port = forms.IntegerField(
required=False, required=False,
) )
tag = TagFilterField(model)
class ServiceBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): class ServiceBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
@ -1390,5 +1405,5 @@ class ServiceBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
class Meta: class Meta:
nullable_fields = [ nullable_fields = [
'site', 'tenant', 'role', 'description', 'description',
] ]

View File

@ -392,13 +392,12 @@ class IPAddressTestCase(TestCase):
params = {'vrf': [vrfs[0].rd, vrfs[1].rd]} params = {'vrf': [vrfs[0].rd, vrfs[1].rd]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
# TODO: Test for multiple values
def test_device(self): def test_device(self):
device = Device.objects.first() devices = Device.objects.all()[:2]
params = {'device_id': device.pk} params = {'device_id': [devices[0].pk, devices[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'device': device.name} params = {'device': [devices[0].name, devices[1].name]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_virtual_machine(self): def test_virtual_machine(self):
vms = VirtualMachine.objects.all()[:2] vms = VirtualMachine.objects.all()[:2]

View File

@ -0,0 +1,176 @@
from django.test import TestCase
from ipam.choices import IPAddressStatusChoices, PrefixStatusChoices
from ipam.models import IPAddress, Prefix, VRF
import netaddr
class OrderingTestBase(TestCase):
vrfs = None
def setUp(self):
"""
Setup the VRFs for the class as a whole
"""
self.vrfs = (VRF(name="VRF A"), VRF(name="VRF B"), VRF(name="VRF C"))
VRF.objects.bulk_create(self.vrfs)
def _compare(self, queryset, objectset):
"""
Perform the comparison of the queryset object and the object used to instantiate the queryset.
"""
for i, obj in enumerate(queryset):
self.assertEqual(obj, objectset[i])
def _compare_ne(self, queryset, objectset):
"""
Perform the comparison of the queryset object and the object used to instantiate the queryset.
"""
for i, obj in enumerate(queryset):
self.assertNotEqual(obj, objectset[i])
class PrefixOrderingTestCase(OrderingTestBase):
def test_prefix_vrf_ordering(self):
"""
This is a very basic test, which tests both prefixes without VRFs and prefixes with VRFs
"""
# Setup VRFs
vrfa, vrfb, vrfc = self.vrfs
# Setup Prefixes
prefixes = (
Prefix(status=PrefixStatusChoices.STATUS_CONTAINER, vrf=None, family=4, prefix=netaddr.IPNetwork('192.168.0.0/16')),
Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=None, family=4, prefix=netaddr.IPNetwork('192.168.0.0/24')),
Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=None, family=4, prefix=netaddr.IPNetwork('192.168.1.0/24')),
Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=None, family=4, prefix=netaddr.IPNetwork('192.168.2.0/24')),
Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=None, family=4, prefix=netaddr.IPNetwork('192.168.3.0/24')),
Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=None, family=4, prefix=netaddr.IPNetwork('192.168.4.0/24')),
Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=None, family=4, prefix=netaddr.IPNetwork('192.168.5.0/24')),
Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, prefix=netaddr.IPNetwork('10.0.0.0/8')),
Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, prefix=netaddr.IPNetwork('10.0.0.0/16')),
Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, prefix=netaddr.IPNetwork('10.0.0.0/24')),
Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, prefix=netaddr.IPNetwork('10.0.1.0/24')),
Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, prefix=netaddr.IPNetwork('10.0.2.0/24')),
Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, prefix=netaddr.IPNetwork('10.0.3.0/24')),
Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, prefix=netaddr.IPNetwork('10.0.4.0/24')),
Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, prefix=netaddr.IPNetwork('10.1.0.0/16')),
Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, prefix=netaddr.IPNetwork('10.1.1.0/24')),
Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, prefix=netaddr.IPNetwork('10.1.2.0/24')),
Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, prefix=netaddr.IPNetwork('10.1.3.0/24')),
Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, prefix=netaddr.IPNetwork('10.1.4.0/24')),
Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, prefix=netaddr.IPNetwork('10.2.0.0/16')),
Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, prefix=netaddr.IPNetwork('10.2.1.0/24')),
Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, prefix=netaddr.IPNetwork('10.2.2.0/24')),
Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, prefix=netaddr.IPNetwork('10.2.3.0/24')),
Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, prefix=netaddr.IPNetwork('10.2.4.0/24')),
Prefix(status=PrefixStatusChoices.STATUS_CONTAINER, vrf=vrfb, family=4, prefix=netaddr.IPNetwork('172.16.0.0/12')),
Prefix(status=PrefixStatusChoices.STATUS_CONTAINER, vrf=vrfb, family=4, prefix=netaddr.IPNetwork('172.16.0.0/16')),
Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfb, family=4, prefix=netaddr.IPNetwork('172.16.0.0/24')),
Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfb, family=4, prefix=netaddr.IPNetwork('172.16.1.0/24')),
Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfb, family=4, prefix=netaddr.IPNetwork('172.16.2.0/24')),
Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfb, family=4, prefix=netaddr.IPNetwork('172.16.3.0/24')),
Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfb, family=4, prefix=netaddr.IPNetwork('172.16.4.0/24')),
Prefix(status=PrefixStatusChoices.STATUS_CONTAINER, vrf=vrfb, family=4, prefix=netaddr.IPNetwork('172.17.0.0/16')),
Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfb, family=4, prefix=netaddr.IPNetwork('172.17.0.0/24')),
Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfb, family=4, prefix=netaddr.IPNetwork('172.17.1.0/24')),
Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfb, family=4, prefix=netaddr.IPNetwork('172.17.2.0/24')),
Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfb, family=4, prefix=netaddr.IPNetwork('172.17.3.0/24')),
Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfb, family=4, prefix=netaddr.IPNetwork('172.17.4.0/24')),
)
Prefix.objects.bulk_create(prefixes)
# Test
self._compare(Prefix.objects.all(), prefixes)
def test_prefix_complex_ordering(self):
"""
This function tests a complex ordering of interwoven prefixes and vrfs. This is the current expected ordering of VRFs
This includes the testing of the Container status.
The proper ordering, to get proper containerization should be:
None:10.0.0.0/8
None:10.0.0.0/16
VRF A:10.0.0.0/24
VRF A:10.0.1.0/24
VRF A:10.0.1.0/25
None:10.1.0.0/16
VRF A:10.1.0.0/24
VRF A:10.1.1.0/24
None: 192.168.0.0/16
"""
# Setup VRFs
vrfa, vrfb, vrfc = self.vrfs
# Setup Prefixes
prefixes = [
Prefix(status=PrefixStatusChoices.STATUS_CONTAINER, vrf=None, family=4, prefix=netaddr.IPNetwork('10.0.0.0/8')),
Prefix(status=PrefixStatusChoices.STATUS_CONTAINER, vrf=None, family=4, prefix=netaddr.IPNetwork('10.0.0.0/16')),
Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=None, family=4, prefix=netaddr.IPNetwork('10.1.0.0/16')),
Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=None, family=4, prefix=netaddr.IPNetwork('192.168.0.0/16')),
Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, prefix=netaddr.IPNetwork('10.0.0.0/24')),
Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, prefix=netaddr.IPNetwork('10.0.1.0/24')),
Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, prefix=netaddr.IPNetwork('10.0.1.0/25')),
Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, prefix=netaddr.IPNetwork('10.1.0.0/24')),
Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, prefix=netaddr.IPNetwork('10.1.1.0/24')),
]
Prefix.objects.bulk_create(prefixes)
# Test
self._compare(Prefix.objects.all(), prefixes)
class IPAddressOrderingTestCase(OrderingTestBase):
def test_address_vrf_ordering(self):
"""
This function tests ordering with the inclusion of vrfs
"""
# Setup VRFs
vrfa, vrfb, vrfc = self.vrfs
# Setup Addresses
addresses = (
IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, address=netaddr.IPNetwork('10.0.0.1/24')),
IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, address=netaddr.IPNetwork('10.0.1.1/24')),
IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, address=netaddr.IPNetwork('10.0.2.1/24')),
IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, address=netaddr.IPNetwork('10.0.3.1/24')),
IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, address=netaddr.IPNetwork('10.0.4.1/24')),
IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, address=netaddr.IPNetwork('10.1.0.1/24')),
IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, address=netaddr.IPNetwork('10.1.1.1/24')),
IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, address=netaddr.IPNetwork('10.1.2.1/24')),
IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, address=netaddr.IPNetwork('10.1.3.1/24')),
IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, address=netaddr.IPNetwork('10.1.4.1/24')),
IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, address=netaddr.IPNetwork('10.2.0.1/24')),
IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, address=netaddr.IPNetwork('10.2.1.1/24')),
IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, address=netaddr.IPNetwork('10.2.2.1/24')),
IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, address=netaddr.IPNetwork('10.2.3.1/24')),
IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, address=netaddr.IPNetwork('10.2.4.1/24')),
IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfb, family=4, address=netaddr.IPNetwork('172.16.0.1/24')),
IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfb, family=4, address=netaddr.IPNetwork('172.16.1.1/24')),
IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfb, family=4, address=netaddr.IPNetwork('172.16.2.1/24')),
IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfb, family=4, address=netaddr.IPNetwork('172.16.3.1/24')),
IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfb, family=4, address=netaddr.IPNetwork('172.16.4.1/24')),
IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfb, family=4, address=netaddr.IPNetwork('172.17.0.1/24')),
IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfb, family=4, address=netaddr.IPNetwork('172.17.1.1/24')),
IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfb, family=4, address=netaddr.IPNetwork('172.17.2.1/24')),
IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfb, family=4, address=netaddr.IPNetwork('172.17.3.1/24')),
IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfb, family=4, address=netaddr.IPNetwork('172.17.4.1/24')),
IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=None, family=4, address=netaddr.IPNetwork('192.168.0.1/24')),
IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=None, family=4, address=netaddr.IPNetwork('192.168.1.1/24')),
IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=None, family=4, address=netaddr.IPNetwork('192.168.2.1/24')),
IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=None, family=4, address=netaddr.IPNetwork('192.168.3.1/24')),
IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=None, family=4, address=netaddr.IPNetwork('192.168.4.1/24')),
IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=None, family=4, address=netaddr.IPNetwork('192.168.5.1/24')),
)
IPAddress.objects.bulk_create(addresses)
# Test
self._compare(IPAddress.objects.all(), addresses)

View File

@ -1,26 +1,18 @@
from netaddr import IPNetwork import datetime
import urllib.parse
from django.test import Client, TestCase from netaddr import IPNetwork
from django.urls import reverse
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
from ipam.choices import ServiceProtocolChoices from ipam.choices import *
from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
from utilities.testing import create_test_user from utilities.testing import ViewTestCases
class VRFTestCase(TestCase): class VRFTestCase(ViewTestCases.PrimaryObjectViewTestCase):
model = VRF
def setUp(self): @classmethod
user = create_test_user( def setUpTestData(cls):
permissions=[
'ipam.view_vrf',
'ipam.add_vrf',
]
)
self.client = Client()
self.client.force_login(user)
VRF.objects.bulk_create([ VRF.objects.bulk_create([
VRF(name='VRF 1', rd='65000:1'), VRF(name='VRF 1', rd='65000:1'),
@ -28,48 +20,34 @@ class VRFTestCase(TestCase):
VRF(name='VRF 3', rd='65000:3'), VRF(name='VRF 3', rd='65000:3'),
]) ])
def test_vrf_list(self): cls.form_data = {
'name': 'VRF X',
url = reverse('ipam:vrf_list') 'rd': '65000:999',
params = { 'tenant': None,
"q": "65000", 'enforce_unique': True,
'description': 'A new VRF',
'tags': 'Alpha,Bravo,Charlie',
} }
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params))) cls.csv_data = (
self.assertEqual(response.status_code, 200)
def test_vrf(self):
vrf = VRF.objects.first()
response = self.client.get(vrf.get_absolute_url())
self.assertEqual(response.status_code, 200)
def test_vrf_import(self):
csv_data = (
"name", "name",
"VRF 4", "VRF 4",
"VRF 5", "VRF 5",
"VRF 6", "VRF 6",
) )
response = self.client.post(reverse('ipam:vrf_import'), {'csv': '\n'.join(csv_data)}) cls.bulk_edit_data = {
'tenant': None,
self.assertEqual(response.status_code, 200) 'enforce_unique': False,
self.assertEqual(VRF.objects.count(), 6) 'description': 'New description',
}
class RIRTestCase(TestCase): class RIRTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
model = RIR
def setUp(self): @classmethod
user = create_test_user( def setUpTestData(cls):
permissions=[
'ipam.view_rir',
'ipam.add_rir',
]
)
self.client = Client()
self.client.force_login(user)
RIR.objects.bulk_create([ RIR.objects.bulk_create([
RIR(name='RIR 1', slug='rir-1'), RIR(name='RIR 1', slug='rir-1'),
@ -77,91 +55,66 @@ class RIRTestCase(TestCase):
RIR(name='RIR 3', slug='rir-3'), RIR(name='RIR 3', slug='rir-3'),
]) ])
def test_rir_list(self): cls.form_data = {
'name': 'RIR X',
'slug': 'rir-x',
'is_private': True,
}
url = reverse('ipam:rir_list') cls.csv_data = (
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
def test_rir_import(self):
csv_data = (
"name,slug", "name,slug",
"RIR 4,rir-4", "RIR 4,rir-4",
"RIR 5,rir-5", "RIR 5,rir-5",
"RIR 6,rir-6", "RIR 6,rir-6",
) )
response = self.client.post(reverse('ipam:rir_import'), {'csv': '\n'.join(csv_data)})
self.assertEqual(response.status_code, 200) class AggregateTestCase(ViewTestCases.PrimaryObjectViewTestCase):
self.assertEqual(RIR.objects.count(), 6) model = Aggregate
@classmethod
def setUpTestData(cls):
class AggregateTestCase(TestCase): rirs = (
RIR(name='RIR 1', slug='rir-1'),
def setUp(self): RIR(name='RIR 2', slug='rir-2'),
user = create_test_user(
permissions=[
'ipam.view_aggregate',
'ipam.add_aggregate',
]
) )
self.client = Client() RIR.objects.bulk_create(rirs)
self.client.force_login(user)
rir = RIR(name='RIR 1', slug='rir-1')
rir.save()
Aggregate.objects.bulk_create([ Aggregate.objects.bulk_create([
Aggregate(family=4, prefix=IPNetwork('10.1.0.0/16'), rir=rir), Aggregate(family=4, prefix=IPNetwork('10.1.0.0/16'), rir=rirs[0]),
Aggregate(family=4, prefix=IPNetwork('10.2.0.0/16'), rir=rir), Aggregate(family=4, prefix=IPNetwork('10.2.0.0/16'), rir=rirs[0]),
Aggregate(family=4, prefix=IPNetwork('10.3.0.0/16'), rir=rir), Aggregate(family=4, prefix=IPNetwork('10.3.0.0/16'), rir=rirs[0]),
]) ])
def test_aggregate_list(self): cls.form_data = {
'family': 4,
url = reverse('ipam:aggregate_list') 'prefix': IPNetwork('10.99.0.0/16'),
params = { 'rir': rirs[1].pk,
"rir": RIR.objects.first().slug, 'date_added': datetime.date(2020, 1, 1),
'description': 'A new aggregate',
'tags': 'Alpha,Bravo,Charlie',
} }
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params))) cls.csv_data = (
self.assertEqual(response.status_code, 200)
def test_aggregate(self):
aggregate = Aggregate.objects.first()
response = self.client.get(aggregate.get_absolute_url())
self.assertEqual(response.status_code, 200)
def test_aggregate_import(self):
csv_data = (
"prefix,rir", "prefix,rir",
"10.4.0.0/16,RIR 1", "10.4.0.0/16,RIR 1",
"10.5.0.0/16,RIR 1", "10.5.0.0/16,RIR 1",
"10.6.0.0/16,RIR 1", "10.6.0.0/16,RIR 1",
) )
response = self.client.post(reverse('ipam:aggregate_import'), {'csv': '\n'.join(csv_data)}) cls.bulk_edit_data = {
'rir': rirs[1].pk,
self.assertEqual(response.status_code, 200) 'date_added': datetime.date(2020, 1, 1),
self.assertEqual(Aggregate.objects.count(), 6) 'description': 'New description',
}
class RoleTestCase(TestCase): class RoleTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
model = Role
def setUp(self): @classmethod
user = create_test_user( def setUpTestData(cls):
permissions=[
'ipam.view_role',
'ipam.add_role',
]
)
self.client = Client()
self.client.force_login(user)
Role.objects.bulk_create([ Role.objects.bulk_create([
Role(name='Role 1', slug='role-1'), Role(name='Role 1', slug='role-1'),
@ -169,146 +122,135 @@ class RoleTestCase(TestCase):
Role(name='Role 3', slug='role-3'), Role(name='Role 3', slug='role-3'),
]) ])
def test_role_list(self): cls.form_data = {
'name': 'Role X',
'slug': 'role-x',
'weight': 200,
'description': 'A new role',
}
url = reverse('ipam:role_list') cls.csv_data = (
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
def test_role_import(self):
csv_data = (
"name,slug,weight", "name,slug,weight",
"Role 4,role-4,1000", "Role 4,role-4,1000",
"Role 5,role-5,1000", "Role 5,role-5,1000",
"Role 6,role-6,1000", "Role 6,role-6,1000",
) )
response = self.client.post(reverse('ipam:role_import'), {'csv': '\n'.join(csv_data)})
self.assertEqual(response.status_code, 200) class PrefixTestCase(ViewTestCases.PrimaryObjectViewTestCase):
self.assertEqual(Role.objects.count(), 6) model = Prefix
@classmethod
def setUpTestData(cls):
class PrefixTestCase(TestCase): sites = (
Site(name='Site 1', slug='site-1'),
def setUp(self): Site(name='Site 2', slug='site-2'),
user = create_test_user(
permissions=[
'ipam.view_prefix',
'ipam.add_prefix',
]
) )
self.client = Client() Site.objects.bulk_create(sites)
self.client.force_login(user)
site = Site(name='Site 1', slug='site-1') vrfs = (
site.save() VRF(name='VRF 1', rd='65000:1'),
VRF(name='VRF 2', rd='65000:2'),
)
VRF.objects.bulk_create(vrfs)
roles = (
Role(name='Role 1', slug='role-1'),
Role(name='Role 2', slug='role-2'),
)
Prefix.objects.bulk_create([ Prefix.objects.bulk_create([
Prefix(family=4, prefix=IPNetwork('10.1.0.0/16'), site=site), Prefix(family=4, prefix=IPNetwork('10.1.0.0/16'), vrf=vrfs[0], site=sites[0], role=roles[0]),
Prefix(family=4, prefix=IPNetwork('10.2.0.0/16'), site=site), Prefix(family=4, prefix=IPNetwork('10.2.0.0/16'), vrf=vrfs[0], site=sites[0], role=roles[0]),
Prefix(family=4, prefix=IPNetwork('10.3.0.0/16'), site=site), Prefix(family=4, prefix=IPNetwork('10.3.0.0/16'), vrf=vrfs[0], site=sites[0], role=roles[0]),
]) ])
def test_prefix_list(self): cls.form_data = {
'prefix': IPNetwork('192.0.2.0/24'),
url = reverse('ipam:prefix_list') 'site': sites[1].pk,
params = { 'vrf': vrfs[1].pk,
"site": Site.objects.first().slug, 'tenant': None,
'vlan': None,
'status': PrefixStatusChoices.STATUS_RESERVED,
'role': roles[1].pk,
'is_pool': True,
'description': 'A new prefix',
'tags': 'Alpha,Bravo,Charlie',
} }
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params))) cls.csv_data = (
self.assertEqual(response.status_code, 200)
def test_prefix(self):
prefix = Prefix.objects.first()
response = self.client.get(prefix.get_absolute_url())
self.assertEqual(response.status_code, 200)
def test_prefix_import(self):
csv_data = (
"prefix,status", "prefix,status",
"10.4.0.0/16,Active", "10.4.0.0/16,Active",
"10.5.0.0/16,Active", "10.5.0.0/16,Active",
"10.6.0.0/16,Active", "10.6.0.0/16,Active",
) )
response = self.client.post(reverse('ipam:prefix_import'), {'csv': '\n'.join(csv_data)}) cls.bulk_edit_data = {
'site': sites[1].pk,
self.assertEqual(response.status_code, 200) 'vrf': vrfs[1].pk,
self.assertEqual(Prefix.objects.count(), 6) 'tenant': None,
'status': PrefixStatusChoices.STATUS_RESERVED,
'role': roles[1].pk,
class IPAddressTestCase(TestCase): 'is_pool': False,
'description': 'New description',
def setUp(self):
user = create_test_user(
permissions=[
'ipam.view_ipaddress',
'ipam.add_ipaddress',
]
)
self.client = Client()
self.client.force_login(user)
vrf = VRF(name='VRF 1', rd='65000:1')
vrf.save()
IPAddress.objects.bulk_create([
IPAddress(family=4, address=IPNetwork('192.0.2.1/24'), vrf=vrf),
IPAddress(family=4, address=IPNetwork('192.0.2.2/24'), vrf=vrf),
IPAddress(family=4, address=IPNetwork('192.0.2.3/24'), vrf=vrf),
])
def test_ipaddress_list(self):
url = reverse('ipam:ipaddress_list')
params = {
"vrf": VRF.objects.first().rd,
} }
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
self.assertEqual(response.status_code, 200)
def test_ipaddress(self): class IPAddressTestCase(ViewTestCases.PrimaryObjectViewTestCase):
model = IPAddress
ipaddress = IPAddress.objects.first() @classmethod
response = self.client.get(ipaddress.get_absolute_url()) def setUpTestData(cls):
self.assertEqual(response.status_code, 200)
def test_ipaddress_import(self): vrfs = (
VRF(name='VRF 1', rd='65000:1'),
VRF(name='VRF 2', rd='65000:2'),
)
csv_data = ( IPAddress.objects.bulk_create([
IPAddress(family=4, address=IPNetwork('192.0.2.1/24'), vrf=vrfs[0]),
IPAddress(family=4, address=IPNetwork('192.0.2.2/24'), vrf=vrfs[0]),
IPAddress(family=4, address=IPNetwork('192.0.2.3/24'), vrf=vrfs[0]),
])
cls.form_data = {
'vrf': vrfs[1].pk,
'address': IPNetwork('192.0.2.99/24'),
'tenant': None,
'status': IPAddressStatusChoices.STATUS_RESERVED,
'role': IPAddressRoleChoices.ROLE_ANYCAST,
'interface': None,
'nat_inside': None,
'dns_name': 'example',
'description': 'A new IP address',
'tags': 'Alpha,Bravo,Charlie',
}
cls.csv_data = (
"address,status", "address,status",
"192.0.2.4/24,Active", "192.0.2.4/24,Active",
"192.0.2.5/24,Active", "192.0.2.5/24,Active",
"192.0.2.6/24,Active", "192.0.2.6/24,Active",
) )
response = self.client.post(reverse('ipam:ipaddress_import'), {'csv': '\n'.join(csv_data)}) cls.bulk_edit_data = {
'vrf': vrfs[1].pk,
self.assertEqual(response.status_code, 200) 'tenant': None,
self.assertEqual(IPAddress.objects.count(), 6) 'status': IPAddressStatusChoices.STATUS_RESERVED,
'role': IPAddressRoleChoices.ROLE_ANYCAST,
'dns_name': 'example',
'description': 'New description',
}
class VLANGroupTestCase(TestCase): class VLANGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
model = VLANGroup
def setUp(self): @classmethod
user = create_test_user( def setUpTestData(cls):
permissions=[
'ipam.view_vlangroup',
'ipam.add_vlangroup',
]
)
self.client = Client()
self.client.force_login(user)
site = Site(name='Site 1', slug='site-1') site = Site.objects.create(name='Site 1', slug='site-1')
site.save()
VLANGroup.objects.bulk_create([ VLANGroup.objects.bulk_create([
VLANGroup(name='VLAN Group 1', slug='vlan-group-1', site=site), VLANGroup(name='VLAN Group 1', slug='vlan-group-1', site=site),
@ -316,104 +258,96 @@ class VLANGroupTestCase(TestCase):
VLANGroup(name='VLAN Group 3', slug='vlan-group-3', site=site), VLANGroup(name='VLAN Group 3', slug='vlan-group-3', site=site),
]) ])
def test_vlangroup_list(self): cls.form_data = {
'name': 'VLAN Group X',
url = reverse('ipam:vlangroup_list') 'slug': 'vlan-group-x',
params = { 'site': site.pk,
"site": Site.objects.first().slug,
} }
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params))) cls.csv_data = (
self.assertEqual(response.status_code, 200)
def test_vlangroup_import(self):
csv_data = (
"name,slug", "name,slug",
"VLAN Group 4,vlan-group-4", "VLAN Group 4,vlan-group-4",
"VLAN Group 5,vlan-group-5", "VLAN Group 5,vlan-group-5",
"VLAN Group 6,vlan-group-6", "VLAN Group 6,vlan-group-6",
) )
response = self.client.post(reverse('ipam:vlangroup_import'), {'csv': '\n'.join(csv_data)})
self.assertEqual(response.status_code, 200) class VLANTestCase(ViewTestCases.PrimaryObjectViewTestCase):
self.assertEqual(VLANGroup.objects.count(), 6) model = VLAN
@classmethod
def setUpTestData(cls):
class VLANTestCase(TestCase): sites = (
Site(name='Site 1', slug='site-1'),
def setUp(self): Site(name='Site 2', slug='site-2'),
user = create_test_user(
permissions=[
'ipam.view_vlan',
'ipam.add_vlan',
]
) )
self.client = Client() Site.objects.bulk_create(sites)
self.client.force_login(user)
vlangroup = VLANGroup(name='VLAN Group 1', slug='vlan-group-1') vlangroups = (
vlangroup.save() VLANGroup(name='VLAN Group 1', slug='vlan-group-1', site=sites[0]),
VLANGroup(name='VLAN Group 2', slug='vlan-group-2', site=sites[1]),
)
VLANGroup.objects.bulk_create(vlangroups)
roles = (
Role(name='Role 1', slug='role-1'),
Role(name='Role 2', slug='role-2'),
)
Role.objects.bulk_create(roles)
VLAN.objects.bulk_create([ VLAN.objects.bulk_create([
VLAN(group=vlangroup, vid=101, name='VLAN101'), VLAN(group=vlangroups[0], vid=101, name='VLAN101', site=sites[0], role=roles[0]),
VLAN(group=vlangroup, vid=102, name='VLAN102'), VLAN(group=vlangroups[0], vid=102, name='VLAN102', site=sites[0], role=roles[0]),
VLAN(group=vlangroup, vid=103, name='VLAN103'), VLAN(group=vlangroups[0], vid=103, name='VLAN103', site=sites[0], role=roles[0]),
]) ])
def test_vlan_list(self): cls.form_data = {
'site': sites[1].pk,
url = reverse('ipam:vlan_list') 'group': vlangroups[1].pk,
params = { 'vid': 999,
"group": VLANGroup.objects.first().slug, 'name': 'VLAN999',
'tenant': None,
'status': VLANStatusChoices.STATUS_RESERVED,
'role': roles[1].pk,
'description': 'A new VLAN',
'tags': 'Alpha,Bravo,Charlie',
} }
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params))) cls.csv_data = (
self.assertEqual(response.status_code, 200)
def test_vlan(self):
vlan = VLAN.objects.first()
response = self.client.get(vlan.get_absolute_url())
self.assertEqual(response.status_code, 200)
def test_vlan_import(self):
csv_data = (
"vid,name,status", "vid,name,status",
"104,VLAN104,Active", "104,VLAN104,Active",
"105,VLAN105,Active", "105,VLAN105,Active",
"106,VLAN106,Active", "106,VLAN106,Active",
) )
response = self.client.post(reverse('ipam:vlan_import'), {'csv': '\n'.join(csv_data)}) cls.bulk_edit_data = {
'site': sites[1].pk,
self.assertEqual(response.status_code, 200) 'group': vlangroups[1].pk,
self.assertEqual(VLAN.objects.count(), 6) 'tenant': None,
'status': VLANStatusChoices.STATUS_RESERVED,
'role': roles[1].pk,
'description': 'New description',
}
class ServiceTestCase(TestCase): class ServiceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
model = Service
def setUp(self): # Disable inapplicable tests
user = create_test_user(permissions=['ipam.view_service']) test_import_objects = None
self.client = Client()
self.client.force_login(user)
site = Site(name='Site 1', slug='site-1') # TODO: Resolve URL for Service creation
site.save() test_create_object = None
manufacturer = Manufacturer(name='Manufacturer 1', slug='manufacturer-1') @classmethod
manufacturer.save() def setUpTestData(cls):
devicetype = DeviceType(manufacturer=manufacturer, model='Device Type 1') site = Site.objects.create(name='Site 1', slug='site-1')
devicetype.save() manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1')
devicerole = DeviceRole(name='Device Role 1', slug='device-role-1') devicerole = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1')
devicerole.save() device = Device.objects.create(name='Device 1', site=site, device_type=devicetype, device_role=devicerole)
device = Device(name='Device 1', site=site, device_type=devicetype, device_role=devicerole)
device.save()
Service.objects.bulk_create([ Service.objects.bulk_create([
Service(device=device, name='Service 1', protocol=ServiceProtocolChoices.PROTOCOL_TCP, port=101), Service(device=device, name='Service 1', protocol=ServiceProtocolChoices.PROTOCOL_TCP, port=101),
@ -421,18 +355,19 @@ class ServiceTestCase(TestCase):
Service(device=device, name='Service 3', protocol=ServiceProtocolChoices.PROTOCOL_TCP, port=103), Service(device=device, name='Service 3', protocol=ServiceProtocolChoices.PROTOCOL_TCP, port=103),
]) ])
def test_service_list(self): cls.form_data = {
'device': device.pk,
url = reverse('ipam:service_list') 'virtual_machine': None,
params = { 'name': 'Service X',
"device_id": Device.objects.first(), 'protocol': ServiceProtocolChoices.PROTOCOL_TCP,
'port': 999,
'ipaddresses': [],
'description': 'A new service',
'tags': 'Alpha,Bravo,Charlie',
} }
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params))) cls.bulk_edit_data = {
self.assertEqual(response.status_code, 200) 'protocol': ServiceProtocolChoices.PROTOCOL_UDP,
'port': 888,
def test_service(self): 'description': 'New description',
}
service = Service.objects.first()
response = self.client.get(service.get_absolute_url())
self.assertEqual(response.status_code, 200)

View File

@ -8,97 +8,97 @@ app_name = 'ipam'
urlpatterns = [ urlpatterns = [
# VRFs # VRFs
path(r'vrfs/', views.VRFListView.as_view(), name='vrf_list'), path('vrfs/', views.VRFListView.as_view(), name='vrf_list'),
path(r'vrfs/add/', views.VRFCreateView.as_view(), name='vrf_add'), path('vrfs/add/', views.VRFCreateView.as_view(), name='vrf_add'),
path(r'vrfs/import/', views.VRFBulkImportView.as_view(), name='vrf_import'), path('vrfs/import/', views.VRFBulkImportView.as_view(), name='vrf_import'),
path(r'vrfs/edit/', views.VRFBulkEditView.as_view(), name='vrf_bulk_edit'), path('vrfs/edit/', views.VRFBulkEditView.as_view(), name='vrf_bulk_edit'),
path(r'vrfs/delete/', views.VRFBulkDeleteView.as_view(), name='vrf_bulk_delete'), path('vrfs/delete/', views.VRFBulkDeleteView.as_view(), name='vrf_bulk_delete'),
path(r'vrfs/<int:pk>/', views.VRFView.as_view(), name='vrf'), path('vrfs/<int:pk>/', views.VRFView.as_view(), name='vrf'),
path(r'vrfs/<int:pk>/edit/', views.VRFEditView.as_view(), name='vrf_edit'), path('vrfs/<int:pk>/edit/', views.VRFEditView.as_view(), name='vrf_edit'),
path(r'vrfs/<int:pk>/delete/', views.VRFDeleteView.as_view(), name='vrf_delete'), path('vrfs/<int:pk>/delete/', views.VRFDeleteView.as_view(), name='vrf_delete'),
path(r'vrfs/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='vrf_changelog', kwargs={'model': VRF}), path('vrfs/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='vrf_changelog', kwargs={'model': VRF}),
# RIRs # RIRs
path(r'rirs/', views.RIRListView.as_view(), name='rir_list'), path('rirs/', views.RIRListView.as_view(), name='rir_list'),
path(r'rirs/add/', views.RIRCreateView.as_view(), name='rir_add'), path('rirs/add/', views.RIRCreateView.as_view(), name='rir_add'),
path(r'rirs/import/', views.RIRBulkImportView.as_view(), name='rir_import'), path('rirs/import/', views.RIRBulkImportView.as_view(), name='rir_import'),
path(r'rirs/delete/', views.RIRBulkDeleteView.as_view(), name='rir_bulk_delete'), path('rirs/delete/', views.RIRBulkDeleteView.as_view(), name='rir_bulk_delete'),
path(r'rirs/<slug:slug>/edit/', views.RIREditView.as_view(), name='rir_edit'), path('rirs/<slug:slug>/edit/', views.RIREditView.as_view(), name='rir_edit'),
path(r'vrfs/<slug:slug>/changelog/', ObjectChangeLogView.as_view(), name='rir_changelog', kwargs={'model': RIR}), path('vrfs/<slug:slug>/changelog/', ObjectChangeLogView.as_view(), name='rir_changelog', kwargs={'model': RIR}),
# Aggregates # Aggregates
path(r'aggregates/', views.AggregateListView.as_view(), name='aggregate_list'), path('aggregates/', views.AggregateListView.as_view(), name='aggregate_list'),
path(r'aggregates/add/', views.AggregateCreateView.as_view(), name='aggregate_add'), path('aggregates/add/', views.AggregateCreateView.as_view(), name='aggregate_add'),
path(r'aggregates/import/', views.AggregateBulkImportView.as_view(), name='aggregate_import'), path('aggregates/import/', views.AggregateBulkImportView.as_view(), name='aggregate_import'),
path(r'aggregates/edit/', views.AggregateBulkEditView.as_view(), name='aggregate_bulk_edit'), path('aggregates/edit/', views.AggregateBulkEditView.as_view(), name='aggregate_bulk_edit'),
path(r'aggregates/delete/', views.AggregateBulkDeleteView.as_view(), name='aggregate_bulk_delete'), path('aggregates/delete/', views.AggregateBulkDeleteView.as_view(), name='aggregate_bulk_delete'),
path(r'aggregates/<int:pk>/', views.AggregateView.as_view(), name='aggregate'), path('aggregates/<int:pk>/', views.AggregateView.as_view(), name='aggregate'),
path(r'aggregates/<int:pk>/edit/', views.AggregateEditView.as_view(), name='aggregate_edit'), path('aggregates/<int:pk>/edit/', views.AggregateEditView.as_view(), name='aggregate_edit'),
path(r'aggregates/<int:pk>/delete/', views.AggregateDeleteView.as_view(), name='aggregate_delete'), path('aggregates/<int:pk>/delete/', views.AggregateDeleteView.as_view(), name='aggregate_delete'),
path(r'aggregates/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='aggregate_changelog', kwargs={'model': Aggregate}), path('aggregates/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='aggregate_changelog', kwargs={'model': Aggregate}),
# Roles # Roles
path(r'roles/', views.RoleListView.as_view(), name='role_list'), path('roles/', views.RoleListView.as_view(), name='role_list'),
path(r'roles/add/', views.RoleCreateView.as_view(), name='role_add'), path('roles/add/', views.RoleCreateView.as_view(), name='role_add'),
path(r'roles/import/', views.RoleBulkImportView.as_view(), name='role_import'), path('roles/import/', views.RoleBulkImportView.as_view(), name='role_import'),
path(r'roles/delete/', views.RoleBulkDeleteView.as_view(), name='role_bulk_delete'), path('roles/delete/', views.RoleBulkDeleteView.as_view(), name='role_bulk_delete'),
path(r'roles/<slug:slug>/edit/', views.RoleEditView.as_view(), name='role_edit'), path('roles/<slug:slug>/edit/', views.RoleEditView.as_view(), name='role_edit'),
path(r'roles/<slug:slug>/changelog/', ObjectChangeLogView.as_view(), name='role_changelog', kwargs={'model': Role}), path('roles/<slug:slug>/changelog/', ObjectChangeLogView.as_view(), name='role_changelog', kwargs={'model': Role}),
# Prefixes # Prefixes
path(r'prefixes/', views.PrefixListView.as_view(), name='prefix_list'), path('prefixes/', views.PrefixListView.as_view(), name='prefix_list'),
path(r'prefixes/add/', views.PrefixCreateView.as_view(), name='prefix_add'), path('prefixes/add/', views.PrefixCreateView.as_view(), name='prefix_add'),
path(r'prefixes/import/', views.PrefixBulkImportView.as_view(), name='prefix_import'), path('prefixes/import/', views.PrefixBulkImportView.as_view(), name='prefix_import'),
path(r'prefixes/edit/', views.PrefixBulkEditView.as_view(), name='prefix_bulk_edit'), path('prefixes/edit/', views.PrefixBulkEditView.as_view(), name='prefix_bulk_edit'),
path(r'prefixes/delete/', views.PrefixBulkDeleteView.as_view(), name='prefix_bulk_delete'), path('prefixes/delete/', views.PrefixBulkDeleteView.as_view(), name='prefix_bulk_delete'),
path(r'prefixes/<int:pk>/', views.PrefixView.as_view(), name='prefix'), path('prefixes/<int:pk>/', views.PrefixView.as_view(), name='prefix'),
path(r'prefixes/<int:pk>/edit/', views.PrefixEditView.as_view(), name='prefix_edit'), path('prefixes/<int:pk>/edit/', views.PrefixEditView.as_view(), name='prefix_edit'),
path(r'prefixes/<int:pk>/delete/', views.PrefixDeleteView.as_view(), name='prefix_delete'), path('prefixes/<int:pk>/delete/', views.PrefixDeleteView.as_view(), name='prefix_delete'),
path(r'prefixes/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='prefix_changelog', kwargs={'model': Prefix}), path('prefixes/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='prefix_changelog', kwargs={'model': Prefix}),
path(r'prefixes/<int:pk>/prefixes/', views.PrefixPrefixesView.as_view(), name='prefix_prefixes'), path('prefixes/<int:pk>/prefixes/', views.PrefixPrefixesView.as_view(), name='prefix_prefixes'),
path(r'prefixes/<int:pk>/ip-addresses/', views.PrefixIPAddressesView.as_view(), name='prefix_ipaddresses'), path('prefixes/<int:pk>/ip-addresses/', views.PrefixIPAddressesView.as_view(), name='prefix_ipaddresses'),
# IP addresses # IP addresses
path(r'ip-addresses/', views.IPAddressListView.as_view(), name='ipaddress_list'), path('ip-addresses/', views.IPAddressListView.as_view(), name='ipaddress_list'),
path(r'ip-addresses/add/', views.IPAddressCreateView.as_view(), name='ipaddress_add'), path('ip-addresses/add/', views.IPAddressCreateView.as_view(), name='ipaddress_add'),
path(r'ip-addresses/bulk-add/', views.IPAddressBulkCreateView.as_view(), name='ipaddress_bulk_add'), path('ip-addresses/bulk-add/', views.IPAddressBulkCreateView.as_view(), name='ipaddress_bulk_add'),
path(r'ip-addresses/import/', views.IPAddressBulkImportView.as_view(), name='ipaddress_import'), path('ip-addresses/import/', views.IPAddressBulkImportView.as_view(), name='ipaddress_import'),
path(r'ip-addresses/edit/', views.IPAddressBulkEditView.as_view(), name='ipaddress_bulk_edit'), path('ip-addresses/edit/', views.IPAddressBulkEditView.as_view(), name='ipaddress_bulk_edit'),
path(r'ip-addresses/delete/', views.IPAddressBulkDeleteView.as_view(), name='ipaddress_bulk_delete'), path('ip-addresses/delete/', views.IPAddressBulkDeleteView.as_view(), name='ipaddress_bulk_delete'),
path(r'ip-addresses/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='ipaddress_changelog', kwargs={'model': IPAddress}), path('ip-addresses/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='ipaddress_changelog', kwargs={'model': IPAddress}),
path(r'ip-addresses/assign/', views.IPAddressAssignView.as_view(), name='ipaddress_assign'), path('ip-addresses/assign/', views.IPAddressAssignView.as_view(), name='ipaddress_assign'),
path(r'ip-addresses/<int:pk>/', views.IPAddressView.as_view(), name='ipaddress'), path('ip-addresses/<int:pk>/', views.IPAddressView.as_view(), name='ipaddress'),
path(r'ip-addresses/<int:pk>/edit/', views.IPAddressEditView.as_view(), name='ipaddress_edit'), path('ip-addresses/<int:pk>/edit/', views.IPAddressEditView.as_view(), name='ipaddress_edit'),
path(r'ip-addresses/<int:pk>/delete/', views.IPAddressDeleteView.as_view(), name='ipaddress_delete'), path('ip-addresses/<int:pk>/delete/', views.IPAddressDeleteView.as_view(), name='ipaddress_delete'),
# VLAN groups # VLAN groups
path(r'vlan-groups/', views.VLANGroupListView.as_view(), name='vlangroup_list'), path('vlan-groups/', views.VLANGroupListView.as_view(), name='vlangroup_list'),
path(r'vlan-groups/add/', views.VLANGroupCreateView.as_view(), name='vlangroup_add'), path('vlan-groups/add/', views.VLANGroupCreateView.as_view(), name='vlangroup_add'),
path(r'vlan-groups/import/', views.VLANGroupBulkImportView.as_view(), name='vlangroup_import'), path('vlan-groups/import/', views.VLANGroupBulkImportView.as_view(), name='vlangroup_import'),
path(r'vlan-groups/delete/', views.VLANGroupBulkDeleteView.as_view(), name='vlangroup_bulk_delete'), path('vlan-groups/delete/', views.VLANGroupBulkDeleteView.as_view(), name='vlangroup_bulk_delete'),
path(r'vlan-groups/<int:pk>/edit/', views.VLANGroupEditView.as_view(), name='vlangroup_edit'), path('vlan-groups/<int:pk>/edit/', views.VLANGroupEditView.as_view(), name='vlangroup_edit'),
path(r'vlan-groups/<int:pk>/vlans/', views.VLANGroupVLANsView.as_view(), name='vlangroup_vlans'), path('vlan-groups/<int:pk>/vlans/', views.VLANGroupVLANsView.as_view(), name='vlangroup_vlans'),
path(r'vlan-groups/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='vlangroup_changelog', kwargs={'model': VLANGroup}), path('vlan-groups/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='vlangroup_changelog', kwargs={'model': VLANGroup}),
# VLANs # VLANs
path(r'vlans/', views.VLANListView.as_view(), name='vlan_list'), path('vlans/', views.VLANListView.as_view(), name='vlan_list'),
path(r'vlans/add/', views.VLANCreateView.as_view(), name='vlan_add'), path('vlans/add/', views.VLANCreateView.as_view(), name='vlan_add'),
path(r'vlans/import/', views.VLANBulkImportView.as_view(), name='vlan_import'), path('vlans/import/', views.VLANBulkImportView.as_view(), name='vlan_import'),
path(r'vlans/edit/', views.VLANBulkEditView.as_view(), name='vlan_bulk_edit'), path('vlans/edit/', views.VLANBulkEditView.as_view(), name='vlan_bulk_edit'),
path(r'vlans/delete/', views.VLANBulkDeleteView.as_view(), name='vlan_bulk_delete'), path('vlans/delete/', views.VLANBulkDeleteView.as_view(), name='vlan_bulk_delete'),
path(r'vlans/<int:pk>/', views.VLANView.as_view(), name='vlan'), path('vlans/<int:pk>/', views.VLANView.as_view(), name='vlan'),
path(r'vlans/<int:pk>/members/', views.VLANMembersView.as_view(), name='vlan_members'), path('vlans/<int:pk>/members/', views.VLANMembersView.as_view(), name='vlan_members'),
path(r'vlans/<int:pk>/edit/', views.VLANEditView.as_view(), name='vlan_edit'), path('vlans/<int:pk>/edit/', views.VLANEditView.as_view(), name='vlan_edit'),
path(r'vlans/<int:pk>/delete/', views.VLANDeleteView.as_view(), name='vlan_delete'), path('vlans/<int:pk>/delete/', views.VLANDeleteView.as_view(), name='vlan_delete'),
path(r'vlans/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='vlan_changelog', kwargs={'model': VLAN}), path('vlans/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='vlan_changelog', kwargs={'model': VLAN}),
# Services # Services
path(r'services/', views.ServiceListView.as_view(), name='service_list'), path('services/', views.ServiceListView.as_view(), name='service_list'),
path(r'services/edit/', views.ServiceBulkEditView.as_view(), name='service_bulk_edit'), path('services/edit/', views.ServiceBulkEditView.as_view(), name='service_bulk_edit'),
path(r'services/delete/', views.ServiceBulkDeleteView.as_view(), name='service_bulk_delete'), path('services/delete/', views.ServiceBulkDeleteView.as_view(), name='service_bulk_delete'),
path(r'services/<int:pk>/', views.ServiceView.as_view(), name='service'), path('services/<int:pk>/', views.ServiceView.as_view(), name='service'),
path(r'services/<int:pk>/edit/', views.ServiceEditView.as_view(), name='service_edit'), path('services/<int:pk>/edit/', views.ServiceEditView.as_view(), name='service_edit'),
path(r'services/<int:pk>/delete/', views.ServiceDeleteView.as_view(), name='service_delete'), path('services/<int:pk>/delete/', views.ServiceDeleteView.as_view(), name='service_delete'),
path(r'services/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='service_changelog', kwargs={'model': Service}), path('services/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='service_changelog', kwargs={'model': Service}),
] ]

View File

@ -118,7 +118,6 @@ class VRFListView(PermissionRequiredMixin, ObjectListView):
filterset = filters.VRFFilterSet filterset = filters.VRFFilterSet
filterset_form = forms.VRFFilterForm filterset_form = forms.VRFFilterForm
table = tables.VRFTable table = tables.VRFTable
template_name = 'ipam/vrf_list.html'
class VRFView(PermissionRequiredMixin, View): class VRFView(PermissionRequiredMixin, View):
@ -293,7 +292,6 @@ class AggregateListView(PermissionRequiredMixin, ObjectListView):
queryset = Aggregate.objects.prefetch_related('rir').annotate( queryset = Aggregate.objects.prefetch_related('rir').annotate(
child_count=RawSQL('SELECT COUNT(*) FROM ipam_prefix WHERE ipam_prefix.prefix <<= ipam_aggregate.prefix', ()) child_count=RawSQL('SELECT COUNT(*) FROM ipam_prefix WHERE ipam_prefix.prefix <<= ipam_aggregate.prefix', ())
) )
filterset = filters.AggregateFilterSet filterset = filters.AggregateFilterSet
filterset_form = forms.AggregateFilterForm filterset_form = forms.AggregateFilterForm
table = tables.AggregateDetailTable table = tables.AggregateDetailTable
@ -411,7 +409,6 @@ class RoleListView(PermissionRequiredMixin, ObjectListView):
permission_required = 'ipam.view_role' permission_required = 'ipam.view_role'
queryset = Role.objects.all() queryset = Role.objects.all()
table = tables.RoleTable table = tables.RoleTable
template_name = 'ipam/role_list.html'
class RoleCreateView(PermissionRequiredMixin, ObjectEditView): class RoleCreateView(PermissionRequiredMixin, ObjectEditView):
@ -644,7 +641,6 @@ class IPAddressListView(PermissionRequiredMixin, ObjectListView):
filterset = filters.IPAddressFilterSet filterset = filters.IPAddressFilterSet
filterset_form = forms.IPAddressFilterForm filterset_form = forms.IPAddressFilterForm
table = tables.IPAddressDetailTable table = tables.IPAddressDetailTable
template_name = 'ipam/ipaddress_list.html'
class IPAddressView(PermissionRequiredMixin, View): class IPAddressView(PermissionRequiredMixin, View):
@ -817,7 +813,6 @@ class VLANGroupListView(PermissionRequiredMixin, ObjectListView):
filterset = filters.VLANGroupFilterSet filterset = filters.VLANGroupFilterSet
filterset_form = forms.VLANGroupFilterForm filterset_form = forms.VLANGroupFilterForm
table = tables.VLANGroupTable table = tables.VLANGroupTable
template_name = 'ipam/vlangroup_list.html'
class VLANGroupCreateView(PermissionRequiredMixin, ObjectEditView): class VLANGroupCreateView(PermissionRequiredMixin, ObjectEditView):
@ -893,7 +888,6 @@ class VLANListView(PermissionRequiredMixin, ObjectListView):
filterset = filters.VLANFilterSet filterset = filters.VLANFilterSet
filterset_form = forms.VLANFilterForm filterset_form = forms.VLANFilterForm
table = tables.VLANDetailTable table = tables.VLANDetailTable
template_name = 'ipam/vlan_list.html'
class VLANView(PermissionRequiredMixin, View): class VLANView(PermissionRequiredMixin, View):
@ -989,7 +983,7 @@ class ServiceListView(PermissionRequiredMixin, ObjectListView):
filterset = filters.ServiceFilterSet filterset = filters.ServiceFilterSet
filterset_form = forms.ServiceFilterForm filterset_form = forms.ServiceFilterForm
table = tables.ServiceTable table = tables.ServiceTable
template_name = 'ipam/service_list.html' action_buttons = ('export',)
class ServiceView(PermissionRequiredMixin, View): class ServiceView(PermissionRequiredMixin, View):

View File

@ -10,7 +10,8 @@
# Example: ALLOWED_HOSTS = ['netbox.example.com', 'netbox.internal.local'] # Example: ALLOWED_HOSTS = ['netbox.example.com', 'netbox.internal.local']
ALLOWED_HOSTS = [] ALLOWED_HOSTS = []
# PostgreSQL database configuration. # PostgreSQL database configuration. See the Django documentation for a complete list of available parameters:
# https://docs.djangoproject.com/en/stable/ref/settings/#databases
DATABASE = { DATABASE = {
'NAME': 'netbox', # Database name 'NAME': 'netbox', # Database name
'USER': '', # PostgreSQL username 'USER': '', # PostgreSQL username
@ -27,6 +28,9 @@ REDIS = {
'webhooks': { 'webhooks': {
'HOST': 'localhost', 'HOST': 'localhost',
'PORT': 6379, 'PORT': 6379,
# Comment out `HOST` and `PORT` lines and uncomment the following if using Redis Sentinel
# 'SENTINELS': [('mysentinel.redis.example.com', 6379)],
# 'SENTINEL_SERVICE': 'netbox',
'PASSWORD': '', 'PASSWORD': '',
'DATABASE': 0, 'DATABASE': 0,
'DEFAULT_TIMEOUT': 300, 'DEFAULT_TIMEOUT': 300,
@ -35,6 +39,9 @@ REDIS = {
'caching': { 'caching': {
'HOST': 'localhost', 'HOST': 'localhost',
'PORT': 6379, 'PORT': 6379,
# Comment out `HOST` and `PORT` lines and uncomment the following if using Redis Sentinel
# 'SENTINELS': [('mysentinel.redis.example.com', 6379)],
# 'SENTINEL_SERVICE': 'netbox',
'PASSWORD': '', 'PASSWORD': '',
'DATABASE': 1, 'DATABASE': 1,
'DEFAULT_TIMEOUT': 300, 'DEFAULT_TIMEOUT': 300,

View File

@ -74,6 +74,7 @@ CORS_ORIGIN_WHITELIST = getattr(configuration, 'CORS_ORIGIN_WHITELIST', [])
DATE_FORMAT = getattr(configuration, 'DATE_FORMAT', 'N j, Y') DATE_FORMAT = getattr(configuration, 'DATE_FORMAT', 'N j, Y')
DATETIME_FORMAT = getattr(configuration, 'DATETIME_FORMAT', 'N j, Y g:i a') DATETIME_FORMAT = getattr(configuration, 'DATETIME_FORMAT', 'N j, Y g:i a')
DEBUG = getattr(configuration, 'DEBUG', False) DEBUG = getattr(configuration, 'DEBUG', False)
DEVELOPER = getattr(configuration, 'DEVELOPER', False)
EMAIL = getattr(configuration, 'EMAIL', {}) EMAIL = getattr(configuration, 'EMAIL', {})
ENFORCE_GLOBAL_UNIQUE = getattr(configuration, 'ENFORCE_GLOBAL_UNIQUE', False) ENFORCE_GLOBAL_UNIQUE = getattr(configuration, 'ENFORCE_GLOBAL_UNIQUE', False)
EXEMPT_VIEW_PERMISSIONS = getattr(configuration, 'EXEMPT_VIEW_PERMISSIONS', []) EXEMPT_VIEW_PERMISSIONS = getattr(configuration, 'EXEMPT_VIEW_PERMISSIONS', [])
@ -169,18 +170,31 @@ if 'caching' not in REDIS:
WEBHOOKS_REDIS = REDIS.get('webhooks', {}) WEBHOOKS_REDIS = REDIS.get('webhooks', {})
WEBHOOKS_REDIS_HOST = WEBHOOKS_REDIS.get('HOST', 'localhost') WEBHOOKS_REDIS_HOST = WEBHOOKS_REDIS.get('HOST', 'localhost')
WEBHOOKS_REDIS_PORT = WEBHOOKS_REDIS.get('PORT', 6379) WEBHOOKS_REDIS_PORT = WEBHOOKS_REDIS.get('PORT', 6379)
WEBHOOKS_REDIS_SENTINELS = WEBHOOKS_REDIS.get('SENTINELS', [])
WEBHOOKS_REDIS_USING_SENTINEL = all([
isinstance(WEBHOOKS_REDIS_SENTINELS, (list, tuple)),
len(WEBHOOKS_REDIS_SENTINELS) > 0
])
WEBHOOKS_REDIS_SENTINEL_SERVICE = WEBHOOKS_REDIS.get('SENTINEL_SERVICE', 'default')
WEBHOOKS_REDIS_PASSWORD = WEBHOOKS_REDIS.get('PASSWORD', '') WEBHOOKS_REDIS_PASSWORD = WEBHOOKS_REDIS.get('PASSWORD', '')
WEBHOOKS_REDIS_DATABASE = WEBHOOKS_REDIS.get('DATABASE', 0) WEBHOOKS_REDIS_DATABASE = WEBHOOKS_REDIS.get('DATABASE', 0)
WEBHOOKS_REDIS_DEFAULT_TIMEOUT = WEBHOOKS_REDIS.get('DEFAULT_TIMEOUT', 300) WEBHOOKS_REDIS_DEFAULT_TIMEOUT = WEBHOOKS_REDIS.get('DEFAULT_TIMEOUT', 300)
WEBHOOKS_REDIS_SSL = WEBHOOKS_REDIS.get('SSL', False) WEBHOOKS_REDIS_SSL = WEBHOOKS_REDIS.get('SSL', False)
CACHING_REDIS = REDIS.get('caching', {}) CACHING_REDIS = REDIS.get('caching', {})
CACHING_REDIS_HOST = WEBHOOKS_REDIS.get('HOST', 'localhost') CACHING_REDIS_HOST = CACHING_REDIS.get('HOST', 'localhost')
CACHING_REDIS_PORT = WEBHOOKS_REDIS.get('PORT', 6379) CACHING_REDIS_PORT = CACHING_REDIS.get('PORT', 6379)
CACHING_REDIS_PASSWORD = WEBHOOKS_REDIS.get('PASSWORD', '') CACHING_REDIS_SENTINELS = CACHING_REDIS.get('SENTINELS', [])
CACHING_REDIS_DATABASE = WEBHOOKS_REDIS.get('DATABASE', 0) CACHING_REDIS_USING_SENTINEL = all([
CACHING_REDIS_DEFAULT_TIMEOUT = WEBHOOKS_REDIS.get('DEFAULT_TIMEOUT', 300) isinstance(CACHING_REDIS_SENTINELS, (list, tuple)),
CACHING_REDIS_SSL = WEBHOOKS_REDIS.get('SSL', False) len(CACHING_REDIS_SENTINELS) > 0
])
CACHING_REDIS_SENTINEL_SERVICE = CACHING_REDIS.get('SENTINEL_SERVICE', 'default')
CACHING_REDIS_PASSWORD = CACHING_REDIS.get('PASSWORD', '')
CACHING_REDIS_DATABASE = CACHING_REDIS.get('DATABASE', 0)
CACHING_REDIS_DEFAULT_TIMEOUT = CACHING_REDIS.get('DEFAULT_TIMEOUT', 300)
CACHING_REDIS_SSL = CACHING_REDIS.get('SSL', False)
# #
@ -393,28 +407,35 @@ if LDAP_CONFIG is not None:
# #
# Caching # Caching
# #
if CACHING_REDIS_USING_SENTINEL:
if CACHING_REDIS_SSL: CACHEOPS_SENTINEL = {
REDIS_CACHE_CON_STRING = 'rediss://' 'locations': CACHING_REDIS_SENTINELS,
'service_name': CACHING_REDIS_SENTINEL_SERVICE,
'db': CACHING_REDIS_DATABASE,
}
else: else:
if CACHING_REDIS_SSL:
REDIS_CACHE_CON_STRING = 'rediss://'
else:
REDIS_CACHE_CON_STRING = 'redis://' REDIS_CACHE_CON_STRING = 'redis://'
if CACHING_REDIS_PASSWORD: if CACHING_REDIS_PASSWORD:
REDIS_CACHE_CON_STRING = '{}:{}@'.format(REDIS_CACHE_CON_STRING, CACHING_REDIS_PASSWORD) REDIS_CACHE_CON_STRING = '{}:{}@'.format(REDIS_CACHE_CON_STRING, CACHING_REDIS_PASSWORD)
REDIS_CACHE_CON_STRING = '{}{}:{}/{}'.format( REDIS_CACHE_CON_STRING = '{}{}:{}/{}'.format(
REDIS_CACHE_CON_STRING, REDIS_CACHE_CON_STRING,
CACHING_REDIS_HOST, CACHING_REDIS_HOST,
CACHING_REDIS_PORT, CACHING_REDIS_PORT,
CACHING_REDIS_DATABASE CACHING_REDIS_DATABASE
) )
CACHEOPS_REDIS = REDIS_CACHE_CON_STRING
if not CACHE_TIMEOUT: if not CACHE_TIMEOUT:
CACHEOPS_ENABLED = False CACHEOPS_ENABLED = False
else: else:
CACHEOPS_ENABLED = True CACHEOPS_ENABLED = True
CACHEOPS_REDIS = REDIS_CACHE_CON_STRING
CACHEOPS_DEFAULTS = { CACHEOPS_DEFAULTS = {
'timeout': CACHE_TIMEOUT 'timeout': CACHE_TIMEOUT
} }
@ -533,6 +554,15 @@ RQ_QUEUES = {
'PASSWORD': WEBHOOKS_REDIS_PASSWORD, 'PASSWORD': WEBHOOKS_REDIS_PASSWORD,
'DEFAULT_TIMEOUT': WEBHOOKS_REDIS_DEFAULT_TIMEOUT, 'DEFAULT_TIMEOUT': WEBHOOKS_REDIS_DEFAULT_TIMEOUT,
'SSL': WEBHOOKS_REDIS_SSL, 'SSL': WEBHOOKS_REDIS_SSL,
} if not WEBHOOKS_REDIS_USING_SENTINEL else {
'SENTINELS': WEBHOOKS_REDIS_SENTINELS,
'MASTER_NAME': WEBHOOKS_REDIS_SENTINEL_SERVICE,
'DB': WEBHOOKS_REDIS_DATABASE,
'PASSWORD': WEBHOOKS_REDIS_PASSWORD,
'SOCKET_TIMEOUT': None,
'CONNECTION_KWARGS': {
'socket_connect_timeout': WEBHOOKS_REDIS_DEFAULT_TIMEOUT
},
} }
} }

View File

@ -1,6 +1,6 @@
import urllib.parse import urllib.parse
from django.test import TestCase from utilities.testing import TestCase
from django.urls import reverse from django.urls import reverse
@ -11,7 +11,7 @@ class HomeViewTestCase(TestCase):
url = reverse('home') url = reverse('home')
response = self.client.get(url) response = self.client.get(url)
self.assertEqual(response.status_code, 200) self.assertHttpStatus(response, 200)
def test_search(self): def test_search(self):
@ -21,4 +21,4 @@ class HomeViewTestCase(TestCase):
} }
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params))) response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
self.assertEqual(response.status_code, 200) self.assertHttpStatus(response, 200)

View File

@ -26,49 +26,49 @@ schema_view = get_schema_view(
_patterns = [ _patterns = [
# Base views # Base views
path(r'', HomeView.as_view(), name='home'), path('', HomeView.as_view(), name='home'),
path(r'search/', SearchView.as_view(), name='search'), path('search/', SearchView.as_view(), name='search'),
# Login/logout # Login/logout
path(r'login/', LoginView.as_view(), name='login'), path('login/', LoginView.as_view(), name='login'),
path(r'logout/', LogoutView.as_view(), name='logout'), path('logout/', LogoutView.as_view(), name='logout'),
# Apps # Apps
path(r'circuits/', include('circuits.urls')), path('circuits/', include('circuits.urls')),
path(r'dcim/', include('dcim.urls')), path('dcim/', include('dcim.urls')),
path(r'extras/', include('extras.urls')), path('extras/', include('extras.urls')),
path(r'ipam/', include('ipam.urls')), path('ipam/', include('ipam.urls')),
path(r'secrets/', include('secrets.urls')), path('secrets/', include('secrets.urls')),
path(r'tenancy/', include('tenancy.urls')), path('tenancy/', include('tenancy.urls')),
path(r'user/', include('users.urls')), path('user/', include('users.urls')),
path(r'virtualization/', include('virtualization.urls')), path('virtualization/', include('virtualization.urls')),
# API # API
path(r'api/', APIRootView.as_view(), name='api-root'), path('api/', APIRootView.as_view(), name='api-root'),
path(r'api/circuits/', include('circuits.api.urls')), path('api/circuits/', include('circuits.api.urls')),
path(r'api/dcim/', include('dcim.api.urls')), path('api/dcim/', include('dcim.api.urls')),
path(r'api/extras/', include('extras.api.urls')), path('api/extras/', include('extras.api.urls')),
path(r'api/ipam/', include('ipam.api.urls')), path('api/ipam/', include('ipam.api.urls')),
path(r'api/secrets/', include('secrets.api.urls')), path('api/secrets/', include('secrets.api.urls')),
path(r'api/tenancy/', include('tenancy.api.urls')), path('api/tenancy/', include('tenancy.api.urls')),
path(r'api/virtualization/', include('virtualization.api.urls')), path('api/virtualization/', include('virtualization.api.urls')),
path(r'api/docs/', schema_view.with_ui('swagger'), name='api_docs'), path('api/docs/', schema_view.with_ui('swagger'), name='api_docs'),
path(r'api/redoc/', schema_view.with_ui('redoc'), name='api_redocs'), path('api/redoc/', schema_view.with_ui('redoc'), name='api_redocs'),
re_path(r'^api/swagger(?P<format>.json|.yaml)$', schema_view.without_ui(), name='schema_swagger'), re_path(r'^api/swagger(?P<format>.json|.yaml)$', schema_view.without_ui(), name='schema_swagger'),
# Serving static media in Django to pipe it through LoginRequiredMiddleware # Serving static media in Django to pipe it through LoginRequiredMiddleware
path(r'media/<path:path>', serve, {'document_root': settings.MEDIA_ROOT}), path('media/<path:path>', serve, {'document_root': settings.MEDIA_ROOT}),
# Admin # Admin
path(r'admin/', admin_site.urls), path('admin/', admin_site.urls),
path(r'admin/webhook-backend-status/', include('django_rq.urls')), path('admin/webhook-backend-status/', include('django_rq.urls')),
] ]
if settings.DEBUG: if settings.DEBUG:
import debug_toolbar import debug_toolbar
_patterns += [ _patterns += [
path(r'__debug__/', include(debug_toolbar.urls)), path('__debug__/', include(debug_toolbar.urls)),
] ]
if settings.METRICS_ENABLED: if settings.METRICS_ENABLED:
@ -78,7 +78,7 @@ if settings.METRICS_ENABLED:
# Prepend BASE_PATH # Prepend BASE_PATH
urlpatterns = [ urlpatterns = [
path(r'{}'.format(settings.BASE_PATH), include(_patterns)) path('{}'.format(settings.BASE_PATH), include(_patterns))
] ]
handler500 = 'utilities.views.server_error' handler500 = 'utilities.views.server_error'

View File

@ -252,7 +252,7 @@ class HomeView(View):
'search_form': SearchForm(), 'search_form': SearchForm(),
'stats': stats, 'stats': stats,
'report_results': ReportResult.objects.order_by('-created')[:10], 'report_results': ReportResult.objects.order_by('-created')[:10],
'changelog': ObjectChange.objects.prefetch_related('user', 'changed_object_type')[:50] 'changelog': ObjectChange.objects.prefetch_related('user', 'changed_object_type')[:15]
}) })

View File

@ -62,8 +62,20 @@ footer p {
} }
} }
/* Scroll the drop-down menus at or above 768px wide to match bootstrap's behavior for hiding dropdown menus */
@media (min-width: 768px) {
.navbar-nav>li>ul {
max-height: calc(80vh - 50px);
overflow-y: auto;
}
}
/* Collapse the nav menu on displays less than 980px wide */ /* Collapse the nav menu on displays less than 980px wide */
@media (max-width: 979px) { @media (max-width: 979px) {
#navbar {
max-height: calc(80vh - 50px);
overflow-y: auto;
}
.navbar-header { .navbar-header {
float: none; float: none;
} }

View File

@ -56,3 +56,12 @@ text {
.blocked:hover+.add-device { .blocked:hover+.add-device {
fill: none; fill: none;
} }
.unit {
margin: 0;
padding: 5px 0px;
fill: #c0c0c0;
font-size: 10px;
font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;
}

View File

@ -0,0 +1,11 @@
$('.rendered-context-format').on('click', function() {
if (!$(this).hasClass('active')) {
// Update selection in the button group
$('span.rendered-context-format').removeClass('active');
$('span.rendered-context-format[data-format=' + $(this).data('format') + ']').addClass('active');
// Hide all rendered contexts and only show the selected one
$('div.rendered-context-data').hide();
$('div.rendered-context-data[data-format=' + $(this).data('format') + ']').show();
}
});

View File

@ -190,15 +190,18 @@ $(document).ready(function() {
$.each(element.attributes, function(index, attr){ $.each(element.attributes, function(index, attr){
if (attr.name.includes("data-additional-query-param-")){ if (attr.name.includes("data-additional-query-param-")){
var param_name = attr.name.split("data-additional-query-param-")[1]; var param_name = attr.name.split("data-additional-query-param-")[1];
$.each($.parseJSON(attr.value), function(index, value) {
if (param_name in parameters) { if (param_name in parameters) {
if (Array.isArray(parameters[param_name])) { if (Array.isArray(parameters[param_name])) {
parameters[param_name].push(attr.value) parameters[param_name].push(value);
} else { } else {
parameters[param_name] = [parameters[param_name], attr.value] parameters[param_name] = [parameters[param_name], value];
} }
} else { } else {
parameters[param_name] = attr.value; parameters[param_name] = value;
} }
});
} }
}); });
@ -220,19 +223,19 @@ $(document).ready(function() {
} }
if( record.group !== undefined && record.group !== null && record.site !== undefined && record.site !== null ) { if( record.group !== undefined && record.group !== null && record.site !== undefined && record.site !== null ) {
results[record.site.name + ":" + record.group.name] = results[record.site.name + ":" + record.group.name] || { text: record.site.name + " / " + record.group.name, children: [] } results[record.site.name + ":" + record.group.name] = results[record.site.name + ":" + record.group.name] || { text: record.site.name + " / " + record.group.name, children: [] };
results[record.site.name + ":" + record.group.name].children.push(record); results[record.site.name + ":" + record.group.name].children.push(record);
} }
else if( record.group !== undefined && record.group !== null ) { else if( record.group !== undefined && record.group !== null ) {
results[record.group.name] = results[record.group.name] || { text: record.group.name, children: [] } results[record.group.name] = results[record.group.name] || { text: record.group.name, children: [] };
results[record.group.name].children.push(record); results[record.group.name].children.push(record);
} }
else if( record.site !== undefined && record.site !== null ) { else if( record.site !== undefined && record.site !== null ) {
results[record.site.name] = results[record.site.name] || { text: record.site.name, children: [] } results[record.site.name] = results[record.site.name] || { text: record.site.name, children: [] };
results[record.site.name].children.push(record); results[record.site.name].children.push(record);
} }
else if ( (record.group !== undefined || record.group == null) && (record.site !== undefined || record.site === null) ) { else if ( (record.group !== undefined || record.group == null) && (record.site !== undefined || record.site === null) ) {
results['global'] = results['global'] || { text: 'Global', children: [] } results['global'] = results['global'] || { text: 'Global', children: [] };
results['global'].children.push(record); results['global'].children.push(record);
} }
else { else {
@ -246,10 +249,9 @@ $(document).ready(function() {
// Handle the null option, but only add it once // Handle the null option, but only add it once
if (element.getAttribute('data-null-option') && data.previous === null) { if (element.getAttribute('data-null-option') && data.previous === null) {
var null_option = $(element).children()[0];
results.unshift({ results.unshift({
id: null_option.value, id: 'null',
text: null_option.text text: 'None'
}); });
} }

View File

@ -15,15 +15,15 @@ router = routers.DefaultRouter()
router.APIRootView = SecretsRootView router.APIRootView = SecretsRootView
# Field choices # Field choices
router.register(r'_choices', views.SecretsFieldChoicesViewSet, basename='field-choice') router.register('_choices', views.SecretsFieldChoicesViewSet, basename='field-choice')
# Secrets # Secrets
router.register(r'secret-roles', views.SecretRoleViewSet) router.register('secret-roles', views.SecretRoleViewSet)
router.register(r'secrets', views.SecretViewSet) router.register('secrets', views.SecretViewSet)
# Miscellaneous # Miscellaneous
router.register(r'get-session-key', views.GetSessionKeyViewSet, basename='get-session-key') router.register('get-session-key', views.GetSessionKeyViewSet, basename='get-session-key')
router.register(r'generate-rsa-key-pair', views.GenerateRSAKeyPairViewSet, basename='generate-rsa-key-pair') router.register('generate-rsa-key-pair', views.GenerateRSAKeyPairViewSet, basename='generate-rsa-key-pair')
app_name = 'secrets-api' app_name = 'secrets-api'
urlpatterns = router.urls urlpatterns = router.urls

View File

@ -93,8 +93,8 @@ class SecretViewSet(ModelViewSet):
secret = self.get_object() secret = self.get_object()
# Attempt to decrypt the secret if the master key is known # Attempt to decrypt the secret if the user is permitted and the master key is known
if self.master_key is not None: if secret.decryptable_by(request.user) and self.master_key is not None:
secret.decrypt(self.master_key) secret.decrypt(self.master_key)
serializer = self.get_serializer(secret) serializer = self.get_serializer(secret)
@ -111,6 +111,8 @@ class SecretViewSet(ModelViewSet):
if self.master_key is not None: if self.master_key is not None:
secrets = [] secrets = []
for secret in page: for secret in page:
# Enforce role permissions
if secret.decryptable_by(request.user):
secret.decrypt(self.master_key) secret.decrypt(self.master_key)
secrets.append(secret) secrets.append(secret)
serializer = self.get_serializer(secrets, many=True) serializer = self.get_serializer(secrets, many=True)

View File

@ -4,10 +4,12 @@ from django import forms
from taggit.forms import TagField from taggit.forms import TagField
from dcim.models import Device from dcim.models import Device
from extras.forms import AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldFilterForm, CustomFieldForm from extras.forms import (
AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldFilterForm, CustomFieldModelForm, CustomFieldModelCSVForm,
)
from utilities.forms import ( from utilities.forms import (
APISelect, APISelectMultiple, BootstrapMixin, FilterChoiceField, FlexibleModelChoiceField, SlugField, APISelect, APISelectMultiple, BootstrapMixin, DynamicModelChoiceField, DynamicModelMultipleChoiceField,
StaticSelect2Multiple FlexibleModelChoiceField, SlugField, StaticSelect2Multiple, TagFilterField,
) )
from .constants import * from .constants import *
from .models import Secret, SecretRole, UserKey from .models import Secret, SecretRole, UserKey
@ -68,7 +70,7 @@ class SecretRoleCSVForm(forms.ModelForm):
# Secrets # Secrets
# #
class SecretForm(BootstrapMixin, CustomFieldForm): class SecretForm(BootstrapMixin, CustomFieldModelForm):
plaintext = forms.CharField( plaintext = forms.CharField(
max_length=SECRET_PLAINTEXT_MAX_LENGTH, max_length=SECRET_PLAINTEXT_MAX_LENGTH,
required=False, required=False,
@ -85,6 +87,12 @@ class SecretForm(BootstrapMixin, CustomFieldForm):
label='Plaintext (verify)', label='Plaintext (verify)',
widget=forms.PasswordInput() widget=forms.PasswordInput()
) )
role = DynamicModelChoiceField(
queryset=SecretRole.objects.all(),
widget=APISelect(
api_url="/api/secrets/secret-roles/"
)
)
tags = TagField( tags = TagField(
required=False required=False
) )
@ -94,11 +102,6 @@ class SecretForm(BootstrapMixin, CustomFieldForm):
fields = [ fields = [
'role', 'name', 'plaintext', 'plaintext2', 'tags', 'role', 'name', 'plaintext', 'plaintext2', 'tags',
] ]
widgets = {
'role': APISelect(
api_url="/api/secrets/secret-roles/"
)
}
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@ -116,7 +119,7 @@ class SecretForm(BootstrapMixin, CustomFieldForm):
}) })
class SecretCSVForm(forms.ModelForm): class SecretCSVForm(CustomFieldModelCSVForm):
device = FlexibleModelChoiceField( device = FlexibleModelChoiceField(
queryset=Device.objects.all(), queryset=Device.objects.all(),
to_field_name='name', to_field_name='name',
@ -155,7 +158,7 @@ class SecretBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF
queryset=Secret.objects.all(), queryset=Secret.objects.all(),
widget=forms.MultipleHiddenInput() widget=forms.MultipleHiddenInput()
) )
role = forms.ModelChoiceField( role = DynamicModelChoiceField(
queryset=SecretRole.objects.all(), queryset=SecretRole.objects.all(),
required=False, required=False,
widget=APISelect( widget=APISelect(
@ -179,14 +182,16 @@ class SecretFilterForm(BootstrapMixin, CustomFieldFilterForm):
required=False, required=False,
label='Search' label='Search'
) )
role = FilterChoiceField( role = DynamicModelMultipleChoiceField(
queryset=SecretRole.objects.all(), queryset=SecretRole.objects.all(),
to_field_name='slug', to_field_name='slug',
required=True,
widget=APISelectMultiple( widget=APISelectMultiple(
api_url="/api/secrets/secret-roles/", api_url="/api/secrets/secret-roles/",
value_field="slug", value_field="slug",
) )
) )
tag = TagFilterField(model)
# #

View File

@ -5,7 +5,8 @@ from rest_framework import status
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
from secrets.models import Secret, SecretRole, SessionKey, UserKey from secrets.models import Secret, SecretRole, SessionKey, UserKey
from utilities.testing import APITestCase from users.models import Token
from utilities.testing import APITestCase, create_test_user
from .constants import PRIVATE_KEY, PUBLIC_KEY from .constants import PRIVATE_KEY, PUBLIC_KEY
@ -131,7 +132,15 @@ class SecretTest(APITestCase):
def setUp(self): def setUp(self):
super().setUp() # Create a non-superuser test user
self.user = create_test_user('testuser', permissions=(
'secrets.add_secret',
'secrets.change_secret',
'secrets.delete_secret',
'secrets.view_secret',
))
self.token = Token.objects.create(user=self.user)
self.header = {'HTTP_AUTHORIZATION': 'Token {}'.format(self.token.key)}
userkey = UserKey(user=self.user, public_key=PUBLIC_KEY) userkey = UserKey(user=self.user, public_key=PUBLIC_KEY)
userkey.save() userkey.save()
@ -144,11 +153,11 @@ class SecretTest(APITestCase):
'HTTP_X_SESSION_KEY': base64.b64encode(session_key.key), 'HTTP_X_SESSION_KEY': base64.b64encode(session_key.key),
} }
self.plaintext = { self.plaintexts = (
'secret1': 'Secret #1 Plaintext', 'Secret #1 Plaintext',
'secret2': 'Secret #2 Plaintext', 'Secret #2 Plaintext',
'secret3': 'Secret #3 Plaintext', 'Secret #3 Plaintext',
} )
site = Site.objects.create(name='Test Site 1', slug='test-site-1') site = Site.objects.create(name='Test Site 1', slug='test-site-1')
manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1') manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
@ -160,17 +169,17 @@ class SecretTest(APITestCase):
self.secretrole1 = SecretRole.objects.create(name='Test Secret Role 1', slug='test-secret-role-1') self.secretrole1 = SecretRole.objects.create(name='Test Secret Role 1', slug='test-secret-role-1')
self.secretrole2 = SecretRole.objects.create(name='Test Secret Role 2', slug='test-secret-role-2') self.secretrole2 = SecretRole.objects.create(name='Test Secret Role 2', slug='test-secret-role-2')
self.secret1 = Secret( self.secret1 = Secret(
device=self.device, role=self.secretrole1, name='Test Secret 1', plaintext=self.plaintext['secret1'] device=self.device, role=self.secretrole1, name='Test Secret 1', plaintext=self.plaintexts[0]
) )
self.secret1.encrypt(self.master_key) self.secret1.encrypt(self.master_key)
self.secret1.save() self.secret1.save()
self.secret2 = Secret( self.secret2 = Secret(
device=self.device, role=self.secretrole1, name='Test Secret 2', plaintext=self.plaintext['secret2'] device=self.device, role=self.secretrole1, name='Test Secret 2', plaintext=self.plaintexts[1]
) )
self.secret2.encrypt(self.master_key) self.secret2.encrypt(self.master_key)
self.secret2.save() self.secret2.save()
self.secret3 = Secret( self.secret3 = Secret(
device=self.device, role=self.secretrole1, name='Test Secret 3', plaintext=self.plaintext['secret3'] device=self.device, role=self.secretrole1, name='Test Secret 3', plaintext=self.plaintexts[2]
) )
self.secret3.encrypt(self.master_key) self.secret3.encrypt(self.master_key)
self.secret3.save() self.secret3.save()
@ -178,16 +187,32 @@ class SecretTest(APITestCase):
def test_get_secret(self): def test_get_secret(self):
url = reverse('secrets-api:secret-detail', kwargs={'pk': self.secret1.pk}) url = reverse('secrets-api:secret-detail', kwargs={'pk': self.secret1.pk})
response = self.client.get(url, **self.header)
self.assertEqual(response.data['plaintext'], self.plaintext['secret1']) # Secret plaintext not be decrypted as the user has not been assigned to the role
response = self.client.get(url, **self.header)
self.assertIsNone(response.data['plaintext'])
# The plaintext should be present once the user has been assigned to the role
self.secretrole1.users.add(self.user)
response = self.client.get(url, **self.header)
self.assertEqual(response.data['plaintext'], self.plaintexts[0])
def test_list_secrets(self): def test_list_secrets(self):
url = reverse('secrets-api:secret-list') url = reverse('secrets-api:secret-list')
response = self.client.get(url, **self.header)
# Secret plaintext not be decrypted as the user has not been assigned to the role
response = self.client.get(url, **self.header)
self.assertEqual(response.data['count'], 3) self.assertEqual(response.data['count'], 3)
for secret in response.data['results']:
self.assertIsNone(secret['plaintext'])
# The plaintext should be present once the user has been assigned to the role
self.secretrole1.users.add(self.user)
response = self.client.get(url, **self.header)
self.assertEqual(response.data['count'], 3)
for i, secret in enumerate(response.data['results']):
self.assertEqual(secret['plaintext'], self.plaintexts[i])
def test_create_secret(self): def test_create_secret(self):

View File

@ -1,26 +1,18 @@
import base64 import base64
import urllib.parse
from django.test import Client, TestCase
from django.urls import reverse from django.urls import reverse
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
from secrets.models import Secret, SecretRole, SessionKey, UserKey from secrets.models import Secret, SecretRole, SessionKey, UserKey
from utilities.testing import create_test_user from utilities.testing import ViewTestCases
from .constants import PRIVATE_KEY, PUBLIC_KEY from .constants import PRIVATE_KEY, PUBLIC_KEY
class SecretRoleTestCase(TestCase): class SecretRoleTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
model = SecretRole
def setUp(self): @classmethod
user = create_test_user( def setUpTestData(cls):
permissions=[
'secrets.view_secretrole',
'secrets.add_secretrole',
]
)
self.client = Client()
self.client.force_login(user)
SecretRole.objects.bulk_create([ SecretRole.objects.bulk_create([
SecretRole(name='Secret Role 1', slug='secret-role-1'), SecretRole(name='Secret Role 1', slug='secret-role-1'),
@ -28,89 +20,83 @@ class SecretRoleTestCase(TestCase):
SecretRole(name='Secret Role 3', slug='secret-role-3'), SecretRole(name='Secret Role 3', slug='secret-role-3'),
]) ])
def test_secretrole_list(self): cls.form_data = {
'name': 'Secret Role X',
'slug': 'secret-role-x',
'description': 'A secret role',
'users': [],
'groups': [],
}
url = reverse('secrets:secretrole_list') cls.csv_data = (
response = self.client.get(url, follow=True)
self.assertEqual(response.status_code, 200)
def test_secretrole_import(self):
csv_data = (
"name,slug", "name,slug",
"Secret Role 4,secret-role-4", "Secret Role 4,secret-role-4",
"Secret Role 5,secret-role-5", "Secret Role 5,secret-role-5",
"Secret Role 6,secret-role-6", "Secret Role 6,secret-role-6",
) )
response = self.client.post(reverse('secrets:secretrole_import'), {'csv': '\n'.join(csv_data)})
self.assertEqual(response.status_code, 200) class SecretTestCase(ViewTestCases.PrimaryObjectViewTestCase):
self.assertEqual(SecretRole.objects.count(), 6) model = Secret
# Disable inapplicable tests
test_create_object = None
class SecretTestCase(TestCase): # TODO: Check permissions enforcement on secrets.views.secret_edit
test_edit_object = None
@classmethod
def setUpTestData(cls):
site = Site.objects.create(name='Site 1', slug='site-1')
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1')
devicerole = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1')
devices = (
Device(name='Device 1', site=site, device_type=devicetype, device_role=devicerole),
Device(name='Device 2', site=site, device_type=devicetype, device_role=devicerole),
Device(name='Device 3', site=site, device_type=devicetype, device_role=devicerole),
)
Device.objects.bulk_create(devices)
secretroles = (
SecretRole(name='Secret Role 1', slug='secret-role-1'),
SecretRole(name='Secret Role 2', slug='secret-role-2'),
)
SecretRole.objects.bulk_create(secretroles)
# Create one secret per device to allow bulk-editing of names (which must be unique per device/role)
Secret.objects.bulk_create((
Secret(device=devices[0], role=secretroles[0], name='Secret 1', ciphertext=b'1234567890'),
Secret(device=devices[1], role=secretroles[0], name='Secret 2', ciphertext=b'1234567890'),
Secret(device=devices[2], role=secretroles[0], name='Secret 3', ciphertext=b'1234567890'),
))
cls.form_data = {
'device': devices[1].pk,
'role': secretroles[1].pk,
'name': 'Secret X',
}
cls.bulk_edit_data = {
'role': secretroles[1].pk,
'name': 'New name',
}
def setUp(self): def setUp(self):
user = create_test_user(
permissions=[
'secrets.view_secret',
'secrets.add_secret',
]
)
# Set up a master key super().setUp()
userkey = UserKey(user=user, public_key=PUBLIC_KEY)
# Set up a master key for the test user
userkey = UserKey(user=self.user, public_key=PUBLIC_KEY)
userkey.save() userkey.save()
master_key = userkey.get_master_key(PRIVATE_KEY) master_key = userkey.get_master_key(PRIVATE_KEY)
self.session_key = SessionKey(userkey=userkey) self.session_key = SessionKey(userkey=userkey)
self.session_key.save(master_key) self.session_key.save(master_key)
self.client = Client() def test_import_objects(self):
self.client.force_login(user) self.add_permissions('secrets.add_secret')
site = Site(name='Site 1', slug='site-1')
site.save()
manufacturer = Manufacturer(name='Manufacturer 1', slug='manufacturer-1')
manufacturer.save()
devicetype = DeviceType(manufacturer=manufacturer, model='Device Type 1')
devicetype.save()
devicerole = DeviceRole(name='Device Role 1', slug='device-role-1')
devicerole.save()
device = Device(name='Device 1', site=site, device_type=devicetype, device_role=devicerole)
device.save()
secretrole = SecretRole(name='Secret Role 1', slug='secret-role-1')
secretrole.save()
Secret.objects.bulk_create([
Secret(device=device, role=secretrole, name='Secret 1', ciphertext=b'1234567890'),
Secret(device=device, role=secretrole, name='Secret 2', ciphertext=b'1234567890'),
Secret(device=device, role=secretrole, name='Secret 3', ciphertext=b'1234567890'),
])
def test_secret_list(self):
url = reverse('secrets:secret_list')
params = {
"role": SecretRole.objects.first().slug,
}
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)), follow=True)
self.assertEqual(response.status_code, 200)
def test_secret(self):
secret = Secret.objects.first()
response = self.client.get(secret.get_absolute_url(), follow=True)
self.assertEqual(response.status_code, 200)
def test_secret_import(self):
csv_data = ( csv_data = (
"device,role,name,plaintext", "device,role,name,plaintext",
@ -125,5 +111,5 @@ class SecretTestCase(TestCase):
response = self.client.post(reverse('secrets:secret_import'), {'csv': '\n'.join(csv_data)}) response = self.client.post(reverse('secrets:secret_import'), {'csv': '\n'.join(csv_data)})
self.assertEqual(response.status_code, 200) self.assertHttpStatus(response, 200)
self.assertEqual(Secret.objects.count(), 6) self.assertEqual(Secret.objects.count(), 6)

View File

@ -8,21 +8,21 @@ app_name = 'secrets'
urlpatterns = [ urlpatterns = [
# Secret roles # Secret roles
path(r'secret-roles/', views.SecretRoleListView.as_view(), name='secretrole_list'), path('secret-roles/', views.SecretRoleListView.as_view(), name='secretrole_list'),
path(r'secret-roles/add/', views.SecretRoleCreateView.as_view(), name='secretrole_add'), path('secret-roles/add/', views.SecretRoleCreateView.as_view(), name='secretrole_add'),
path(r'secret-roles/import/', views.SecretRoleBulkImportView.as_view(), name='secretrole_import'), path('secret-roles/import/', views.SecretRoleBulkImportView.as_view(), name='secretrole_import'),
path(r'secret-roles/delete/', views.SecretRoleBulkDeleteView.as_view(), name='secretrole_bulk_delete'), path('secret-roles/delete/', views.SecretRoleBulkDeleteView.as_view(), name='secretrole_bulk_delete'),
path(r'secret-roles/<slug:slug>/edit/', views.SecretRoleEditView.as_view(), name='secretrole_edit'), path('secret-roles/<slug:slug>/edit/', views.SecretRoleEditView.as_view(), name='secretrole_edit'),
path(r'secret-roles/<slug:slug>/changelog/', ObjectChangeLogView.as_view(), name='secretrole_changelog', kwargs={'model': SecretRole}), path('secret-roles/<slug:slug>/changelog/', ObjectChangeLogView.as_view(), name='secretrole_changelog', kwargs={'model': SecretRole}),
# Secrets # Secrets
path(r'secrets/', views.SecretListView.as_view(), name='secret_list'), path('secrets/', views.SecretListView.as_view(), name='secret_list'),
path(r'secrets/import/', views.SecretBulkImportView.as_view(), name='secret_import'), path('secrets/import/', views.SecretBulkImportView.as_view(), name='secret_import'),
path(r'secrets/edit/', views.SecretBulkEditView.as_view(), name='secret_bulk_edit'), path('secrets/edit/', views.SecretBulkEditView.as_view(), name='secret_bulk_edit'),
path(r'secrets/delete/', views.SecretBulkDeleteView.as_view(), name='secret_bulk_delete'), path('secrets/delete/', views.SecretBulkDeleteView.as_view(), name='secret_bulk_delete'),
path(r'secrets/<int:pk>/', views.SecretView.as_view(), name='secret'), path('secrets/<int:pk>/', views.SecretView.as_view(), name='secret'),
path(r'secrets/<int:pk>/edit/', views.secret_edit, name='secret_edit'), path('secrets/<int:pk>/edit/', views.secret_edit, name='secret_edit'),
path(r'secrets/<int:pk>/delete/', views.SecretDeleteView.as_view(), name='secret_delete'), path('secrets/<int:pk>/delete/', views.SecretDeleteView.as_view(), name='secret_delete'),
path(r'secrets/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='secret_changelog', kwargs={'model': Secret}), path('secrets/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='secret_changelog', kwargs={'model': Secret}),
] ]

View File

@ -35,7 +35,6 @@ class SecretRoleListView(PermissionRequiredMixin, ObjectListView):
permission_required = 'secrets.view_secretrole' permission_required = 'secrets.view_secretrole'
queryset = SecretRole.objects.annotate(secret_count=Count('secrets')) queryset = SecretRole.objects.annotate(secret_count=Count('secrets'))
table = tables.SecretRoleTable table = tables.SecretRoleTable
template_name = 'secrets/secretrole_list.html'
class SecretRoleCreateView(PermissionRequiredMixin, ObjectEditView): class SecretRoleCreateView(PermissionRequiredMixin, ObjectEditView):
@ -73,7 +72,7 @@ class SecretListView(PermissionRequiredMixin, ObjectListView):
filterset = filters.SecretFilterSet filterset = filters.SecretFilterSet
filterset_form = forms.SecretFilterForm filterset_form = forms.SecretFilterForm
table = tables.SecretTable table = tables.SecretTable
template_name = 'secrets/secret_list.html' action_buttons = ('import', 'export')
class SecretView(PermissionRequiredMixin, View): class SecretView(PermissionRequiredMixin, View):

View File

@ -1,22 +0,0 @@
{% extends '_base.html' %}
{% load buttons %}
{% block content %}
<div class="pull-right noprint">
{% if perms.circuits.add_circuit %}
{% add_button 'circuits:circuit_add' %}
{% import_button 'circuits:circuit_import' %}
{% endif %}
{% export_button content_type %}
</div>
<h1>{% block title %}Circuits{% endblock %}</h1>
<div class="row">
<div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_edit_url='circuits:circuit_bulk_edit' bulk_delete_url='circuits:circuit_bulk_delete' %}
</div>
<div class="col-md-3 noprint">
{% include 'inc/search_panel.html' %}
{% include 'inc/tags_panel.html' %}
</div>
</div>
{% endblock %}

View File

@ -1,18 +0,0 @@
{% extends '_base.html' %}
{% load buttons %}
{% block content %}
<div class="pull-right noprint">
{% if perms.circuits.add_circuittype %}
{% add_button 'circuits:circuittype_add' %}
{% import_button 'circuits:circuittype_import' %}
{% endif %}
{% export_button content_type %}
</div>
<h1>{% block title %}Circuit Types{% endblock %}</h1>
<div class="row">
<div class="col-md-12">
{% include 'utilities/obj_table.html' with bulk_delete_url='circuits:circuittype_bulk_delete' %}
</div>
</div>
{% endblock %}

View File

@ -1,22 +0,0 @@
{% extends '_base.html' %}
{% load buttons %}
{% block content %}
<div class="pull-right noprint">
{% if perms.circuits.add_provider %}
{% add_button 'circuits:provider_add' %}
{% import_button 'circuits:provider_import' %}
{% endif %}
{% export_button content_type %}
</div>
<h1>{% block title %}Providers{% endblock %}</h1>
<div class="row">
<div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_edit_url='circuits:provider_bulk_edit' bulk_delete_url='circuits:provider_bulk_delete' %}
</div>
<div class="col-md-3 noprint">
{% include 'inc/search_panel.html' %}
{% include 'inc/tags_panel.html' %}
</div>
</div>
{% endblock %}

View File

@ -1,20 +0,0 @@
{% extends '_base.html' %}
{% load buttons %}
{% block content %}
<div class="pull-right noprint">
{% if perms.dcim.add_cable %}
{% import_button 'dcim:cable_import' %}
{% endif %}
{% export_button content_type %}
</div>
<h1>{% block title %}Cables{% endblock %}</h1>
<div class="row">
<div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_edit_url='dcim:cable_bulk_edit' bulk_delete_url='dcim:cable_bulk_delete' %}
</div>
<div class="col-md-3 noprint">
{% include 'inc/search_panel.html' %}
</div>
</div>
{% endblock %}

View File

@ -48,14 +48,30 @@
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add Components <span class="caret"></span> <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add Components <span class="caret"></span>
</button> </button>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
{% if perms.dcim.add_consoleport %}<li><a href="{% url 'dcim:consoleport_add' pk=device.pk %}">Console Ports</a></li>{% endif %} {% if perms.dcim.add_consoleport %}
{% if perms.dcim.add_consoleserverport %}<li><a href="{% url 'dcim:consoleserverport_add' pk=device.pk %}">Console Server Ports</a></li>{% endif %} <li><a href="{% url 'dcim:consoleport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}">Console Ports</a></li>
{% if perms.dcim.add_powerport %}<li><a href="{% url 'dcim:powerport_add' pk=device.pk %}">Power Ports</a></li>{% endif %} {% endif %}
{% if perms.dcim.add_poweroutlet %}<li><a href="{% url 'dcim:poweroutlet_add' pk=device.pk %}">Power Outlets</a></li>{% endif %} {% if perms.dcim.add_consoleserverport %}
{% if perms.dcim.add_interface %}<li><a href="{% url 'dcim:interface_add' pk=device.pk %}">Interfaces</a></li>{% endif %} <li><a href="{% url 'dcim:consoleserverport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}">Console Server Ports</a></li>
{% if perms.dcim.add_frontport %}<li><a href="{% url 'dcim:frontport_add' pk=device.pk %}">Front Ports</a></li>{% endif %} {% endif %}
{% if perms.dcim.add_rearport %}<li><a href="{% url 'dcim:rearport_add' pk=device.pk %}">Rear Ports</a></li>{% endif %} {% if perms.dcim.add_powerport %}
{% if perms.dcim.add_devicebay %}<li><a href="{% url 'dcim:devicebay_add' pk=device.pk %}">Device Bays</a></li>{% endif %} <li><a href="{% url 'dcim:powerport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}">Power Ports</a></li>
{% endif %}
{% if perms.dcim.add_poweroutlet %}
<li><a href="{% url 'dcim:poweroutlet_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}">Power Outlets</a></li>
{% endif %}
{% if perms.dcim.add_interface %}
<li><a href="{% url 'dcim:interface_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}">Interfaces</a></li>
{% endif %}
{% if perms.dcim.add_frontport %}
<li><a href="{% url 'dcim:frontport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}">Front Ports</a></li>
{% endif %}
{% if perms.dcim.add_rearport %}
<li><a href="{% url 'dcim:rearport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}">Rear Ports</a></li>
{% endif %}
{% if perms.dcim.add_devicebay %}
<li><a href="{% url 'dcim:devicebay_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}">Device Bays</a></li>
{% endif %}
</ul> </ul>
</div> </div>
{% endif %} {% endif %}
@ -333,12 +349,12 @@
{% if perms.dcim.add_interface or perms.dcim.add_consoleport or perms.dcim.add_powerport %} {% if perms.dcim.add_interface or perms.dcim.add_consoleport or perms.dcim.add_powerport %}
<div class="panel-footer text-right noprint"> <div class="panel-footer text-right noprint">
{% if perms.dcim.add_consoleport %} {% if perms.dcim.add_consoleport %}
<a href="{% url 'dcim:consoleport_add' pk=device.pk %}" class="btn btn-xs btn-primary"> <a href="{% url 'dcim:consoleport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-xs btn-primary">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add console port <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add console port
</a> </a>
{% endif %} {% endif %}
{% if perms.dcim.add_powerport %} {% if perms.dcim.add_powerport %}
<a href="{% url 'dcim:powerport_add' pk=device.pk %}" class="btn btn-xs btn-primary"> <a href="{% url 'dcim:powerport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-xs btn-primary">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add power port <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add power port
</a> </a>
{% endif %} {% endif %}
@ -524,13 +540,13 @@
</button> </button>
{% endif %} {% endif %}
{% if device_bays and perms.dcim.delete_devicebay %} {% if device_bays and perms.dcim.delete_devicebay %}
<button type="submit" formaction="{% url 'dcim:devicebay_bulk_delete' pk=device.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs"> <button type="submit" formaction="{% url 'dcim:devicebay_bulk_delete' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete selected <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete selected
</button> </button>
{% endif %} {% endif %}
{% if perms.dcim.add_devicebay %} {% if perms.dcim.add_devicebay %}
<div class="pull-right"> <div class="pull-right">
<a href="{% url 'dcim:devicebay_add' pk=device.pk %}" class="btn btn-primary btn-xs"> <a href="{% url 'dcim:devicebay_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-primary btn-xs">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add device bays <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add device bays
</a> </a>
</div> </div>
@ -587,7 +603,7 @@
<button type="submit" name="_rename" formaction="{% url 'dcim:interface_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs"> <button type="submit" name="_rename" formaction="{% url 'dcim:interface_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
</button> </button>
<button type="submit" name="_edit" formaction="{% url 'dcim:interface_bulk_edit' pk=device.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs"> <button type="submit" name="_edit" formaction="{% url 'dcim:interface_bulk_edit' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
</button> </button>
{% endif %} {% endif %}
@ -597,13 +613,13 @@
</button> </button>
{% endif %} {% endif %}
{% if interfaces and perms.dcim.delete_interface %} {% if interfaces and perms.dcim.delete_interface %}
<button type="submit" name="_delete" formaction="{% url 'dcim:interface_bulk_delete' pk=device.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs"> <button type="submit" name="_delete" formaction="{% url 'dcim:interface_bulk_delete' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
</button> </button>
{% endif %} {% endif %}
{% if perms.dcim.add_interface %} {% if perms.dcim.add_interface %}
<div class="pull-right"> <div class="pull-right">
<a href="{% url 'dcim:interface_add' pk=device.pk %}" class="btn btn-primary btn-xs"> <a href="{% url 'dcim:interface_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-primary btn-xs">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add interfaces <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add interfaces
</a> </a>
</div> </div>
@ -619,6 +635,7 @@
{% if perms.dcim.delete_consoleserverport %} {% if perms.dcim.delete_consoleserverport %}
<form method="post"> <form method="post">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="device" value="{{ device.pk }}" />
{% endif %} {% endif %}
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
@ -649,7 +666,7 @@
<button type="submit" name="_rename" formaction="{% url 'dcim:consoleserverport_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs"> <button type="submit" name="_rename" formaction="{% url 'dcim:consoleserverport_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
</button> </button>
<button type="submit" name="_edit" formaction="{% url 'dcim:consoleserverport_bulk_edit' pk=device.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs"> <button type="submit" name="_edit" formaction="{% url 'dcim:consoleserverport_bulk_edit' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
</button> </button>
<button type="submit" name="_disconnect" formaction="{% url 'dcim:consoleserverport_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs"> <button type="submit" name="_disconnect" formaction="{% url 'dcim:consoleserverport_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
@ -657,13 +674,13 @@
</button> </button>
{% endif %} {% endif %}
{% if consoleserverports and perms.dcim.delete_consoleserverport %} {% if consoleserverports and perms.dcim.delete_consoleserverport %}
<button type="submit" formaction="{% url 'dcim:consoleserverport_bulk_delete' pk=device.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs"> <button type="submit" formaction="{% url 'dcim:consoleserverport_bulk_delete' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
</button> </button>
{% endif %} {% endif %}
{% if perms.dcim.add_consoleserverport %} {% if perms.dcim.add_consoleserverport %}
<div class="pull-right"> <div class="pull-right">
<a href="{% url 'dcim:consoleserverport_add' pk=device.pk %}" class="btn btn-primary btn-xs"> <a href="{% url 'dcim:consoleserverport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-primary btn-xs">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add console server ports <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add console server ports
</a> </a>
</div> </div>
@ -679,6 +696,7 @@
{% if perms.dcim.delete_poweroutlet %} {% if perms.dcim.delete_poweroutlet %}
<form method="post"> <form method="post">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="device" value="{{ device.pk }}" />
{% endif %} {% endif %}
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
@ -710,7 +728,7 @@
<button type="submit" name="_rename" formaction="{% url 'dcim:poweroutlet_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs"> <button type="submit" name="_rename" formaction="{% url 'dcim:poweroutlet_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
</button> </button>
<button type="submit" name="_edit" formaction="{% url 'dcim:poweroutlet_bulk_edit' pk=device.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs"> <button type="submit" name="_edit" formaction="{% url 'dcim:poweroutlet_bulk_edit' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
</button> </button>
<button type="submit" name="_disconnect" formaction="{% url 'dcim:poweroutlet_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs"> <button type="submit" name="_disconnect" formaction="{% url 'dcim:poweroutlet_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
@ -718,13 +736,13 @@
</button> </button>
{% endif %} {% endif %}
{% if poweroutlets and perms.dcim.delete_poweroutlet %} {% if poweroutlets and perms.dcim.delete_poweroutlet %}
<button type="submit" formaction="{% url 'dcim:poweroutlet_bulk_delete' pk=device.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs"> <button type="submit" formaction="{% url 'dcim:poweroutlet_bulk_delete' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
</button> </button>
{% endif %} {% endif %}
{% if perms.dcim.add_poweroutlet %} {% if perms.dcim.add_poweroutlet %}
<div class="pull-right"> <div class="pull-right">
<a href="{% url 'dcim:poweroutlet_add' pk=device.pk %}" class="btn btn-primary btn-xs"> <a href="{% url 'dcim:poweroutlet_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-primary btn-xs">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add power outlets <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add power outlets
</a> </a>
</div> </div>
@ -739,6 +757,7 @@
{% if front_ports %} {% if front_ports %}
<form method="post"> <form method="post">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="device" value="{{ device.pk }}" />
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<strong>Front Ports</strong> <strong>Front Ports</strong>
@ -770,7 +789,7 @@
<button type="submit" name="_rename" formaction="{% url 'dcim:frontport_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs"> <button type="submit" name="_rename" formaction="{% url 'dcim:frontport_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
</button> </button>
<button type="submit" name="_edit" formaction="{% url 'dcim:frontport_bulk_edit' pk=device.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs"> <button type="submit" name="_edit" formaction="{% url 'dcim:frontport_bulk_edit' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
</button> </button>
<button type="submit" name="_disconnect" formaction="{% url 'dcim:frontport_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs"> <button type="submit" name="_disconnect" formaction="{% url 'dcim:frontport_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
@ -778,13 +797,13 @@
</button> </button>
{% endif %} {% endif %}
{% if front_ports and perms.dcim.delete_frontport %} {% if front_ports and perms.dcim.delete_frontport %}
<button type="submit" formaction="{% url 'dcim:frontport_bulk_delete' pk=device.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs"> <button type="submit" formaction="{% url 'dcim:frontport_bulk_delete' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
</button> </button>
{% endif %} {% endif %}
{% if perms.dcim.add_frontport %} {% if perms.dcim.add_frontport %}
<div class="pull-right"> <div class="pull-right">
<a href="{% url 'dcim:frontport_add' pk=device.pk %}" class="btn btn-primary btn-xs"> <a href="{% url 'dcim:frontport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-primary btn-xs">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add front ports <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add front ports
</a> </a>
</div> </div>
@ -797,6 +816,7 @@
{% if rear_ports %} {% if rear_ports %}
<form method="post"> <form method="post">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="device" value="{{ device.pk }}" />
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<strong>Rear Ports</strong> <strong>Rear Ports</strong>
@ -827,7 +847,7 @@
<button type="submit" name="_rename" formaction="{% url 'dcim:rearport_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs"> <button type="submit" name="_rename" formaction="{% url 'dcim:rearport_bulk_rename' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
</button> </button>
<button type="submit" name="_edit" formaction="{% url 'dcim:rearport_bulk_edit' pk=device.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs"> <button type="submit" name="_edit" formaction="{% url 'dcim:rearport_bulk_edit' %}?return_url={{ device.get_absolute_url }}" class="btn btn-warning btn-xs">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
</button> </button>
<button type="submit" name="_disconnect" formaction="{% url 'dcim:rearport_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs"> <button type="submit" name="_disconnect" formaction="{% url 'dcim:rearport_bulk_disconnect' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
@ -835,13 +855,13 @@
</button> </button>
{% endif %} {% endif %}
{% if rear_ports and perms.dcim.delete_rearport %} {% if rear_ports and perms.dcim.delete_rearport %}
<button type="submit" formaction="{% url 'dcim:rearport_bulk_delete' pk=device.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs"> <button type="submit" formaction="{% url 'dcim:rearport_bulk_delete' %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
</button> </button>
{% endif %} {% endif %}
{% if perms.dcim.add_rearport %} {% if perms.dcim.add_rearport %}
<div class="pull-right"> <div class="pull-right">
<a href="{% url 'dcim:rearport_add' pk=device.pk %}" class="btn btn-primary btn-xs"> <a href="{% url 'dcim:rearport_add' %}?device={{ device.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-primary btn-xs">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add rear ports <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add rear ports
</a> </a>
</div> </div>

View File

@ -1,10 +1,10 @@
{% extends '_base.html' %} {% extends '_base.html' %}
{% load form_helpers %} {% load form_helpers %}
{% block title %}Create {{ component_type }} ({{ parent }}){% endblock %} {% block title %}Create {{ component_type }}{% endblock %}
{% block content %} {% block content %}
<form action="." method="post" class="form form-horizontal"> <form action="" method="post" class="form form-horizontal">
{% csrf_token %} {% csrf_token %}
<div class="row"> <div class="row">
<div class="col-md-6 col-md-offset-3"> <div class="col-md-6 col-md-offset-3">
@ -21,12 +21,6 @@
<strong>{{ component_type|title }}</strong> <strong>{{ component_type|title }}</strong>
</div> </div>
<div class="panel-body"> <div class="panel-body">
<div class="form-group">
<label class="col-md-3 control-label required">Device</label>
<div class="col-md-9">
<p class="form-control-static">{{ parent }}</p>
</div>
</div>
{% render_form form %} {% render_form form %}
</div> </div>
</div> </div>

View File

@ -1,20 +0,0 @@
{% extends '_base.html' %}
{% load buttons %}
{% load helpers %}
{% block content %}
<div class="pull-right noprint">
{% export_button content_type %}
</div>
<h1>{% block title %}{{ table.Meta.model|model_name|capfirst }}s{% endblock %}</h1>
<div class="row">
<div class="col-md-9">
{% include 'responsive_table.html' %}
{% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
</div>
<div class="col-md-3 noprint">
{% include 'inc/search_panel.html' %}
{% include 'inc/tags_panel.html' %}
</div>
</div>
{% endblock %}

View File

@ -54,7 +54,7 @@
</table> </table>
{% if perms.dcim.add_inventoryitem %} {% if perms.dcim.add_inventoryitem %}
<div class="panel-footer text-right noprint"> <div class="panel-footer text-right noprint">
<a href="{% url 'dcim:inventoryitem_add' device=device.pk %}" class="btn btn-primary btn-xs"> <a href="{% url 'dcim:inventoryitem_add' %}?device={{ device.pk }}&return_url={% url 'dcim:device_inventory' pk=device.pk %}" class="btn btn-primary btn-xs">
<span class="fa fa-plus" aria-hidden="true"></span> Add Inventory Item <span class="fa fa-plus" aria-hidden="true"></span> Add Inventory Item
</a> </a>
</div> </div>

View File

@ -1,22 +1,24 @@
{% extends '_base.html' %} {% extends 'utilities/obj_list.html' %}
{% load buttons %}
{% block content %} {% block bulk_buttons %}
<div class="pull-right noprint"> {% if perms.dcim.change_device %}
{% if perms.dcim.add_device %} <div class="btn-group">
{% add_button 'dcim:device_add' %} <button type="button" class="btn btn-sm btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{% import_button 'dcim:device_import' %} <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add Components <span class="caret"></span>
</button>
<ul class="dropdown-menu">
{% if perms.dcim.add_consoleport %}<li><a href="{% url 'dcim:device_bulk_add_consoleport' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="formaction">Console Ports</a></li>{% endif %}
{% if perms.dcim.add_consoleserverport %}<li><a href="{% url 'dcim:device_bulk_add_consoleserverport' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="formaction">Console Server Ports</a></li>{% endif %}
{% if perms.dcim.add_powerport %}<li><a href="{% url 'dcim:device_bulk_add_powerport' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="formaction">Power Ports</a></li>{% endif %}
{% if perms.dcim.add_poweroutlet %}<li><a href="{% url 'dcim:device_bulk_add_poweroutlet' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="formaction">Power Outlets</a></li>{% endif %}
{% if perms.dcim.add_interface %}<li><a href="{% url 'dcim:device_bulk_add_interface' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="formaction">Interfaces</a></li>{% endif %}
{% if perms.dcim.add_devicebay %}<li><a href="{% url 'dcim:device_bulk_add_devicebay' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="formaction">Device Bays</a></li>{% endif %}
</ul>
</div>
{% endif %}
{% if perms.dcim.add_virtualchassis %}
<button type="submit" name="_edit" formaction="{% url 'dcim:virtualchassis_add' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-primary btn-sm">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Create Virtual Chassis
</button>
{% endif %} {% endif %}
{% export_button content_type %}
</div>
<h1>{% block title %}Devices{% endblock %}</h1>
<div class="row">
<div class="col-md-9">
{% include 'dcim/inc/device_table.html' with bulk_edit_url='dcim:device_bulk_edit' bulk_delete_url='dcim:device_bulk_delete' %}
</div>
<div class="col-md-3 noprint">
{% include 'inc/search_panel.html' %}
{% include 'inc/tags_panel.html' %}
</div>
</div>
{% endblock %} {% endblock %}

View File

@ -1,18 +0,0 @@
{% extends '_base.html' %}
{% load buttons %}
{% block content %}
<div class="pull-right noprint">
{% if perms.dcim.add_devicerole %}
{% add_button 'dcim:devicerole_add' %}
{% import_button 'dcim:devicerole_import' %}
{% endif %}
{% export_button content_type %}
</div>
<h1>{% block title %}Device Roles{% endblock %}</h1>
<div class="row">
<div class="col-md-12">
{% include 'utilities/obj_table.html' with bulk_delete_url='dcim:devicerole_bulk_delete' %}
</div>
</div>
{% endblock %}

View File

@ -22,14 +22,14 @@
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add Components <span class="caret"></span> <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add Components <span class="caret"></span>
</button> </button>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
{% if perms.dcim.add_consoleporttemplate %}<li><a href="{% url 'dcim:devicetype_add_consoleport' pk=devicetype.pk %}">Console Ports</a></li>{% endif %} {% if perms.dcim.add_consoleporttemplate %}<li><a href="{% url 'dcim:consoleporttemplate_add' %}?device_type={{ devicetype.pk }}&return_url={{ devicetype.get_absolute_url }}">Console Ports</a></li>{% endif %}
{% if perms.dcim.add_consoleserverporttemplate %}<li><a href="{% url 'dcim:devicetype_add_consoleserverport' pk=devicetype.pk %}">Console Server Ports</a></li>{% endif %} {% if perms.dcim.add_consoleserverporttemplate %}<li><a href="{% url 'dcim:consoleserverporttemplate_add' %}?device_type={{ devicetype.pk }}&return_url={{ devicetype.get_absolute_url }}">Console Server Ports</a></li>{% endif %}
{% if perms.dcim.add_powerporttemplate %}<li><a href="{% url 'dcim:devicetype_add_powerport' pk=devicetype.pk %}">Power Ports</a></li>{% endif %} {% if perms.dcim.add_powerporttemplate %}<li><a href="{% url 'dcim:powerporttemplate_add' %}?device_type={{ devicetype.pk }}&return_url={{ devicetype.get_absolute_url }}">Power Ports</a></li>{% endif %}
{% if perms.dcim.add_poweroutlettemplate %}<li><a href="{% url 'dcim:devicetype_add_poweroutlet' pk=devicetype.pk %}">Power Outlets</a></li>{% endif %} {% if perms.dcim.add_poweroutlettemplate %}<li><a href="{% url 'dcim:poweroutlettemplate_add' %}?device_type={{ devicetype.pk }}&return_url={{ devicetype.get_absolute_url }}">Power Outlets</a></li>{% endif %}
{% if perms.dcim.add_interfacetemplate %}<li><a href="{% url 'dcim:devicetype_add_interface' pk=devicetype.pk %}">Interfaces</a></li>{% endif %} {% if perms.dcim.add_interfacetemplate %}<li><a href="{% url 'dcim:interfacetemplate_add' %}?device_type={{ devicetype.pk }}&return_url={{ devicetype.get_absolute_url }}">Interfaces</a></li>{% endif %}
{% if perms.dcim.add_frontporttemplate %}<li><a href="{% url 'dcim:devicetype_add_frontport' pk=devicetype.pk %}">Front Ports</a></li>{% endif %} {% if perms.dcim.add_frontporttemplate %}<li><a href="{% url 'dcim:frontporttemplate_add' %}?device_type={{ devicetype.pk }}&return_url={{ devicetype.get_absolute_url }}">Front Ports</a></li>{% endif %}
{% if perms.dcim.add_rearporttemplate %}<li><a href="{% url 'dcim:devicetype_add_rearport' pk=devicetype.pk %}">Rear Ports</a></li>{% endif %} {% if perms.dcim.add_rearporttemplate %}<li><a href="{% url 'dcim:rearporttemplate_add' %}?device_type={{ devicetype.pk }}&return_url={{ devicetype.get_absolute_url }}">Rear Ports</a></li>{% endif %}
{% if perms.dcim.add_devicebaytemplate %}<li><a href="{% url 'dcim:devicetype_add_devicebay' pk=devicetype.pk %}">Device Bays</a></li>{% endif %} {% if perms.dcim.add_devicebaytemplate %}<li><a href="{% url 'dcim:devicebaytemplate_add' %}?device_type={{ devicetype.pk }}&return_url={{ devicetype.get_absolute_url }}">Device Bays</a></li>{% endif %}
</ul> </ul>
</div> </div>
{% endif %} {% endif %}
@ -136,48 +136,48 @@
{% if devicetype.consoleport_templates.exists or devicetype.powerport_templates.exists %} {% if devicetype.consoleport_templates.exists or devicetype.powerport_templates.exists %}
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-6">
{% include 'dcim/inc/devicetype_component_table.html' with table=consoleport_table title='Console Ports' add_url='dcim:devicetype_add_consoleport' delete_url='dcim:devicetype_delete_consoleport' %} {% include 'dcim/inc/devicetype_component_table.html' with table=consoleport_table title='Console Ports' add_url='dcim:consoleporttemplate_add' edit_url='dcim:consoleporttemplate_bulk_edit' delete_url='dcim:consoleporttemplate_bulk_delete' %}
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
{% include 'dcim/inc/devicetype_component_table.html' with table=powerport_table title='Power Ports' add_url='dcim:devicetype_add_powerport' delete_url='dcim:devicetype_delete_powerport' %} {% include 'dcim/inc/devicetype_component_table.html' with table=powerport_table title='Power Ports' add_url='dcim:powerporttemplate_add' edit_url='dcim:powerporttemplate_bulk_edit' delete_url='dcim:powerporttemplate_bulk_delete' %}
</div> </div>
</div> </div>
{% endif %} {% endif %}
{% if devicetype.is_parent_device or devicebay_table.rows %} {% if devicetype.is_parent_device or devicebay_table.rows %}
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">
{% include 'dcim/inc/devicetype_component_table.html' with table=devicebay_table title='Device Bays' add_url='dcim:devicetype_add_devicebay' delete_url='dcim:devicetype_delete_devicebay' %} {% include 'dcim/inc/devicetype_component_table.html' with table=devicebay_table title='Device Bays' add_url='dcim:devicebaytemplate_add' edit_url=None delete_url='dcim:devicebaytemplate_bulk_delete' %}
</div> </div>
</div> </div>
{% endif %} {% endif %}
{% if devicetype.consoleserverport_templates.exists %} {% if devicetype.consoleserverport_templates.exists %}
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">
{% include 'dcim/inc/devicetype_component_table.html' with table=consoleserverport_table title='Console Server Ports' add_url='dcim:devicetype_add_consoleserverport' delete_url='dcim:devicetype_delete_consoleserverport' %} {% include 'dcim/inc/devicetype_component_table.html' with table=consoleserverport_table title='Console Server Ports' add_url='dcim:consoleserverporttemplate_add' edit_url='dcim:consoleserverporttemplate_bulk_edit' delete_url='dcim:consoleserverporttemplate_bulk_delete' %}
</div> </div>
</div> </div>
{% endif %} {% endif %}
{% if devicetype.poweroutlet_templates.exists %} {% if devicetype.poweroutlet_templates.exists %}
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">
{% include 'dcim/inc/devicetype_component_table.html' with table=poweroutlet_table title='Power Outlets' add_url='dcim:devicetype_add_poweroutlet' delete_url='dcim:devicetype_delete_poweroutlet' %} {% include 'dcim/inc/devicetype_component_table.html' with table=poweroutlet_table title='Power Outlets' add_url='dcim:poweroutlettemplate_add' edit_url='dcim:poweroutlettemplate_bulk_edit' delete_url='dcim:poweroutlettemplate_bulk_delete' %}
</div> </div>
</div> </div>
{% endif %} {% endif %}
{% if devicetype.interface_templates.exists %} {% if devicetype.interface_templates.exists %}
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">
{% include 'dcim/inc/devicetype_component_table.html' with table=interface_table title='Interfaces' add_url='dcim:devicetype_add_interface' delete_url='dcim:devicetype_delete_interface' %} {% include 'dcim/inc/devicetype_component_table.html' with table=interface_table title='Interfaces' add_url='dcim:interfacetemplate_add' edit_url='dcim:interfacetemplate_bulk_edit' delete_url='dcim:interfacetemplate_bulk_delete' %}
</div> </div>
</div> </div>
{% endif %} {% endif %}
{% if devicetype.frontport_templates.exists or devicetype.rearport_templates.exists %} {% if devicetype.frontport_templates.exists or devicetype.rearport_templates.exists %}
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-6">
{% include 'dcim/inc/devicetype_component_table.html' with table=front_port_table title='Front Ports' add_url='dcim:devicetype_add_frontport' delete_url='dcim:devicetype_delete_frontport' %} {% include 'dcim/inc/devicetype_component_table.html' with table=front_port_table title='Front Ports' add_url='dcim:frontporttemplate_add' edit_url='dcim:frontporttemplate_bulk_edit' delete_url='dcim:frontporttemplate_bulk_delete' %}
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
{% include 'dcim/inc/devicetype_component_table.html' with table=rear_port_table title='Rear Ports' add_url='dcim:devicetype_add_rearport' delete_url='dcim:devicetype_delete_rearport' %} {% include 'dcim/inc/devicetype_component_table.html' with table=rear_port_table title='Rear Ports' add_url='dcim:rearporttemplate_add' edit_url='dcim:rearporttemplate_bulk_edit' delete_url='dcim:rearporttemplate_bulk_delete' %}
</div> </div>
</div> </div>
{% endif %} {% endif %}

View File

@ -1,22 +0,0 @@
{% extends '_base.html' %}
{% load buttons %}
{% block content %}
<div class="pull-right noprint">
{% if perms.dcim.add_devicetype %}
{% add_button 'dcim:devicetype_add' %}
{% import_button 'dcim:devicetype_import' %}
{% endif %}
{% export_button content_type %}
</div>
<h1>{% block title %}Device Types{% endblock %}</h1>
<div class="row">
<div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_edit_url='dcim:devicetype_bulk_edit' bulk_delete_url='dcim:devicetype_bulk_delete' %}
</div>
<div class="col-md-3 noprint">
{% include 'inc/search_panel.html' %}
{% include 'inc/tags_panel.html' %}
</div>
</div>
{% endblock %}

View File

@ -1,24 +0,0 @@
{% extends 'utilities/obj_table.html' %}
{% block extra_actions %}
{% if perms.dcim.change_device %}
<div class="btn-group">
<button type="button" class="btn btn-sm btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add Components <span class="caret"></span>
</button>
<ul class="dropdown-menu">
{% if perms.dcim.add_consoleport %}<li><a href="{% url 'dcim:device_bulk_add_consoleport' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="formaction">Console Ports</a></li>{% endif %}
{% if perms.dcim.add_consoleserverport %}<li><a href="{% url 'dcim:device_bulk_add_consoleserverport' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="formaction">Console Server Ports</a></li>{% endif %}
{% if perms.dcim.add_powerport %}<li><a href="{% url 'dcim:device_bulk_add_powerport' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="formaction">Power Ports</a></li>{% endif %}
{% if perms.dcim.add_poweroutlet %}<li><a href="{% url 'dcim:device_bulk_add_poweroutlet' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="formaction">Power Outlets</a></li>{% endif %}
{% if perms.dcim.add_interface %}<li><a href="{% url 'dcim:device_bulk_add_interface' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="formaction">Interfaces</a></li>{% endif %}
{% if perms.dcim.add_devicebay %}<li><a href="{% url 'dcim:device_bulk_add_devicebay' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="formaction">Device Bays</a></li>{% endif %}
</ul>
</div>
{% endif %}
{% if perms.dcim.add_virtualchassis %}
<button type="submit" name="_edit" formaction="{% url 'dcim:virtualchassis_add' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-primary btn-sm">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Create Virtual Chassis
</button>
{% endif %}
{% endblock %}

View File

@ -9,18 +9,18 @@
<div class="panel-footer noprint"> <div class="panel-footer noprint">
{% if table.rows %} {% if table.rows %}
{% if edit_url %} {% if edit_url %}
<button type="submit" name="_edit" formaction="{% url edit_url pk=devicetype.pk %}?return_url={{ devicetype.get_absolute_url }}" class="btn btn-xs btn-warning"> <button type="submit" name="_edit" formaction="{% url edit_url %}?return_url={{ devicetype.get_absolute_url }}" class="btn btn-xs btn-warning">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit Selected <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit Selected
</button> </button>
{% endif %} {% endif %}
{% if delete_url %} {% if delete_url %}
<button type="submit" name="_delete" formaction="{% url delete_url pk=devicetype.pk %}?return_url={{ devicetype.get_absolute_url }}" class="btn btn-xs btn-danger"> <button type="submit" name="_delete" formaction="{% url delete_url %}?return_url={{ devicetype.get_absolute_url }}" class="btn btn-xs btn-danger">
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete Selected <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete Selected
</button> </button>
{% endif %} {% endif %}
{% endif %} {% endif %}
<div class="pull-right"> <div class="pull-right">
<a href="{% url add_url pk=devicetype.pk %}{{ add_url_extra }}" class="btn btn-primary btn-xs"> <a href="{% url add_url %}?device_type={{ devicetype.pk }}&return_url={{ devicetype.get_absolute_url }}" class="btn btn-primary btn-xs">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> <span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
Add {{ title }} Add {{ title }}
</a> </a>

View File

@ -1,11 +1,5 @@
{% load helpers %} {% load helpers %}
<ul class="rack_legend">
{% for u in rack.units %}
<li>{{ u }}</li>
{% endfor %}
</ul>
<div class="rack_frame"> <div class="rack_frame">
<object data="{% url 'dcim-api:rack-elevation' pk=rack.pk %}?face={{face}}&render=svg"></object> <object data="{% url 'dcim-api:rack-elevation' pk=rack.pk %}?face={{face}}&render=svg"></object>

View File

@ -1,21 +0,0 @@
{% extends '_base.html' %}
{% load buttons %}
{% load helpers %}
{% block content %}
<div class="pull-right noprint">
{% if perms.dcim.add_devicetype %}
{% import_button 'dcim:inventoryitem_import' %}
{% endif %}
{% export_button content_type %}
</div>
<h1>{% block title %}Inventory Items{% endblock %}</h1>
<div class="row">
<div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_edit_url='dcim:inventoryitem_bulk_edit' bulk_delete_url='dcim:inventoryitem_bulk_delete' %}
</div>
<div class="col-md-3 noprint">
{% include 'inc/search_panel.html' %}
</div>
</div>
{% endblock %}

View File

@ -1,18 +0,0 @@
{% extends '_base.html' %}
{% load buttons %}
{% block content %}
<div class="pull-right noprint">
{% if perms.dcim.add_manufacturer %}
{% add_button 'dcim:manufacturer_add' %}
{% import_button 'dcim:manufacturer_import' %}
{% endif %}
{% export_button content_type %}
</div>
<h1>{% block title %}Manufacturers{% endblock %}</h1>
<div class="row">
<div class="col-md-12">
{% include 'utilities/obj_table.html' with bulk_delete_url='dcim:manufacturer_bulk_delete' %}
</div>
</div>
{% endblock %}

View File

@ -1,18 +0,0 @@
{% extends '_base.html' %}
{% load buttons %}
{% block content %}
<div class="pull-right noprint">
{% if perms.dcim.add_platform %}
{% add_button 'dcim:platform_add' %}
{% import_button 'dcim:platform_import' %}
{% endif %}
{% export_button content_type %}
</div>
<h1>{% block title %}Platforms{% endblock %}</h1>
<div class="row">
<div class="col-md-12">
{% include 'utilities/obj_table.html' with bulk_delete_url='dcim:platform_bulk_delete' %}
</div>
</div>
{% endblock %}

View File

@ -1,22 +0,0 @@
{% extends '_base.html' %}
{% load buttons %}
{% block content %}
<div class="pull-right noprint">
{% if perms.dcim.add_powerfeed %}
{% add_button 'dcim:powerfeed_add' %}
{% import_button 'dcim:powerfeed_import' %}
{% endif %}
{% export_button content_type %}
</div>
<h1>{% block title %}Power Feeds{% endblock %}</h1>
<div class="row">
<div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_edit_url='dcim:powerfeed_bulk_edit' bulk_delete_url='dcim:powerfeed_bulk_delete' %}
</div>
<div class="col-md-3 noprint">
{% include 'inc/search_panel.html' %}
{% include 'inc/tags_panel.html' %}
</div>
</div>
{% endblock %}

View File

@ -1,21 +0,0 @@
{% extends '_base.html' %}
{% load buttons %}
{% block content %}
<div class="pull-right noprint">
{% if perms.dcim.add_powerpanel %}
{% add_button 'dcim:powerpanel_add' %}
{% import_button 'dcim:powerpanel_import' %}
{% endif %}
{% export_button content_type %}
</div>
<h1>{% block title %}Power Panels{% endblock %}</h1>
<div class="row">
<div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_delete_url='dcim:powerpanel_bulk_delete' %}
</div>
<div class="col-md-3">
{% include 'inc/search_panel.html' %}
</div>
</div>
{% endblock %}

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