mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-16 12:12:53 -06:00
Merge branch 'develop' into 3995-navbar-overflow
This commit is contained in:
commit
6ac8d41323
8
.github/ISSUE_TEMPLATE/bug_report.md
vendored
8
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -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
|
||||||
|
10
.github/ISSUE_TEMPLATE/documentation_change.md
vendored
10
.github/ISSUE_TEMPLATE/documentation_change.md
vendored
@ -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
|
||||||
|
8
.github/ISSUE_TEMPLATE/feature_request.md
vendored
8
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@ -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
|
||||||
|
9
.github/ISSUE_TEMPLATE/housekeeping.md
vendored
9
.github/ISSUE_TEMPLATE/housekeeping.md
vendored
@ -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
|
||||||
|
|
||||||
|
@ -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:
|
||||||
@ -127,7 +135,7 @@ EXEMPT_VIEW_PERMISSIONS = ['*']
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# ENFORCE_GLOBAL_UNIQUE
|
## ENFORCE_GLOBAL_UNIQUE
|
||||||
|
|
||||||
Default: False
|
Default: False
|
||||||
|
|
||||||
|
@ -29,7 +29,7 @@ server {
|
|||||||
|
|
||||||
location / {
|
location / {
|
||||||
proxy_pass http://127.0.0.1:8001;
|
proxy_pass http://127.0.0.1:8001;
|
||||||
proxy_set_header X-Forwarded-Host $server_name;
|
proxy_set_header X-Forwarded-Host $http_host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
}
|
}
|
||||||
@ -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
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -110,8 +110,8 @@ AUTH_LDAP_USER_FLAGS_BY_GROUP = {
|
|||||||
AUTH_LDAP_FIND_GROUP_PERMS = True
|
AUTH_LDAP_FIND_GROUP_PERMS = True
|
||||||
|
|
||||||
# Cache groups for one hour to reduce LDAP traffic
|
# Cache groups for one hour to reduce LDAP traffic
|
||||||
AUTH_LDAP_CACHE_GROUPS = True
|
AUTH_LDAP_CACHE_TIMEOUT = 3600
|
||||||
AUTH_LDAP_GROUP_CACHE_TIMEOUT = 3600
|
|
||||||
```
|
```
|
||||||
|
|
||||||
* `is_active` - All users must be mapped to at least this group to enable authentication. Without this, users cannot log in.
|
* `is_active` - All users must be mapped to at least this group to enable authentication. Without this, users cannot log in.
|
||||||
|
@ -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.
|
||||||
|
@ -1,15 +1,67 @@
|
|||||||
# v2.7.3 (FUTURE)
|
# v2.7.5 (FUTURE)
|
||||||
|
|
||||||
|
## Enhancements
|
||||||
|
|
||||||
|
* [#3995](https://github.com/netbox-community/netbox/issues/3995) - Make dropdown menus in the navigation bar scrollable
|
||||||
|
* [#4113](https://github.com/netbox-community/netbox/issues/4113) - Add bulk edit functionality for device type components
|
||||||
|
|
||||||
|
## Bug Fixes
|
||||||
|
|
||||||
|
* [#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
|
||||||
|
|
||||||
|
# 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
|
||||||
|
* [#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
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# v2.7.3 (2020-01-28)
|
||||||
|
|
||||||
## Enhancements
|
## Enhancements
|
||||||
|
|
||||||
* [#3310](https://github.com/netbox-community/netbox/issues/3310) - Pre-select site/rack for B side when creating a new cable
|
* [#3310](https://github.com/netbox-community/netbox/issues/3310) - Pre-select site/rack for B side when creating a new cable
|
||||||
|
* [#3338](https://github.com/netbox-community/netbox/issues/3338) - Include circuit terminations in API representation of circuits
|
||||||
* [#3509](https://github.com/netbox-community/netbox/issues/3509) - Add IP address variables for custom scripts
|
* [#3509](https://github.com/netbox-community/netbox/issues/3509) - Add IP address variables for custom scripts
|
||||||
|
* [#3978](https://github.com/netbox-community/netbox/issues/3978) - Add VRF filtering to search NAT IP
|
||||||
|
* [#4005](https://github.com/netbox-community/netbox/issues/4005) - Include timezone context in webhook timestamps
|
||||||
|
|
||||||
## Bug Fixes
|
## Bug Fixes
|
||||||
|
|
||||||
|
* [#3950](https://github.com/netbox-community/netbox/issues/3950) - Automatically select parent manufacturer when specifying initial device type during device creation
|
||||||
|
* [#3982](https://github.com/netbox-community/netbox/issues/3982) - Restore tooltip for reservations on rack elevations
|
||||||
* [#3983](https://github.com/netbox-community/netbox/issues/3983) - Permit the creation of multiple unnamed devices
|
* [#3983](https://github.com/netbox-community/netbox/issues/3983) - Permit the creation of multiple unnamed devices
|
||||||
* [#3989](https://github.com/netbox-community/netbox/issues/3989) - Correct HTTP content type assignment for webhooks
|
* [#3989](https://github.com/netbox-community/netbox/issues/3989) - Correct HTTP content type assignment for webhooks
|
||||||
* [#3995](https://github.com/netbox-community/netbox/issues/3995) - Fixed overflowing dropdown menus becoming unreachable
|
* [#3999](https://github.com/netbox-community/netbox/issues/3999) - Do not filter child results by null if non-required parent fields are blank
|
||||||
|
* [#4008](https://github.com/netbox-community/netbox/issues/4008) - Toggle rack elevation face using front/rear strings
|
||||||
|
* [#4017](https://github.com/netbox-community/netbox/issues/4017) - Remove redundant tenant field from cluster form
|
||||||
|
* [#4019](https://github.com/netbox-community/netbox/issues/4019) - Restore border around background devices in rack elevations
|
||||||
|
* [#4022](https://github.com/netbox-community/netbox/issues/4022) - Fix display of assigned IPs when filtering device interfaces
|
||||||
|
* [#4025](https://github.com/netbox-community/netbox/issues/4025) - Correct display of cable status (various places)
|
||||||
|
* [#4027](https://github.com/netbox-community/netbox/issues/4027) - Repair schema migration for #3569 to convert IP addresses with DHCP status
|
||||||
|
* [#4028](https://github.com/netbox-community/netbox/issues/4028) - Correct URL patterns to match Unicode characters in tag slugs
|
||||||
|
* [#4030](https://github.com/netbox-community/netbox/issues/4030) - Fix exception when setting interfaces to tagged mode in bulk
|
||||||
|
* [#4033](https://github.com/netbox-community/netbox/issues/4033) - Restore missing comments field label of various bulk edit forms
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -3,11 +3,11 @@ from taggit_serializer.serializers import TaggitSerializer, TagListSerializerFie
|
|||||||
|
|
||||||
from circuits.choices import CircuitStatusChoices
|
from circuits.choices import CircuitStatusChoices
|
||||||
from circuits.models import Provider, Circuit, CircuitTermination, CircuitType
|
from circuits.models import Provider, Circuit, CircuitTermination, CircuitType
|
||||||
from dcim.api.nested_serializers import NestedCableSerializer, NestedSiteSerializer
|
from dcim.api.nested_serializers import NestedCableSerializer, NestedInterfaceSerializer, NestedSiteSerializer
|
||||||
from dcim.api.serializers import ConnectedEndpointSerializer
|
from dcim.api.serializers import ConnectedEndpointSerializer
|
||||||
from extras.api.customfields import CustomFieldModelSerializer
|
from extras.api.customfields import CustomFieldModelSerializer
|
||||||
from tenancy.api.nested_serializers import NestedTenantSerializer
|
from tenancy.api.nested_serializers import NestedTenantSerializer
|
||||||
from utilities.api import ChoiceField, ValidatedModelSerializer
|
from utilities.api import ChoiceField, ValidatedModelSerializer, WritableNestedSerializer
|
||||||
from .nested_serializers import *
|
from .nested_serializers import *
|
||||||
|
|
||||||
|
|
||||||
@ -39,18 +39,30 @@ class CircuitTypeSerializer(ValidatedModelSerializer):
|
|||||||
fields = ['id', 'name', 'slug', 'description', 'circuit_count']
|
fields = ['id', 'name', 'slug', 'description', 'circuit_count']
|
||||||
|
|
||||||
|
|
||||||
|
class CircuitCircuitTerminationSerializer(WritableNestedSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail')
|
||||||
|
site = NestedSiteSerializer()
|
||||||
|
connected_endpoint = NestedInterfaceSerializer()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = CircuitTermination
|
||||||
|
fields = ['id', 'url', 'site', 'connected_endpoint', 'port_speed', 'upstream_speed', 'xconnect_id']
|
||||||
|
|
||||||
|
|
||||||
class CircuitSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
class CircuitSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||||
provider = NestedProviderSerializer()
|
provider = NestedProviderSerializer()
|
||||||
status = ChoiceField(choices=CircuitStatusChoices, required=False)
|
status = ChoiceField(choices=CircuitStatusChoices, required=False)
|
||||||
type = NestedCircuitTypeSerializer()
|
type = NestedCircuitTypeSerializer()
|
||||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||||
|
termination_a = CircuitCircuitTerminationSerializer(read_only=True)
|
||||||
|
termination_z = CircuitCircuitTerminationSerializer(read_only=True)
|
||||||
tags = TagListSerializerField(required=False)
|
tags = TagListSerializerField(required=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Circuit
|
model = Circuit
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description',
|
'id', 'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description',
|
||||||
'comments', 'tags', 'custom_fields', 'created', 'last_updated',
|
'termination_a', 'termination_z', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -62,7 +62,9 @@ class CircuitTypeViewSet(ModelViewSet):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class CircuitViewSet(CustomFieldModelViewSet):
|
class CircuitViewSet(CustomFieldModelViewSet):
|
||||||
queryset = Circuit.objects.prefetch_related('type', 'tenant', 'provider').prefetch_related('tags')
|
queryset = Circuit.objects.prefetch_related(
|
||||||
|
'type', 'tenant', 'provider', 'terminations__site', 'terminations__connected_endpoint__device'
|
||||||
|
).prefetch_related('tags')
|
||||||
serializer_class = serializers.CircuitSerializer
|
serializer_class = serializers.CircuitSerializer
|
||||||
filterset_class = filters.CircuitFilterSet
|
filterset_class = filters.CircuitFilterSet
|
||||||
|
|
||||||
|
@ -2,12 +2,14 @@ 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
|
FilterChoiceField, 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 +19,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 +48,7 @@ class ProviderForm(BootstrapMixin, CustomFieldForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class ProviderCSVForm(forms.ModelForm):
|
class ProviderCSVForm(CustomFieldModelCSVForm):
|
||||||
slug = SlugField()
|
slug = SlugField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -89,7 +91,8 @@ class ProviderBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEdi
|
|||||||
label='Admin contact'
|
label='Admin contact'
|
||||||
)
|
)
|
||||||
comments = CommentField(
|
comments = CommentField(
|
||||||
widget=SmallTextarea()
|
widget=SmallTextarea,
|
||||||
|
label='Comments'
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -128,6 +131,7 @@ class ProviderFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
|||||||
required=False,
|
required=False,
|
||||||
label='ASN'
|
label='ASN'
|
||||||
)
|
)
|
||||||
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -159,7 +163,7 @@ class CircuitTypeCSVForm(forms.ModelForm):
|
|||||||
# Circuits
|
# Circuits
|
||||||
#
|
#
|
||||||
|
|
||||||
class CircuitForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
class CircuitForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||||
comments = CommentField()
|
comments = CommentField()
|
||||||
tags = TagField(
|
tags = TagField(
|
||||||
required=False
|
required=False
|
||||||
@ -187,7 +191,7 @@ class CircuitForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
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',
|
||||||
@ -332,6 +336,7 @@ class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm
|
|||||||
min_value=0,
|
min_value=0,
|
||||||
label='Commit rate (Kbps)'
|
label='Commit rate (Kbps)'
|
||||||
)
|
)
|
||||||
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
@ -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 StandardTestCases
|
||||||
|
|
||||||
|
|
||||||
class ProviderTestCase(TestCase):
|
class ProviderTestCase(StandardTestCases.Views):
|
||||||
|
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,45 @@ 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(StandardTestCases.Views):
|
||||||
|
model = CircuitType
|
||||||
|
|
||||||
def setUp(self):
|
# Disable inapplicable tests
|
||||||
user = create_test_user(
|
test_get_object = None
|
||||||
permissions=[
|
test_delete_object = None
|
||||||
'circuits.view_circuittype',
|
test_bulk_edit_objects = None
|
||||||
'circuits.add_circuittype',
|
|
||||||
]
|
@classmethod
|
||||||
)
|
def setUpTestData(cls):
|
||||||
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 +63,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(StandardTestCases.Views):
|
||||||
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)
|
|
||||||
|
@ -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}),
|
||||||
|
|
||||||
]
|
]
|
||||||
|
@ -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
|
||||||
|
@ -4,17 +4,30 @@ from .choices import InterfaceTypeChoices
|
|||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Rack elevation rendering
|
# Racks
|
||||||
#
|
#
|
||||||
|
|
||||||
|
RACK_U_HEIGHT_DEFAULT = 42
|
||||||
|
|
||||||
RACK_ELEVATION_UNIT_WIDTH_DEFAULT = 230
|
RACK_ELEVATION_UNIT_WIDTH_DEFAULT = 230
|
||||||
RACK_ELEVATION_UNIT_HEIGHT_DEFAULT = 20
|
RACK_ELEVATION_UNIT_HEIGHT_DEFAULT = 20
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Interface type groups
|
# RearPorts
|
||||||
#
|
#
|
||||||
|
|
||||||
|
REARPORT_POSITIONS_MIN = 1
|
||||||
|
REARPORT_POSITIONS_MAX = 64
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Interfaces
|
||||||
|
#
|
||||||
|
|
||||||
|
INTERFACE_MTU_MIN = 1
|
||||||
|
INTERFACE_MTU_MAX = 32767 # Max value of a signed 16-bit integer
|
||||||
|
|
||||||
VIRTUAL_IFACE_TYPES = [
|
VIRTUAL_IFACE_TYPES = [
|
||||||
InterfaceTypeChoices.TYPE_VIRTUAL,
|
InterfaceTypeChoices.TYPE_VIRTUAL,
|
||||||
InterfaceTypeChoices.TYPE_LAG,
|
InterfaceTypeChoices.TYPE_LAG,
|
||||||
@ -31,6 +44,17 @@ WIRELESS_IFACE_TYPES = [
|
|||||||
NONCONNECTABLE_IFACE_TYPES = VIRTUAL_IFACE_TYPES + WIRELESS_IFACE_TYPES
|
NONCONNECTABLE_IFACE_TYPES = VIRTUAL_IFACE_TYPES + WIRELESS_IFACE_TYPES
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# PowerFeeds
|
||||||
|
#
|
||||||
|
|
||||||
|
POWERFEED_VOLTAGE_DEFAULT = 120
|
||||||
|
|
||||||
|
POWERFEED_AMPERAGE_DEFAULT = 20
|
||||||
|
|
||||||
|
POWERFEED_MAX_UTILIZATION_DEFAULT = 80 # Percentage
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Cabling and connections
|
# Cabling and connections
|
||||||
#
|
#
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -5,7 +5,6 @@ from django.contrib.auth.models import User
|
|||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.contrib.postgres.forms.array import SimpleArrayField
|
from django.contrib.postgres.forms.array import SimpleArrayField
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from django.db.models import Q
|
|
||||||
from mptt.forms import TreeNodeChoiceField
|
from mptt.forms import TreeNodeChoiceField
|
||||||
from netaddr import EUI
|
from netaddr import EUI
|
||||||
from netaddr.core import AddrFormatError
|
from netaddr.core import AddrFormatError
|
||||||
@ -14,7 +13,8 @@ from timezone_field import TimeZoneFormField
|
|||||||
|
|
||||||
from circuits.models import Circuit, Provider
|
from circuits.models import Circuit, Provider
|
||||||
from extras.forms import (
|
from extras.forms import (
|
||||||
AddRemoveTagsForm, CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm, LocalConfigContextFilterForm
|
AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldModelCSVForm, CustomFieldFilterForm, CustomFieldModelForm,
|
||||||
|
LocalConfigContextFilterForm,
|
||||||
)
|
)
|
||||||
from ipam.constants import BGP_ASN_MAX, BGP_ASN_MIN
|
from ipam.constants import BGP_ASN_MAX, BGP_ASN_MIN
|
||||||
from ipam.models import IPAddress, VLAN
|
from ipam.models import IPAddress, VLAN
|
||||||
@ -22,9 +22,9 @@ from tenancy.forms import TenancyFilterForm, TenancyForm
|
|||||||
from tenancy.models import Tenant, TenantGroup
|
from tenancy.models import Tenant, TenantGroup
|
||||||
from utilities.forms import (
|
from utilities.forms import (
|
||||||
APISelect, APISelectMultiple, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm,
|
APISelect, APISelectMultiple, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm,
|
||||||
BulkEditNullBooleanSelect, ChainedFieldsMixin, ChainedModelChoiceField, ColorSelect, CommentField, ComponentForm,
|
BulkEditNullBooleanSelect, ChainedFieldsMixin, ChainedModelChoiceField, ColorSelect, CommentField, ConfirmationForm,
|
||||||
ConfirmationForm, CSVChoiceField, ExpandableNameField, FilterChoiceField, FlexibleModelChoiceField, JSONField,
|
CSVChoiceField, ExpandableNameField, FilterChoiceField, FlexibleModelChoiceField, JSONField, SelectWithPK,
|
||||||
SelectWithPK, SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple, BOOLEAN_WITH_BLANK_CHOICES,
|
SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES,
|
||||||
)
|
)
|
||||||
from virtualization.models import Cluster, ClusterGroup, VirtualMachine
|
from virtualization.models import Cluster, ClusterGroup, VirtualMachine
|
||||||
from .choices import *
|
from .choices import *
|
||||||
@ -41,7 +41,7 @@ DEVICE_BY_PK_RE = r'{\d+\}'
|
|||||||
INTERFACE_MODE_HELP_TEXT = """
|
INTERFACE_MODE_HELP_TEXT = """
|
||||||
Access: One untagged VLAN<br />
|
Access: One untagged VLAN<br />
|
||||||
Tagged: One untagged VLAN and/or one or more tagged VLANs<br />
|
Tagged: One untagged VLAN and/or one or more tagged VLANs<br />
|
||||||
Tagged All: Implies all VLANs are available (w/optional untagged VLAN)
|
Tagged (All): Implies all VLANs are available (w/optional untagged VLAN)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
@ -216,7 +216,7 @@ class RegionFilterForm(BootstrapMixin, forms.Form):
|
|||||||
# Sites
|
# Sites
|
||||||
#
|
#
|
||||||
|
|
||||||
class SiteForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
class SiteForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||||
region = TreeNodeChoiceField(
|
region = TreeNodeChoiceField(
|
||||||
queryset=Region.objects.all(),
|
queryset=Region.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
@ -264,7 +264,7 @@ class SiteForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class SiteCSVForm(forms.ModelForm):
|
class SiteCSVForm(CustomFieldModelCSVForm):
|
||||||
status = CSVChoiceField(
|
status = CSVChoiceField(
|
||||||
choices=SiteStatusChoices,
|
choices=SiteStatusChoices,
|
||||||
required=False,
|
required=False,
|
||||||
@ -367,6 +367,7 @@ class SiteFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
|
|||||||
value_field="slug",
|
value_field="slug",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -460,7 +461,7 @@ class RackRoleCSVForm(forms.ModelForm):
|
|||||||
# Racks
|
# Racks
|
||||||
#
|
#
|
||||||
|
|
||||||
class RackForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
class RackForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||||
group = ChainedModelChoiceField(
|
group = ChainedModelChoiceField(
|
||||||
queryset=RackGroup.objects.all(),
|
queryset=RackGroup.objects.all(),
|
||||||
chains=(
|
chains=(
|
||||||
@ -505,7 +506,7 @@ class RackForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class RackCSVForm(forms.ModelForm):
|
class RackCSVForm(CustomFieldModelCSVForm):
|
||||||
site = forms.ModelChoiceField(
|
site = forms.ModelChoiceField(
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
to_field_name='name',
|
to_field_name='name',
|
||||||
@ -677,7 +678,8 @@ class RackBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor
|
|||||||
widget=StaticSelect2()
|
widget=StaticSelect2()
|
||||||
)
|
)
|
||||||
comments = CommentField(
|
comments = CommentField(
|
||||||
widget=SmallTextarea
|
widget=SmallTextarea,
|
||||||
|
label='Comments'
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -742,6 +744,7 @@ class RackFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
|
|||||||
null_option=True,
|
null_option=True,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -897,7 +900,7 @@ class ManufacturerCSVForm(forms.ModelForm):
|
|||||||
# Device types
|
# Device types
|
||||||
#
|
#
|
||||||
|
|
||||||
class DeviceTypeForm(BootstrapMixin, CustomFieldForm):
|
class DeviceTypeForm(BootstrapMixin, CustomFieldModelForm):
|
||||||
slug = SlugField(
|
slug = SlugField(
|
||||||
slug_source='model'
|
slug_source='model'
|
||||||
)
|
)
|
||||||
@ -1020,6 +1023,7 @@ class DeviceTypeFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
|||||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -1038,16 +1042,37 @@ class ConsolePortTemplateForm(BootstrapMixin, forms.ModelForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class ConsolePortTemplateCreateForm(ComponentForm):
|
class ConsolePortTemplateCreateForm(BootstrapMixin, forms.Form):
|
||||||
|
device_type = forms.ModelChoiceField(
|
||||||
|
queryset=DeviceType.objects.all(),
|
||||||
|
widget=APISelect(
|
||||||
|
api_url='/api/dcim/device-types/'
|
||||||
|
)
|
||||||
|
)
|
||||||
name_pattern = ExpandableNameField(
|
name_pattern = ExpandableNameField(
|
||||||
label='Name'
|
label='Name'
|
||||||
)
|
)
|
||||||
type = forms.ChoiceField(
|
type = forms.ChoiceField(
|
||||||
choices=ConsolePortTypeChoices,
|
choices=add_blank_choice(ConsolePortTypeChoices),
|
||||||
widget=StaticSelect2()
|
widget=StaticSelect2()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ConsolePortTemplateBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||||
|
pk = forms.ModelMultipleChoiceField(
|
||||||
|
queryset=ConsolePortTemplate.objects.all(),
|
||||||
|
widget=forms.MultipleHiddenInput()
|
||||||
|
)
|
||||||
|
type = forms.ChoiceField(
|
||||||
|
choices=add_blank_choice(ConsolePortTypeChoices),
|
||||||
|
required=False,
|
||||||
|
widget=StaticSelect2()
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
nullable_fields = ('type',)
|
||||||
|
|
||||||
|
|
||||||
class ConsoleServerPortTemplateForm(BootstrapMixin, forms.ModelForm):
|
class ConsoleServerPortTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -1060,7 +1085,13 @@ class ConsoleServerPortTemplateForm(BootstrapMixin, forms.ModelForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class ConsoleServerPortTemplateCreateForm(ComponentForm):
|
class ConsoleServerPortTemplateCreateForm(BootstrapMixin, forms.Form):
|
||||||
|
device_type = forms.ModelChoiceField(
|
||||||
|
queryset=DeviceType.objects.all(),
|
||||||
|
widget=APISelect(
|
||||||
|
api_url='/api/dcim/device-types/'
|
||||||
|
)
|
||||||
|
)
|
||||||
name_pattern = ExpandableNameField(
|
name_pattern = ExpandableNameField(
|
||||||
label='Name'
|
label='Name'
|
||||||
)
|
)
|
||||||
@ -1070,6 +1101,21 @@ class ConsoleServerPortTemplateCreateForm(ComponentForm):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ConsoleServerPortTemplateBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||||
|
pk = forms.ModelMultipleChoiceField(
|
||||||
|
queryset=ConsoleServerPortTemplate.objects.all(),
|
||||||
|
widget=forms.MultipleHiddenInput()
|
||||||
|
)
|
||||||
|
type = forms.ChoiceField(
|
||||||
|
choices=add_blank_choice(ConsolePortTypeChoices),
|
||||||
|
required=False,
|
||||||
|
widget=StaticSelect2()
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
nullable_fields = ('type',)
|
||||||
|
|
||||||
|
|
||||||
class PowerPortTemplateForm(BootstrapMixin, forms.ModelForm):
|
class PowerPortTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -1082,7 +1128,13 @@ class PowerPortTemplateForm(BootstrapMixin, forms.ModelForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class PowerPortTemplateCreateForm(ComponentForm):
|
class PowerPortTemplateCreateForm(BootstrapMixin, forms.Form):
|
||||||
|
device_type = forms.ModelChoiceField(
|
||||||
|
queryset=DeviceType.objects.all(),
|
||||||
|
widget=APISelect(
|
||||||
|
api_url='/api/dcim/device-types/'
|
||||||
|
)
|
||||||
|
)
|
||||||
name_pattern = ExpandableNameField(
|
name_pattern = ExpandableNameField(
|
||||||
label='Name'
|
label='Name'
|
||||||
)
|
)
|
||||||
@ -1102,6 +1154,31 @@ class PowerPortTemplateCreateForm(ComponentForm):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PowerPortTemplateBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||||
|
pk = forms.ModelMultipleChoiceField(
|
||||||
|
queryset=PowerPortTemplate.objects.all(),
|
||||||
|
widget=forms.MultipleHiddenInput()
|
||||||
|
)
|
||||||
|
type = forms.ChoiceField(
|
||||||
|
choices=add_blank_choice(PowerPortTypeChoices),
|
||||||
|
required=False,
|
||||||
|
widget=StaticSelect2()
|
||||||
|
)
|
||||||
|
maximum_draw = forms.IntegerField(
|
||||||
|
min_value=1,
|
||||||
|
required=False,
|
||||||
|
help_text="Maximum power draw (watts)"
|
||||||
|
)
|
||||||
|
allocated_draw = forms.IntegerField(
|
||||||
|
min_value=1,
|
||||||
|
required=False,
|
||||||
|
help_text="Allocated power draw (watts)"
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
nullable_fields = ('type', 'maximum_draw', 'allocated_draw')
|
||||||
|
|
||||||
|
|
||||||
class PowerOutletTemplateForm(BootstrapMixin, forms.ModelForm):
|
class PowerOutletTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -1124,7 +1201,13 @@ class PowerOutletTemplateForm(BootstrapMixin, forms.ModelForm):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class PowerOutletTemplateCreateForm(ComponentForm):
|
class PowerOutletTemplateCreateForm(BootstrapMixin, forms.Form):
|
||||||
|
device_type = forms.ModelChoiceField(
|
||||||
|
queryset=DeviceType.objects.all(),
|
||||||
|
widget=APISelect(
|
||||||
|
api_url='/api/dcim/device-types/'
|
||||||
|
)
|
||||||
|
)
|
||||||
name_pattern = ExpandableNameField(
|
name_pattern = ExpandableNameField(
|
||||||
label='Name'
|
label='Name'
|
||||||
)
|
)
|
||||||
@ -1143,13 +1226,35 @@ class PowerOutletTemplateCreateForm(ComponentForm):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
# Limit power_port choices to current DeviceType
|
# Limit power_port choices to current DeviceType
|
||||||
self.fields['power_port'].queryset = PowerPortTemplate.objects.filter(
|
device_type = DeviceType.objects.get(
|
||||||
device_type=self.parent
|
pk=self.initial.get('device_type') or self.data.get('device_type')
|
||||||
)
|
)
|
||||||
|
self.fields['power_port'].queryset = PowerPortTemplate.objects.filter(
|
||||||
|
device_type=device_type
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PowerOutletTemplateBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||||
|
pk = forms.ModelMultipleChoiceField(
|
||||||
|
queryset=PowerOutletTemplate.objects.all(),
|
||||||
|
widget=forms.MultipleHiddenInput()
|
||||||
|
)
|
||||||
|
type = forms.ChoiceField(
|
||||||
|
choices=add_blank_choice(PowerOutletTypeChoices),
|
||||||
|
required=False,
|
||||||
|
widget=StaticSelect2()
|
||||||
|
)
|
||||||
|
feed_leg = forms.ChoiceField(
|
||||||
|
choices=add_blank_choice(PowerOutletFeedLegChoices),
|
||||||
|
required=False,
|
||||||
|
widget=StaticSelect2()
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
nullable_fields = ('type', 'feed_leg')
|
||||||
|
|
||||||
|
|
||||||
class InterfaceTemplateForm(BootstrapMixin, forms.ModelForm):
|
class InterfaceTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||||
@ -1165,7 +1270,13 @@ class InterfaceTemplateForm(BootstrapMixin, forms.ModelForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class InterfaceTemplateCreateForm(ComponentForm):
|
class InterfaceTemplateCreateForm(BootstrapMixin, forms.Form):
|
||||||
|
device_type = forms.ModelChoiceField(
|
||||||
|
queryset=DeviceType.objects.all(),
|
||||||
|
widget=APISelect(
|
||||||
|
api_url='/api/dcim/device-types/'
|
||||||
|
)
|
||||||
|
)
|
||||||
name_pattern = ExpandableNameField(
|
name_pattern = ExpandableNameField(
|
||||||
label='Name'
|
label='Name'
|
||||||
)
|
)
|
||||||
@ -1222,7 +1333,13 @@ class FrontPortTemplateForm(BootstrapMixin, forms.ModelForm):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class FrontPortTemplateCreateForm(ComponentForm):
|
class FrontPortTemplateCreateForm(BootstrapMixin, forms.Form):
|
||||||
|
device_type = forms.ModelChoiceField(
|
||||||
|
queryset=DeviceType.objects.all(),
|
||||||
|
widget=APISelect(
|
||||||
|
api_url='/api/dcim/device-types/'
|
||||||
|
)
|
||||||
|
)
|
||||||
name_pattern = ExpandableNameField(
|
name_pattern = ExpandableNameField(
|
||||||
label='Name'
|
label='Name'
|
||||||
)
|
)
|
||||||
@ -1237,18 +1354,21 @@ class FrontPortTemplateCreateForm(ComponentForm):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
device_type = DeviceType.objects.get(
|
||||||
|
pk=self.initial.get('device_type') or self.data.get('device_type')
|
||||||
|
)
|
||||||
|
|
||||||
# Determine which rear port positions are occupied. These will be excluded from the list of available mappings.
|
# Determine which rear port positions are occupied. These will be excluded from the list of available mappings.
|
||||||
occupied_port_positions = [
|
occupied_port_positions = [
|
||||||
(front_port.rear_port_id, front_port.rear_port_position)
|
(front_port.rear_port_id, front_port.rear_port_position)
|
||||||
for front_port in self.parent.frontport_templates.all()
|
for front_port in device_type.frontport_templates.all()
|
||||||
]
|
]
|
||||||
|
|
||||||
# Populate rear port choices
|
# Populate rear port choices
|
||||||
choices = []
|
choices = []
|
||||||
rear_ports = RearPortTemplate.objects.filter(device_type=self.parent)
|
rear_ports = RearPortTemplate.objects.filter(device_type=device_type)
|
||||||
for rear_port in rear_ports:
|
for rear_port in rear_ports:
|
||||||
for i in range(1, rear_port.positions + 1):
|
for i in range(1, rear_port.positions + 1):
|
||||||
if (rear_port.pk, i) not in occupied_port_positions:
|
if (rear_port.pk, i) not in occupied_port_positions:
|
||||||
@ -1279,6 +1399,21 @@ class FrontPortTemplateCreateForm(ComponentForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class FrontPortTemplateBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||||
|
pk = forms.ModelMultipleChoiceField(
|
||||||
|
queryset=FrontPortTemplate.objects.all(),
|
||||||
|
widget=forms.MultipleHiddenInput()
|
||||||
|
)
|
||||||
|
type = forms.ChoiceField(
|
||||||
|
choices=add_blank_choice(PortTypeChoices),
|
||||||
|
required=False,
|
||||||
|
widget=StaticSelect2()
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
nullable_fields = ()
|
||||||
|
|
||||||
|
|
||||||
class RearPortTemplateForm(BootstrapMixin, forms.ModelForm):
|
class RearPortTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -1292,7 +1427,13 @@ class RearPortTemplateForm(BootstrapMixin, forms.ModelForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class RearPortTemplateCreateForm(ComponentForm):
|
class RearPortTemplateCreateForm(BootstrapMixin, forms.Form):
|
||||||
|
device_type = forms.ModelChoiceField(
|
||||||
|
queryset=DeviceType.objects.all(),
|
||||||
|
widget=APISelect(
|
||||||
|
api_url='/api/dcim/device-types/'
|
||||||
|
)
|
||||||
|
)
|
||||||
name_pattern = ExpandableNameField(
|
name_pattern = ExpandableNameField(
|
||||||
label='Name'
|
label='Name'
|
||||||
)
|
)
|
||||||
@ -1301,13 +1442,28 @@ class RearPortTemplateCreateForm(ComponentForm):
|
|||||||
widget=StaticSelect2(),
|
widget=StaticSelect2(),
|
||||||
)
|
)
|
||||||
positions = forms.IntegerField(
|
positions = forms.IntegerField(
|
||||||
min_value=1,
|
min_value=REARPORT_POSITIONS_MIN,
|
||||||
max_value=64,
|
max_value=REARPORT_POSITIONS_MAX,
|
||||||
initial=1,
|
initial=1,
|
||||||
help_text='The number of front ports which may be mapped to each rear port'
|
help_text='The number of front ports which may be mapped to each rear port'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class RearPortTemplateBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||||
|
pk = forms.ModelMultipleChoiceField(
|
||||||
|
queryset=RearPortTemplate.objects.all(),
|
||||||
|
widget=forms.MultipleHiddenInput()
|
||||||
|
)
|
||||||
|
type = forms.ChoiceField(
|
||||||
|
choices=add_blank_choice(PortTypeChoices),
|
||||||
|
required=False,
|
||||||
|
widget=StaticSelect2()
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
nullable_fields = ()
|
||||||
|
|
||||||
|
|
||||||
class DeviceBayTemplateForm(BootstrapMixin, forms.ModelForm):
|
class DeviceBayTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -1320,12 +1476,29 @@ class DeviceBayTemplateForm(BootstrapMixin, forms.ModelForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class DeviceBayTemplateCreateForm(ComponentForm):
|
class DeviceBayTemplateCreateForm(BootstrapMixin, forms.Form):
|
||||||
|
device_type = forms.ModelChoiceField(
|
||||||
|
queryset=DeviceType.objects.all(),
|
||||||
|
widget=APISelect(
|
||||||
|
api_url='/api/dcim/device-types/'
|
||||||
|
)
|
||||||
|
)
|
||||||
name_pattern = ExpandableNameField(
|
name_pattern = ExpandableNameField(
|
||||||
label='Name'
|
label='Name'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: DeviceBayTemplate has no fields suitable for bulk-editing yet
|
||||||
|
# class DeviceBayTemplateBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||||
|
# pk = forms.ModelMultipleChoiceField(
|
||||||
|
# queryset=FrontPortTemplate.objects.all(),
|
||||||
|
# widget=forms.MultipleHiddenInput()
|
||||||
|
# )
|
||||||
|
#
|
||||||
|
# class Meta:
|
||||||
|
# nullable_fields = ()
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Component template import forms
|
# Component template import forms
|
||||||
#
|
#
|
||||||
@ -1516,7 +1689,7 @@ class PlatformCSVForm(forms.ModelForm):
|
|||||||
# Devices
|
# Devices
|
||||||
#
|
#
|
||||||
|
|
||||||
class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||||
site = forms.ModelChoiceField(
|
site = forms.ModelChoiceField(
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
widget=APISelect(
|
widget=APISelect(
|
||||||
@ -1548,6 +1721,7 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
|||||||
)
|
)
|
||||||
manufacturer = forms.ModelChoiceField(
|
manufacturer = forms.ModelChoiceField(
|
||||||
queryset=Manufacturer.objects.all(),
|
queryset=Manufacturer.objects.all(),
|
||||||
|
required=False,
|
||||||
widget=APISelect(
|
widget=APISelect(
|
||||||
api_url="/api/dcim/manufacturers/",
|
api_url="/api/dcim/manufacturers/",
|
||||||
filter_for={
|
filter_for={
|
||||||
@ -1642,6 +1816,16 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
|||||||
if instance and instance.cluster is not None:
|
if instance and instance.cluster is not None:
|
||||||
kwargs['initial']['cluster_group'] = instance.cluster.group
|
kwargs['initial']['cluster_group'] = instance.cluster.group
|
||||||
|
|
||||||
|
if 'device_type' in kwargs['initial'] and 'manufacturer' not in kwargs['initial']:
|
||||||
|
device_type_id = kwargs['initial']['device_type']
|
||||||
|
manufacturer_id = DeviceType.objects.filter(pk=device_type_id).values_list('manufacturer__pk', flat=True).first()
|
||||||
|
kwargs['initial']['manufacturer'] = manufacturer_id
|
||||||
|
|
||||||
|
if 'cluster' in kwargs['initial'] and 'cluster_group' not in kwargs['initial']:
|
||||||
|
cluster_id = kwargs['initial']['cluster']
|
||||||
|
cluster_group_id = Cluster.objects.filter(pk=cluster_id).values_list('group__pk', flat=True).first()
|
||||||
|
kwargs['initial']['cluster_group'] = cluster_group_id
|
||||||
|
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
if self.instance.pk:
|
if self.instance.pk:
|
||||||
@ -1714,7 +1898,7 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
|||||||
self.initial['rack'] = self.instance.parent_bay.device.rack_id
|
self.initial['rack'] = self.instance.parent_bay.device.rack_id
|
||||||
|
|
||||||
|
|
||||||
class BaseDeviceCSVForm(forms.ModelForm):
|
class BaseDeviceCSVForm(CustomFieldModelCSVForm):
|
||||||
device_role = forms.ModelChoiceField(
|
device_role = forms.ModelChoiceField(
|
||||||
queryset=DeviceRole.objects.all(),
|
queryset=DeviceRole.objects.all(),
|
||||||
to_field_name='name',
|
to_field_name='name',
|
||||||
@ -2096,6 +2280,7 @@ class DeviceFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilt
|
|||||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -2123,8 +2308,8 @@ class DeviceBulkAddInterfaceForm(DeviceBulkAddComponentForm):
|
|||||||
)
|
)
|
||||||
mtu = forms.IntegerField(
|
mtu = forms.IntegerField(
|
||||||
required=False,
|
required=False,
|
||||||
min_value=1,
|
min_value=INTERFACE_MTU_MIN,
|
||||||
max_value=32767,
|
max_value=INTERFACE_MTU_MAX,
|
||||||
label='MTU'
|
label='MTU'
|
||||||
)
|
)
|
||||||
mgmt_only = forms.BooleanField(
|
mgmt_only = forms.BooleanField(
|
||||||
@ -2144,6 +2329,7 @@ class DeviceBulkAddInterfaceForm(DeviceBulkAddComponentForm):
|
|||||||
|
|
||||||
class ConsolePortFilterForm(DeviceComponentFilterForm):
|
class ConsolePortFilterForm(DeviceComponentFilterForm):
|
||||||
model = ConsolePort
|
model = ConsolePort
|
||||||
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
|
|
||||||
class ConsolePortForm(BootstrapMixin, forms.ModelForm):
|
class ConsolePortForm(BootstrapMixin, forms.ModelForm):
|
||||||
@ -2161,7 +2347,13 @@ class ConsolePortForm(BootstrapMixin, forms.ModelForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class ConsolePortCreateForm(ComponentForm):
|
class ConsolePortCreateForm(BootstrapMixin, forms.Form):
|
||||||
|
device = forms.ModelChoiceField(
|
||||||
|
queryset=Device.objects.prefetch_related('device_type__manufacturer'),
|
||||||
|
widget=APISelect(
|
||||||
|
api_url="/api/dcim/devices/",
|
||||||
|
)
|
||||||
|
)
|
||||||
name_pattern = ExpandableNameField(
|
name_pattern = ExpandableNameField(
|
||||||
label='Name'
|
label='Name'
|
||||||
)
|
)
|
||||||
@ -2201,6 +2393,7 @@ class ConsolePortCSVForm(forms.ModelForm):
|
|||||||
|
|
||||||
class ConsoleServerPortFilterForm(DeviceComponentFilterForm):
|
class ConsoleServerPortFilterForm(DeviceComponentFilterForm):
|
||||||
model = ConsoleServerPort
|
model = ConsoleServerPort
|
||||||
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
|
|
||||||
class ConsoleServerPortForm(BootstrapMixin, forms.ModelForm):
|
class ConsoleServerPortForm(BootstrapMixin, forms.ModelForm):
|
||||||
@ -2218,7 +2411,13 @@ class ConsoleServerPortForm(BootstrapMixin, forms.ModelForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class ConsoleServerPortCreateForm(ComponentForm):
|
class ConsoleServerPortCreateForm(BootstrapMixin, forms.Form):
|
||||||
|
device = forms.ModelChoiceField(
|
||||||
|
queryset=Device.objects.prefetch_related('device_type__manufacturer'),
|
||||||
|
widget=APISelect(
|
||||||
|
api_url="/api/dcim/devices/",
|
||||||
|
)
|
||||||
|
)
|
||||||
name_pattern = ExpandableNameField(
|
name_pattern = ExpandableNameField(
|
||||||
label='Name'
|
label='Name'
|
||||||
)
|
)
|
||||||
@ -2293,6 +2492,7 @@ class ConsoleServerPortCSVForm(forms.ModelForm):
|
|||||||
|
|
||||||
class PowerPortFilterForm(DeviceComponentFilterForm):
|
class PowerPortFilterForm(DeviceComponentFilterForm):
|
||||||
model = PowerPort
|
model = PowerPort
|
||||||
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
|
|
||||||
class PowerPortForm(BootstrapMixin, forms.ModelForm):
|
class PowerPortForm(BootstrapMixin, forms.ModelForm):
|
||||||
@ -2310,7 +2510,13 @@ class PowerPortForm(BootstrapMixin, forms.ModelForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class PowerPortCreateForm(ComponentForm):
|
class PowerPortCreateForm(BootstrapMixin, forms.Form):
|
||||||
|
device = forms.ModelChoiceField(
|
||||||
|
queryset=Device.objects.prefetch_related('device_type__manufacturer'),
|
||||||
|
widget=APISelect(
|
||||||
|
api_url="/api/dcim/devices/",
|
||||||
|
)
|
||||||
|
)
|
||||||
name_pattern = ExpandableNameField(
|
name_pattern = ExpandableNameField(
|
||||||
label='Name'
|
label='Name'
|
||||||
)
|
)
|
||||||
@ -2360,6 +2566,7 @@ class PowerPortCSVForm(forms.ModelForm):
|
|||||||
|
|
||||||
class PowerOutletFilterForm(DeviceComponentFilterForm):
|
class PowerOutletFilterForm(DeviceComponentFilterForm):
|
||||||
model = PowerOutlet
|
model = PowerOutlet
|
||||||
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
|
|
||||||
class PowerOutletForm(BootstrapMixin, forms.ModelForm):
|
class PowerOutletForm(BootstrapMixin, forms.ModelForm):
|
||||||
@ -2390,7 +2597,13 @@ class PowerOutletForm(BootstrapMixin, forms.ModelForm):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class PowerOutletCreateForm(ComponentForm):
|
class PowerOutletCreateForm(BootstrapMixin, forms.Form):
|
||||||
|
device = forms.ModelChoiceField(
|
||||||
|
queryset=Device.objects.prefetch_related('device_type__manufacturer'),
|
||||||
|
widget=APISelect(
|
||||||
|
api_url="/api/dcim/devices/",
|
||||||
|
)
|
||||||
|
)
|
||||||
name_pattern = ExpandableNameField(
|
name_pattern = ExpandableNameField(
|
||||||
label='Name'
|
label='Name'
|
||||||
)
|
)
|
||||||
@ -2416,11 +2629,13 @@ class PowerOutletCreateForm(ComponentForm):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
# Limit power_port choices to those on the parent device
|
# Limit power_port queryset to PowerPorts which belong to the parent Device
|
||||||
self.fields['power_port'].queryset = PowerPort.objects.filter(device=self.parent)
|
device = Device.objects.get(
|
||||||
|
pk=self.initial.get('device') or self.data.get('device')
|
||||||
|
)
|
||||||
|
self.fields['power_port'].queryset = PowerPort.objects.filter(device=device)
|
||||||
|
|
||||||
|
|
||||||
class PowerOutletCSVForm(forms.ModelForm):
|
class PowerOutletCSVForm(forms.ModelForm):
|
||||||
@ -2478,8 +2693,12 @@ class PowerOutletBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
|
|||||||
queryset=PowerOutlet.objects.all(),
|
queryset=PowerOutlet.objects.all(),
|
||||||
widget=forms.MultipleHiddenInput()
|
widget=forms.MultipleHiddenInput()
|
||||||
)
|
)
|
||||||
|
device = forms.ModelChoiceField(
|
||||||
|
queryset=Device.objects.all(),
|
||||||
|
widget=forms.HiddenInput()
|
||||||
|
)
|
||||||
type = forms.ChoiceField(
|
type = forms.ChoiceField(
|
||||||
choices=PowerOutletTypeChoices,
|
choices=add_blank_choice(PowerOutletTypeChoices),
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
feed_leg = forms.ChoiceField(
|
feed_leg = forms.ChoiceField(
|
||||||
@ -2504,7 +2723,9 @@ class PowerOutletBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
|
|||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
# Limit power_port queryset to PowerPorts which belong to the parent Device
|
# Limit power_port queryset to PowerPorts which belong to the parent Device
|
||||||
self.fields['power_port'].queryset = PowerPort.objects.filter(device=self.parent_obj)
|
if 'device' in self.initial:
|
||||||
|
device = Device.objects.filter(pk=self.initial['device']).first()
|
||||||
|
self.fields['power_port'].queryset = PowerPort.objects.filter(device=device)
|
||||||
|
|
||||||
|
|
||||||
class PowerOutletBulkRenameForm(BulkRenameForm):
|
class PowerOutletBulkRenameForm(BulkRenameForm):
|
||||||
@ -2528,6 +2749,7 @@ class PowerOutletBulkDisconnectForm(ConfirmationForm):
|
|||||||
|
|
||||||
class InterfaceFilterForm(DeviceComponentFilterForm):
|
class InterfaceFilterForm(DeviceComponentFilterForm):
|
||||||
model = Interface
|
model = Interface
|
||||||
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
|
|
||||||
class InterfaceForm(InterfaceCommonForm, BootstrapMixin, forms.ModelForm):
|
class InterfaceForm(InterfaceCommonForm, BootstrapMixin, forms.ModelForm):
|
||||||
@ -2584,14 +2806,19 @@ class InterfaceForm(InterfaceCommonForm, BootstrapMixin, forms.ModelForm):
|
|||||||
type=InterfaceTypeChoices.TYPE_LAG
|
type=InterfaceTypeChoices.TYPE_LAG
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
device = self.instance.device
|
|
||||||
self.fields['lag'].queryset = Interface.objects.filter(
|
self.fields['lag'].queryset = Interface.objects.filter(
|
||||||
device__in=[self.instance.device, self.instance.device.get_vc_master()],
|
device__in=[self.instance.device, self.instance.device.get_vc_master()],
|
||||||
type=InterfaceTypeChoices.TYPE_LAG
|
type=InterfaceTypeChoices.TYPE_LAG
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class InterfaceCreateForm(InterfaceCommonForm, ComponentForm, forms.Form):
|
class InterfaceCreateForm(BootstrapMixin, InterfaceCommonForm, forms.Form):
|
||||||
|
device = forms.ModelChoiceField(
|
||||||
|
queryset=Device.objects.prefetch_related('device_type__manufacturer'),
|
||||||
|
widget=APISelect(
|
||||||
|
api_url="/api/dcim/devices/",
|
||||||
|
)
|
||||||
|
)
|
||||||
name_pattern = ExpandableNameField(
|
name_pattern = ExpandableNameField(
|
||||||
label='Name'
|
label='Name'
|
||||||
)
|
)
|
||||||
@ -2600,7 +2827,8 @@ class InterfaceCreateForm(InterfaceCommonForm, ComponentForm, forms.Form):
|
|||||||
widget=StaticSelect2(),
|
widget=StaticSelect2(),
|
||||||
)
|
)
|
||||||
enabled = forms.BooleanField(
|
enabled = forms.BooleanField(
|
||||||
required=False
|
required=False,
|
||||||
|
initial=True
|
||||||
)
|
)
|
||||||
lag = forms.ModelChoiceField(
|
lag = forms.ModelChoiceField(
|
||||||
queryset=Interface.objects.all(),
|
queryset=Interface.objects.all(),
|
||||||
@ -2610,8 +2838,8 @@ class InterfaceCreateForm(InterfaceCommonForm, ComponentForm, forms.Form):
|
|||||||
)
|
)
|
||||||
mtu = forms.IntegerField(
|
mtu = forms.IntegerField(
|
||||||
required=False,
|
required=False,
|
||||||
min_value=1,
|
min_value=INTERFACE_MTU_MIN,
|
||||||
max_value=32767,
|
max_value=INTERFACE_MTU_MAX,
|
||||||
label='MTU'
|
label='MTU'
|
||||||
)
|
)
|
||||||
mac_address = forms.CharField(
|
mac_address = forms.CharField(
|
||||||
@ -2655,21 +2883,16 @@ class InterfaceCreateForm(InterfaceCommonForm, ComponentForm, forms.Form):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
|
||||||
# Set interfaces enabled by default
|
|
||||||
kwargs['initial'] = kwargs.get('initial', {}).copy()
|
|
||||||
kwargs['initial'].update({'enabled': True})
|
|
||||||
|
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
# Limit LAG choices to interfaces belonging to this device (or its VC master)
|
# Limit LAG choices to interfaces which belong to the parent device (or VC master)
|
||||||
if self.parent is not None:
|
device = Device.objects.get(
|
||||||
|
pk=self.initial.get('device') or self.data.get('device')
|
||||||
|
)
|
||||||
self.fields['lag'].queryset = Interface.objects.filter(
|
self.fields['lag'].queryset = Interface.objects.filter(
|
||||||
device__in=[self.parent, self.parent.get_vc_master()],
|
device__in=[device, device.get_vc_master()],
|
||||||
type=InterfaceTypeChoices.TYPE_LAG
|
type=InterfaceTypeChoices.TYPE_LAG
|
||||||
)
|
)
|
||||||
else:
|
|
||||||
self.fields['lag'].queryset = Interface.objects.none()
|
|
||||||
|
|
||||||
|
|
||||||
class InterfaceCSVForm(forms.ModelForm):
|
class InterfaceCSVForm(forms.ModelForm):
|
||||||
@ -2716,7 +2939,7 @@ class InterfaceCSVForm(forms.ModelForm):
|
|||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
# Limit LAG choices to interfaces belonging to this device (or VC master)
|
# Limit LAG choices to interfaces belonging to this device (or VC master)
|
||||||
if self.is_bound:
|
if self.is_bound and 'device' in self.data:
|
||||||
try:
|
try:
|
||||||
device = self.fields['device'].to_python(self.data['device'])
|
device = self.fields['device'].to_python(self.data['device'])
|
||||||
except forms.ValidationError:
|
except forms.ValidationError:
|
||||||
@ -2739,11 +2962,15 @@ class InterfaceCSVForm(forms.ModelForm):
|
|||||||
return self.cleaned_data['enabled']
|
return self.cleaned_data['enabled']
|
||||||
|
|
||||||
|
|
||||||
class InterfaceBulkEditForm(InterfaceCommonForm, BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
|
class InterfaceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
|
||||||
pk = forms.ModelMultipleChoiceField(
|
pk = forms.ModelMultipleChoiceField(
|
||||||
queryset=Interface.objects.all(),
|
queryset=Interface.objects.all(),
|
||||||
widget=forms.MultipleHiddenInput()
|
widget=forms.MultipleHiddenInput()
|
||||||
)
|
)
|
||||||
|
device = forms.ModelChoiceField(
|
||||||
|
queryset=Device.objects.all(),
|
||||||
|
widget=forms.HiddenInput()
|
||||||
|
)
|
||||||
type = forms.ChoiceField(
|
type = forms.ChoiceField(
|
||||||
choices=add_blank_choice(InterfaceTypeChoices),
|
choices=add_blank_choice(InterfaceTypeChoices),
|
||||||
required=False,
|
required=False,
|
||||||
@ -2765,8 +2992,8 @@ class InterfaceBulkEditForm(InterfaceCommonForm, BootstrapMixin, AddRemoveTagsFo
|
|||||||
)
|
)
|
||||||
mtu = forms.IntegerField(
|
mtu = forms.IntegerField(
|
||||||
required=False,
|
required=False,
|
||||||
min_value=1,
|
min_value=INTERFACE_MTU_MIN,
|
||||||
max_value=32767,
|
max_value=INTERFACE_MTU_MAX,
|
||||||
label='MTU'
|
label='MTU'
|
||||||
)
|
)
|
||||||
mgmt_only = forms.NullBooleanField(
|
mgmt_only = forms.NullBooleanField(
|
||||||
@ -2811,14 +3038,24 @@ class InterfaceBulkEditForm(InterfaceCommonForm, BootstrapMixin, AddRemoveTagsFo
|
|||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
# Limit LAG choices to interfaces which belong to the parent device (or VC master)
|
# Limit LAG choices to interfaces which belong to the parent device (or VC master)
|
||||||
device = self.parent_obj
|
if 'device' in self.initial:
|
||||||
if device is not None:
|
device = Device.objects.filter(pk=self.initial['device']).first()
|
||||||
self.fields['lag'].queryset = Interface.objects.filter(
|
self.fields['lag'].queryset = Interface.objects.filter(
|
||||||
device__in=[device, device.get_vc_master()],
|
device__in=[device, device.get_vc_master()],
|
||||||
type=InterfaceTypeChoices.TYPE_LAG
|
type=InterfaceTypeChoices.TYPE_LAG
|
||||||
)
|
)
|
||||||
else:
|
|
||||||
self.fields['lag'].choices = []
|
def clean(self):
|
||||||
|
|
||||||
|
# Untagged interfaces cannot be assigned tagged VLANs
|
||||||
|
if self.cleaned_data['mode'] == InterfaceModeChoices.MODE_ACCESS and self.cleaned_data['tagged_vlans']:
|
||||||
|
raise forms.ValidationError({
|
||||||
|
'mode': "An access interface cannot have tagged VLANs assigned."
|
||||||
|
})
|
||||||
|
|
||||||
|
# Remove all tagged VLAN assignments from "tagged all" interfaces
|
||||||
|
elif self.cleaned_data['mode'] == InterfaceModeChoices.MODE_TAGGED_ALL:
|
||||||
|
self.cleaned_data['tagged_vlans'] = []
|
||||||
|
|
||||||
|
|
||||||
class InterfaceBulkRenameForm(BulkRenameForm):
|
class InterfaceBulkRenameForm(BulkRenameForm):
|
||||||
@ -2841,6 +3078,7 @@ class InterfaceBulkDisconnectForm(ConfirmationForm):
|
|||||||
|
|
||||||
class FrontPortFilterForm(DeviceComponentFilterForm):
|
class FrontPortFilterForm(DeviceComponentFilterForm):
|
||||||
model = FrontPort
|
model = FrontPort
|
||||||
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
|
|
||||||
class FrontPortForm(BootstrapMixin, forms.ModelForm):
|
class FrontPortForm(BootstrapMixin, forms.ModelForm):
|
||||||
@ -2870,7 +3108,13 @@ class FrontPortForm(BootstrapMixin, forms.ModelForm):
|
|||||||
|
|
||||||
|
|
||||||
# TODO: Merge with FrontPortTemplateCreateForm to remove duplicate logic
|
# TODO: Merge with FrontPortTemplateCreateForm to remove duplicate logic
|
||||||
class FrontPortCreateForm(ComponentForm):
|
class FrontPortCreateForm(BootstrapMixin, forms.Form):
|
||||||
|
device = forms.ModelChoiceField(
|
||||||
|
queryset=Device.objects.prefetch_related('device_type__manufacturer'),
|
||||||
|
widget=APISelect(
|
||||||
|
api_url="/api/dcim/devices/",
|
||||||
|
)
|
||||||
|
)
|
||||||
name_pattern = ExpandableNameField(
|
name_pattern = ExpandableNameField(
|
||||||
label='Name'
|
label='Name'
|
||||||
)
|
)
|
||||||
@ -2890,15 +3134,20 @@ class FrontPortCreateForm(ComponentForm):
|
|||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
# Determine which rear port positions are occupied. These will be excluded from the list of available mappings.
|
device = Device.objects.get(
|
||||||
|
pk=self.initial.get('device') or self.data.get('device')
|
||||||
|
)
|
||||||
|
|
||||||
|
# Determine which rear port positions are occupied. These will be excluded from the list of available
|
||||||
|
# mappings.
|
||||||
occupied_port_positions = [
|
occupied_port_positions = [
|
||||||
(front_port.rear_port_id, front_port.rear_port_position)
|
(front_port.rear_port_id, front_port.rear_port_position)
|
||||||
for front_port in self.parent.frontports.all()
|
for front_port in device.frontports.all()
|
||||||
]
|
]
|
||||||
|
|
||||||
# Populate rear port choices
|
# Populate rear port choices
|
||||||
choices = []
|
choices = []
|
||||||
rear_ports = RearPort.objects.filter(device=self.parent)
|
rear_ports = RearPort.objects.filter(device=device)
|
||||||
for rear_port in rear_ports:
|
for rear_port in rear_ports:
|
||||||
for i in range(1, rear_port.positions + 1):
|
for i in range(1, rear_port.positions + 1):
|
||||||
if (rear_port.pk, i) not in occupied_port_positions:
|
if (rear_port.pk, i) not in occupied_port_positions:
|
||||||
@ -3018,6 +3267,7 @@ class FrontPortBulkDisconnectForm(ConfirmationForm):
|
|||||||
|
|
||||||
class RearPortFilterForm(DeviceComponentFilterForm):
|
class RearPortFilterForm(DeviceComponentFilterForm):
|
||||||
model = RearPort
|
model = RearPort
|
||||||
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
|
|
||||||
class RearPortForm(BootstrapMixin, forms.ModelForm):
|
class RearPortForm(BootstrapMixin, forms.ModelForm):
|
||||||
@ -3036,7 +3286,13 @@ class RearPortForm(BootstrapMixin, forms.ModelForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class RearPortCreateForm(ComponentForm):
|
class RearPortCreateForm(BootstrapMixin, forms.Form):
|
||||||
|
device = forms.ModelChoiceField(
|
||||||
|
queryset=Device.objects.prefetch_related('device_type__manufacturer'),
|
||||||
|
widget=APISelect(
|
||||||
|
api_url="/api/dcim/devices/",
|
||||||
|
)
|
||||||
|
)
|
||||||
name_pattern = ExpandableNameField(
|
name_pattern = ExpandableNameField(
|
||||||
label='Name'
|
label='Name'
|
||||||
)
|
)
|
||||||
@ -3045,8 +3301,8 @@ class RearPortCreateForm(ComponentForm):
|
|||||||
widget=StaticSelect2(),
|
widget=StaticSelect2(),
|
||||||
)
|
)
|
||||||
positions = forms.IntegerField(
|
positions = forms.IntegerField(
|
||||||
min_value=1,
|
min_value=REARPORT_POSITIONS_MIN,
|
||||||
max_value=64,
|
max_value=REARPORT_POSITIONS_MAX,
|
||||||
initial=1,
|
initial=1,
|
||||||
help_text='The number of front ports which may be mapped to each rear port'
|
help_text='The number of front ports which may be mapped to each rear port'
|
||||||
)
|
)
|
||||||
@ -3523,7 +3779,7 @@ class CableBulkEditForm(BootstrapMixin, BulkEditForm):
|
|||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
color = forms.CharField(
|
color = forms.CharField(
|
||||||
max_length=6,
|
max_length=6, # RGB color code
|
||||||
required=False,
|
required=False,
|
||||||
widget=ColorSelect()
|
widget=ColorSelect()
|
||||||
)
|
)
|
||||||
@ -3602,7 +3858,7 @@ class CableFilterForm(BootstrapMixin, forms.Form):
|
|||||||
widget=StaticSelect2()
|
widget=StaticSelect2()
|
||||||
)
|
)
|
||||||
color = forms.CharField(
|
color = forms.CharField(
|
||||||
max_length=6,
|
max_length=6, # RGB color code
|
||||||
required=False,
|
required=False,
|
||||||
widget=ColorSelect()
|
widget=ColorSelect()
|
||||||
)
|
)
|
||||||
@ -3622,6 +3878,7 @@ class CableFilterForm(BootstrapMixin, forms.Form):
|
|||||||
|
|
||||||
class DeviceBayFilterForm(DeviceComponentFilterForm):
|
class DeviceBayFilterForm(DeviceComponentFilterForm):
|
||||||
model = DeviceBay
|
model = DeviceBay
|
||||||
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
|
|
||||||
class DeviceBayForm(BootstrapMixin, forms.ModelForm):
|
class DeviceBayForm(BootstrapMixin, forms.ModelForm):
|
||||||
@ -3639,7 +3896,13 @@ class DeviceBayForm(BootstrapMixin, forms.ModelForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class DeviceBayCreateForm(ComponentForm):
|
class DeviceBayCreateForm(BootstrapMixin, forms.Form):
|
||||||
|
device = forms.ModelChoiceField(
|
||||||
|
queryset=Device.objects.prefetch_related('device_type__manufacturer'),
|
||||||
|
widget=APISelect(
|
||||||
|
api_url="/api/dcim/devices/",
|
||||||
|
)
|
||||||
|
)
|
||||||
name_pattern = ExpandableNameField(
|
name_pattern = ExpandableNameField(
|
||||||
label='Name'
|
label='Name'
|
||||||
)
|
)
|
||||||
@ -3811,6 +4074,42 @@ class InventoryItemForm(BootstrapMixin, forms.ModelForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class InventoryItemCreateForm(BootstrapMixin, forms.Form):
|
||||||
|
device = forms.ModelChoiceField(
|
||||||
|
queryset=Device.objects.prefetch_related('device_type__manufacturer'),
|
||||||
|
widget=APISelect(
|
||||||
|
api_url="/api/dcim/devices/",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
name_pattern = ExpandableNameField(
|
||||||
|
label='Name'
|
||||||
|
)
|
||||||
|
manufacturer = forms.ModelChoiceField(
|
||||||
|
queryset=Manufacturer.objects.all(),
|
||||||
|
required=False,
|
||||||
|
widget=APISelect(
|
||||||
|
api_url="/api/dcim/manufacturers/"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
part_id = forms.CharField(
|
||||||
|
max_length=50,
|
||||||
|
required=False,
|
||||||
|
label='Part ID'
|
||||||
|
)
|
||||||
|
serial = forms.CharField(
|
||||||
|
max_length=50,
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
asset_tag = forms.CharField(
|
||||||
|
max_length=50,
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
description = forms.CharField(
|
||||||
|
max_length=100,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class InventoryItemCSVForm(forms.ModelForm):
|
class InventoryItemCSVForm(forms.ModelForm):
|
||||||
device = FlexibleModelChoiceField(
|
device = FlexibleModelChoiceField(
|
||||||
queryset=Device.objects.all(),
|
queryset=Device.objects.all(),
|
||||||
@ -3921,6 +4220,7 @@ class InventoryItemFilterForm(BootstrapMixin, forms.Form):
|
|||||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -4107,6 +4407,7 @@ class VirtualChassisFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
|||||||
null_option=True,
|
null_option=True,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -4219,7 +4520,7 @@ class PowerPanelFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
|||||||
# Power feeds
|
# Power feeds
|
||||||
#
|
#
|
||||||
|
|
||||||
class PowerFeedForm(BootstrapMixin, CustomFieldForm):
|
class PowerFeedForm(BootstrapMixin, CustomFieldModelForm):
|
||||||
site = ChainedModelChoiceField(
|
site = ChainedModelChoiceField(
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
@ -4264,7 +4565,7 @@ class PowerFeedForm(BootstrapMixin, CustomFieldForm):
|
|||||||
self.initial['site'] = self.instance.power_panel.site
|
self.initial['site'] = self.instance.power_panel.site
|
||||||
|
|
||||||
|
|
||||||
class PowerFeedCSVForm(forms.ModelForm):
|
class PowerFeedCSVForm(CustomFieldModelCSVForm):
|
||||||
site = forms.ModelChoiceField(
|
site = forms.ModelChoiceField(
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
to_field_name='name',
|
to_field_name='name',
|
||||||
@ -4347,7 +4648,7 @@ class PowerFeedBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEd
|
|||||||
queryset=PowerFeed.objects.all(),
|
queryset=PowerFeed.objects.all(),
|
||||||
widget=forms.MultipleHiddenInput
|
widget=forms.MultipleHiddenInput
|
||||||
)
|
)
|
||||||
powerpanel = forms.ModelChoiceField(
|
power_panel = forms.ModelChoiceField(
|
||||||
queryset=PowerPanel.objects.all(),
|
queryset=PowerPanel.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
widget=APISelect(
|
widget=APISelect(
|
||||||
@ -4397,8 +4698,9 @@ class PowerFeedBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEd
|
|||||||
max_utilization = forms.IntegerField(
|
max_utilization = forms.IntegerField(
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
comments = forms.CharField(
|
comments = CommentField(
|
||||||
required=False
|
widget=SmallTextarea,
|
||||||
|
label='Comments'
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -4484,3 +4786,4 @@ class PowerFeedFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
|||||||
max_utilization = forms.IntegerField(
|
max_utilization = forms.IntegerField(
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
|
tag = TagFilterField(model)
|
||||||
|
@ -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):
|
||||||
|
27
netbox/dcim/migrations/0092_fix_rack_outer_unit.py
Normal file
27
netbox/dcim/migrations/0092_fix_rack_outer_unit.py
Normal 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
|
||||||
|
),
|
||||||
|
]
|
@ -405,7 +405,7 @@ class RackElevationHelperMixin:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _draw_device_rear(drawing, device, start, end, text):
|
def _draw_device_rear(drawing, device, start, end, text):
|
||||||
rect = drawing.rect(start, end, class_="blocked")
|
rect = drawing.rect(start, end, class_="slot blocked")
|
||||||
rect.set_desc('{} — {} ({}U) {} {}'.format(
|
rect.set_desc('{} — {} ({}U) {} {}'.format(
|
||||||
device.device_role, device.device_type.display_name,
|
device.device_role, device.device_type.display_name,
|
||||||
device.device_type.u_height, device.asset_tag or '', device.serial or ''
|
device.device_type.u_height, device.asset_tag or '', device.serial or ''
|
||||||
@ -414,7 +414,7 @@ class RackElevationHelperMixin:
|
|||||||
drawing.add(drawing.text(str(device), insert=text))
|
drawing.add(drawing.text(str(device), insert=text))
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _draw_empty(drawing, rack, start, end, text, id_, face_id, class_):
|
def _draw_empty(drawing, rack, start, end, text, id_, face_id, class_, reservation):
|
||||||
link = drawing.add(
|
link = drawing.add(
|
||||||
drawing.a(
|
drawing.a(
|
||||||
href='{}?{}'.format(
|
href='{}?{}'.format(
|
||||||
@ -424,6 +424,10 @@ class RackElevationHelperMixin:
|
|||||||
target='_top'
|
target='_top'
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
if reservation:
|
||||||
|
link.set_desc('{} — {} · {}'.format(
|
||||||
|
reservation.description, reservation.user, reservation.created
|
||||||
|
))
|
||||||
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'))
|
||||||
|
|
||||||
@ -453,12 +457,13 @@ class RackElevationHelperMixin:
|
|||||||
else:
|
else:
|
||||||
# Draw shallow devices, reservations, or empty units
|
# Draw shallow devices, reservations, or empty units
|
||||||
class_ = 'slot'
|
class_ = 'slot'
|
||||||
|
reservation = reserved_units.get(unit["id"])
|
||||||
if device:
|
if device:
|
||||||
class_ += ' occupied'
|
class_ += ' occupied'
|
||||||
if unit["id"] in reserved_units:
|
if reservation:
|
||||||
class_ += ' reserved'
|
class_ += ' reserved'
|
||||||
self._draw_empty(
|
self._draw_empty(
|
||||||
drawing, self, start_cordinates, end_cordinates, text_cordinates, unit["id"], face, class_
|
drawing, self, start_cordinates, end_cordinates, text_cordinates, unit["id"], face, class_, reservation
|
||||||
)
|
)
|
||||||
|
|
||||||
unit_cursor += height
|
unit_cursor += height
|
||||||
@ -483,7 +488,12 @@ class RackElevationHelperMixin:
|
|||||||
|
|
||||||
return elevation
|
return elevation
|
||||||
|
|
||||||
def get_elevation_svg(self, face=DeviceFaceChoices.FACE_FRONT, unit_width=230, unit_height=20):
|
def get_elevation_svg(
|
||||||
|
self,
|
||||||
|
face=DeviceFaceChoices.FACE_FRONT,
|
||||||
|
unit_width=RACK_ELEVATION_UNIT_WIDTH_DEFAULT,
|
||||||
|
unit_height=RACK_ELEVATION_UNIT_HEIGHT_DEFAULT
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Return an SVG of the rack elevation
|
Return an SVG of the rack elevation
|
||||||
|
|
||||||
@ -493,7 +503,7 @@ class RackElevationHelperMixin:
|
|||||||
height of the elevation
|
height of the elevation
|
||||||
"""
|
"""
|
||||||
elevation = self.merge_elevations(face)
|
elevation = self.merge_elevations(face)
|
||||||
reserved_units = self.get_reserved_units().keys()
|
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)
|
||||||
|
|
||||||
@ -569,7 +579,7 @@ class Rack(ChangeLoggedModel, CustomFieldModel, RackElevationHelperMixin):
|
|||||||
help_text='Rail-to-rail width'
|
help_text='Rail-to-rail width'
|
||||||
)
|
)
|
||||||
u_height = models.PositiveSmallIntegerField(
|
u_height = models.PositiveSmallIntegerField(
|
||||||
default=42,
|
default=RACK_U_HEIGHT_DEFAULT,
|
||||||
verbose_name='Height (U)',
|
verbose_name='Height (U)',
|
||||||
validators=[MinValueValidator(1), MaxValueValidator(100)]
|
validators=[MinValueValidator(1), MaxValueValidator(100)]
|
||||||
)
|
)
|
||||||
@ -1008,9 +1018,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',
|
||||||
]
|
]
|
||||||
@ -1859,15 +1866,15 @@ class PowerFeed(ChangeLoggedModel, CableTermination, CustomFieldModel):
|
|||||||
)
|
)
|
||||||
voltage = models.PositiveSmallIntegerField(
|
voltage = models.PositiveSmallIntegerField(
|
||||||
validators=[MinValueValidator(1)],
|
validators=[MinValueValidator(1)],
|
||||||
default=120
|
default=POWERFEED_VOLTAGE_DEFAULT
|
||||||
)
|
)
|
||||||
amperage = models.PositiveSmallIntegerField(
|
amperage = models.PositiveSmallIntegerField(
|
||||||
validators=[MinValueValidator(1)],
|
validators=[MinValueValidator(1)],
|
||||||
default=20
|
default=POWERFEED_AMPERAGE_DEFAULT
|
||||||
)
|
)
|
||||||
max_utilization = models.PositiveSmallIntegerField(
|
max_utilization = models.PositiveSmallIntegerField(
|
||||||
validators=[MinValueValidator(1), MaxValueValidator(100)],
|
validators=[MinValueValidator(1), MaxValueValidator(100)],
|
||||||
default=80,
|
default=POWERFEED_MAX_UTILIZATION_DEFAULT,
|
||||||
help_text="Maximum permissible draw (percentage)"
|
help_text="Maximum permissible draw (percentage)"
|
||||||
)
|
)
|
||||||
available_power = models.PositiveIntegerField(
|
available_power = models.PositiveIntegerField(
|
||||||
|
@ -676,7 +676,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)
|
||||||
@ -1004,7 +1004,7 @@ class InventoryItem(ComponentModel):
|
|||||||
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 (
|
||||||
|
@ -440,7 +440,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"
|
||||||
|
|
||||||
|
|
||||||
@ -777,6 +777,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')
|
||||||
|
@ -2,6 +2,7 @@ from django.test import TestCase
|
|||||||
|
|
||||||
from dcim.forms import *
|
from dcim.forms import *
|
||||||
from dcim.models import *
|
from dcim.models import *
|
||||||
|
from virtualization.models import Cluster, ClusterGroup, ClusterType
|
||||||
|
|
||||||
|
|
||||||
def get_id(model, slug):
|
def get_id(model, slug):
|
||||||
@ -10,71 +11,108 @@ def get_id(model, slug):
|
|||||||
|
|
||||||
class DeviceTestCase(TestCase):
|
class DeviceTestCase(TestCase):
|
||||||
|
|
||||||
fixtures = ['dcim', 'ipam']
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
|
||||||
|
site = Site.objects.create(name='Site 1', slug='site-1')
|
||||||
|
rack = Rack.objects.create(name='Rack 1', site=site)
|
||||||
|
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
||||||
|
device_type = DeviceType.objects.create(
|
||||||
|
manufacturer=manufacturer, model='Device Type 1', slug='device-type-1', u_height=1
|
||||||
|
)
|
||||||
|
device_role = DeviceRole.objects.create(
|
||||||
|
name='Device Role 1', slug='device-role-1', color='ff0000'
|
||||||
|
)
|
||||||
|
Platform.objects.create(name='Platform 1', slug='platform-1')
|
||||||
|
Device.objects.create(
|
||||||
|
name='Device 1', device_type=device_type, device_role=device_role, site=site, rack=rack, position=1
|
||||||
|
)
|
||||||
|
cluster_type = ClusterType.objects.create(name='Cluster Type 1', slug='cluster-type-1')
|
||||||
|
cluster_group = ClusterGroup.objects.create(name='Cluster Group 1', slug='cluster-group-1')
|
||||||
|
Cluster.objects.create(name='Cluster 1', type=cluster_type, group=cluster_group)
|
||||||
|
|
||||||
def test_racked_device(self):
|
def test_racked_device(self):
|
||||||
test = DeviceForm(data={
|
form = DeviceForm(data={
|
||||||
'name': 'test',
|
'name': 'New Device',
|
||||||
'device_role': get_id(DeviceRole, 'leaf-switch'),
|
'device_role': DeviceRole.objects.first().pk,
|
||||||
'tenant': None,
|
'tenant': None,
|
||||||
'manufacturer': get_id(Manufacturer, 'juniper'),
|
'manufacturer': Manufacturer.objects.first().pk,
|
||||||
'device_type': get_id(DeviceType, 'qfx5100-48s'),
|
'device_type': DeviceType.objects.first().pk,
|
||||||
'site': get_id(Site, 'test1'),
|
'site': Site.objects.first().pk,
|
||||||
'rack': '1',
|
'rack': Rack.objects.first().pk,
|
||||||
'face': DeviceFaceChoices.FACE_FRONT,
|
'face': DeviceFaceChoices.FACE_FRONT,
|
||||||
'position': 41,
|
'position': 2,
|
||||||
'platform': get_id(Platform, 'juniper-junos'),
|
'platform': Platform.objects.first().pk,
|
||||||
'status': DeviceStatusChoices.STATUS_ACTIVE,
|
'status': DeviceStatusChoices.STATUS_ACTIVE,
|
||||||
})
|
})
|
||||||
self.assertTrue(test.is_valid(), test.fields['position'].choices)
|
self.assertTrue(form.is_valid())
|
||||||
self.assertTrue(test.save())
|
self.assertTrue(form.save())
|
||||||
|
|
||||||
def test_racked_device_occupied(self):
|
def test_racked_device_occupied(self):
|
||||||
test = DeviceForm(data={
|
form = DeviceForm(data={
|
||||||
'name': 'test',
|
'name': 'test',
|
||||||
'device_role': get_id(DeviceRole, 'leaf-switch'),
|
'device_role': DeviceRole.objects.first().pk,
|
||||||
'tenant': None,
|
'tenant': None,
|
||||||
'manufacturer': get_id(Manufacturer, 'juniper'),
|
'manufacturer': Manufacturer.objects.first().pk,
|
||||||
'device_type': get_id(DeviceType, 'qfx5100-48s'),
|
'device_type': DeviceType.objects.first().pk,
|
||||||
'site': get_id(Site, 'test1'),
|
'site': Site.objects.first().pk,
|
||||||
'rack': '1',
|
'rack': Rack.objects.first().pk,
|
||||||
'face': DeviceFaceChoices.FACE_FRONT,
|
'face': DeviceFaceChoices.FACE_FRONT,
|
||||||
'position': 1,
|
'position': 1,
|
||||||
'platform': get_id(Platform, 'juniper-junos'),
|
'platform': Platform.objects.first().pk,
|
||||||
'status': DeviceStatusChoices.STATUS_ACTIVE,
|
'status': DeviceStatusChoices.STATUS_ACTIVE,
|
||||||
})
|
})
|
||||||
self.assertFalse(test.is_valid())
|
self.assertFalse(form.is_valid())
|
||||||
|
self.assertIn('position', form.errors)
|
||||||
|
|
||||||
def test_non_racked_device(self):
|
def test_non_racked_device(self):
|
||||||
test = DeviceForm(data={
|
form = DeviceForm(data={
|
||||||
'name': 'test',
|
'name': 'New Device',
|
||||||
'device_role': get_id(DeviceRole, 'pdu'),
|
'device_role': DeviceRole.objects.first().pk,
|
||||||
'tenant': None,
|
'tenant': None,
|
||||||
'manufacturer': get_id(Manufacturer, 'servertech'),
|
'manufacturer': Manufacturer.objects.first().pk,
|
||||||
'device_type': get_id(DeviceType, 'cwg-24vym415c9'),
|
'device_type': DeviceType.objects.first().pk,
|
||||||
'site': get_id(Site, 'test1'),
|
'site': Site.objects.first().pk,
|
||||||
'rack': '1',
|
'rack': None,
|
||||||
'face': '',
|
'face': None,
|
||||||
'position': None,
|
'position': None,
|
||||||
'platform': None,
|
'platform': Platform.objects.first().pk,
|
||||||
'status': DeviceStatusChoices.STATUS_ACTIVE,
|
'status': DeviceStatusChoices.STATUS_ACTIVE,
|
||||||
})
|
})
|
||||||
self.assertTrue(test.is_valid())
|
self.assertTrue(form.is_valid())
|
||||||
self.assertTrue(test.save())
|
self.assertTrue(form.save())
|
||||||
|
|
||||||
def test_non_racked_device_with_face(self):
|
def test_non_racked_device_with_face_position(self):
|
||||||
test = DeviceForm(data={
|
form = DeviceForm(data={
|
||||||
'name': 'test',
|
'name': 'New Device',
|
||||||
'device_role': get_id(DeviceRole, 'pdu'),
|
'device_role': DeviceRole.objects.first().pk,
|
||||||
'tenant': None,
|
'tenant': None,
|
||||||
'manufacturer': get_id(Manufacturer, 'servertech'),
|
'manufacturer': Manufacturer.objects.first().pk,
|
||||||
'device_type': get_id(DeviceType, 'cwg-24vym415c9'),
|
'device_type': DeviceType.objects.first().pk,
|
||||||
'site': get_id(Site, 'test1'),
|
'site': Site.objects.first().pk,
|
||||||
'rack': '1',
|
'rack': None,
|
||||||
'face': DeviceFaceChoices.FACE_REAR,
|
'face': DeviceFaceChoices.FACE_REAR,
|
||||||
'position': None,
|
'position': 10,
|
||||||
'platform': None,
|
'platform': None,
|
||||||
'status': DeviceStatusChoices.STATUS_ACTIVE,
|
'status': DeviceStatusChoices.STATUS_ACTIVE,
|
||||||
})
|
})
|
||||||
self.assertTrue(test.is_valid())
|
self.assertFalse(form.is_valid())
|
||||||
self.assertTrue(test.save())
|
self.assertIn('face', form.errors)
|
||||||
|
self.assertIn('position', form.errors)
|
||||||
|
|
||||||
|
def test_initial_data_population(self):
|
||||||
|
device_type = DeviceType.objects.first()
|
||||||
|
cluster = Cluster.objects.first()
|
||||||
|
test = DeviceForm(initial={
|
||||||
|
'device_type': device_type.pk,
|
||||||
|
'device_role': DeviceRole.objects.first().pk,
|
||||||
|
'status': DeviceStatusChoices.STATUS_ACTIVE,
|
||||||
|
'site': Site.objects.first().pk,
|
||||||
|
'cluster': cluster.pk,
|
||||||
|
})
|
||||||
|
|
||||||
|
# Check that the initial value for the manufacturer is set automatically when assigning the device type
|
||||||
|
self.assertEqual(test.initial['manufacturer'], device_type.manufacturer.pk)
|
||||||
|
|
||||||
|
# Check that the initial value for the cluster group is set automatically when assigning the cluster
|
||||||
|
self.assertEqual(test.initial['cluster_group'], cluster.group.pk)
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -14,317 +14,328 @@ 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'),
|
||||||
|
|
||||||
# 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'),
|
||||||
|
|
||||||
# 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'),
|
||||||
|
|
||||||
# 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'),
|
||||||
|
|
||||||
# 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'),
|
||||||
|
|
||||||
# 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'),
|
||||||
|
|
||||||
# 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'),
|
||||||
|
|
||||||
# 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'),
|
||||||
|
|
||||||
# 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'),
|
# TODO: Bulk edit, rename, disconnect views for ConsolePorts
|
||||||
path(r'console-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='consoleport_connect', kwargs={'termination_a_type': ConsolePort}),
|
path('console-ports/delete/', views.ConsolePortBulkDeleteView.as_view(), name='consoleport_bulk_delete'),
|
||||||
path(r'console-ports/<int:pk>/edit/', views.ConsolePortEditView.as_view(), name='consoleport_edit'),
|
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>/delete/', views.ConsolePortDeleteView.as_view(), name='consoleport_delete'),
|
path('console-ports/<int:pk>/edit/', views.ConsolePortEditView.as_view(), name='consoleport_edit'),
|
||||||
path(r'console-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='consoleport_trace', kwargs={'model': ConsolePort}),
|
path('console-ports/<int:pk>/delete/', views.ConsolePortDeleteView.as_view(), name='consoleport_delete'),
|
||||||
path(r'console-ports/import/', views.ConsolePortBulkImportView.as_view(), name='consoleport_import'),
|
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'),
|
# TODO: Bulk edit, rename, disconnect views for PowerPorts
|
||||||
path(r'power-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='powerport_connect', kwargs={'termination_a_type': PowerPort}),
|
path('power-ports/delete/', views.PowerPortBulkDeleteView.as_view(), name='powerport_bulk_delete'),
|
||||||
path(r'power-ports/<int:pk>/edit/', views.PowerPortEditView.as_view(), name='powerport_edit'),
|
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>/delete/', views.PowerPortDeleteView.as_view(), name='powerport_delete'),
|
path('power-ports/<int:pk>/edit/', views.PowerPortEditView.as_view(), name='powerport_edit'),
|
||||||
path(r'power-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='powerport_trace', kwargs={'model': PowerPort}),
|
path('power-ports/<int:pk>/delete/', views.PowerPortDeleteView.as_view(), name='powerport_delete'),
|
||||||
path(r'power-ports/import/', views.PowerPortBulkImportView.as_view(), name='powerport_import'),
|
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}),
|
||||||
|
|
||||||
]
|
]
|
||||||
|
@ -30,6 +30,7 @@ from utilities.views import (
|
|||||||
)
|
)
|
||||||
from virtualization.models import VirtualMachine
|
from virtualization.models import VirtualMachine
|
||||||
from . import filters, forms, tables
|
from . import filters, forms, tables
|
||||||
|
from .choices import DeviceFaceChoices
|
||||||
from .models import (
|
from .models import (
|
||||||
Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||||
DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate,
|
DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate,
|
||||||
@ -376,16 +377,15 @@ class RackElevationListView(PermissionRequiredMixin, View):
|
|||||||
page = paginator.page(paginator.num_pages)
|
page = paginator.page(paginator.num_pages)
|
||||||
|
|
||||||
# Determine rack face
|
# Determine rack face
|
||||||
if request.GET.get('face') == '1':
|
rack_face = request.GET.get('face', DeviceFaceChoices.FACE_FRONT)
|
||||||
face_id = 1
|
if rack_face not in DeviceFaceChoices.values():
|
||||||
else:
|
rack_face = DeviceFaceChoices.FACE_FRONT
|
||||||
face_id = 0
|
|
||||||
|
|
||||||
return render(request, 'dcim/rack_elevation_list.html', {
|
return render(request, 'dcim/rack_elevation_list.html', {
|
||||||
'paginator': paginator,
|
'paginator': paginator,
|
||||||
'page': page,
|
'page': page,
|
||||||
'total_count': total_count,
|
'total_count': total_count,
|
||||||
'face_id': face_id,
|
'rack_face': rack_face,
|
||||||
'filter_form': forms.RackElevationFilterForm(request.GET),
|
'filter_form': forms.RackElevationFilterForm(request.GET),
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -705,8 +705,6 @@ class DeviceTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
|||||||
|
|
||||||
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 +717,21 @@ class ConsolePortTemplateEditView(PermissionRequiredMixin, ObjectEditView):
|
|||||||
model_form = forms.ConsolePortTemplateForm
|
model_form = forms.ConsolePortTemplateForm
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
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 +744,21 @@ class ConsoleServerPortTemplateEditView(PermissionRequiredMixin, ObjectEditView)
|
|||||||
model_form = forms.ConsoleServerPortTemplateForm
|
model_form = forms.ConsoleServerPortTemplateForm
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
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 +771,21 @@ class PowerPortTemplateEditView(PermissionRequiredMixin, ObjectEditView):
|
|||||||
model_form = forms.PowerPortTemplateForm
|
model_form = forms.PowerPortTemplateForm
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
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 +798,21 @@ class PowerOutletTemplateEditView(PermissionRequiredMixin, ObjectEditView):
|
|||||||
model_form = forms.PowerOutletTemplateForm
|
model_form = forms.PowerOutletTemplateForm
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
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
|
||||||
@ -814,7 +828,6 @@ class InterfaceTemplateEditView(PermissionRequiredMixin, ObjectEditView):
|
|||||||
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 +835,11 @@ 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
|
||||||
|
|
||||||
|
|
||||||
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 +852,21 @@ class FrontPortTemplateEditView(PermissionRequiredMixin, ObjectEditView):
|
|||||||
model_form = forms.FrontPortTemplateForm
|
model_form = forms.FrontPortTemplateForm
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
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 +879,21 @@ class RearPortTemplateEditView(PermissionRequiredMixin, ObjectEditView):
|
|||||||
model_form = forms.RearPortTemplateForm
|
model_form = forms.RearPortTemplateForm
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
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 +906,16 @@ class DeviceBayTemplateEditView(PermissionRequiredMixin, ObjectEditView):
|
|||||||
model_form = forms.DeviceBayTemplateForm
|
model_form = forms.DeviceBayTemplateForm
|
||||||
|
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
|
||||||
@ -1205,8 +1229,6 @@ class ConsolePortListView(PermissionRequiredMixin, ObjectListView):
|
|||||||
|
|
||||||
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
|
||||||
@ -1234,8 +1256,8 @@ class ConsolePortBulkImportView(PermissionRequiredMixin, BulkImportView):
|
|||||||
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'
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -1253,8 +1275,6 @@ class ConsoleServerPortListView(PermissionRequiredMixin, ObjectListView):
|
|||||||
|
|
||||||
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 +1302,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 +1321,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'
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -1321,8 +1340,6 @@ class PowerPortListView(PermissionRequiredMixin, ObjectListView):
|
|||||||
|
|
||||||
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
|
||||||
@ -1350,8 +1367,8 @@ class PowerPortBulkImportView(PermissionRequiredMixin, BulkImportView):
|
|||||||
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'
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -1369,8 +1386,6 @@ class PowerOutletListView(PermissionRequiredMixin, ObjectListView):
|
|||||||
|
|
||||||
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 +1413,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 +1432,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'
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -1473,8 +1487,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 +1515,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 +1534,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'
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -1542,8 +1553,6 @@ class FrontPortListView(PermissionRequiredMixin, ObjectListView):
|
|||||||
|
|
||||||
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 +1580,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 +1599,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'
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -1610,8 +1618,6 @@ class RearPortListView(PermissionRequiredMixin, ObjectListView):
|
|||||||
|
|
||||||
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 +1645,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 +1664,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'
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -1680,8 +1685,6 @@ class DeviceBayListView(PermissionRequiredMixin, ObjectListView):
|
|||||||
|
|
||||||
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 +1787,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'
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -2156,13 +2159,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):
|
||||||
|
@ -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',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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(),
|
||||||
|
@ -1,35 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"model": "extras.graph",
|
|
||||||
"pk": 1,
|
|
||||||
"fields": {
|
|
||||||
"type": 300,
|
|
||||||
"weight": 1000,
|
|
||||||
"name": "Site Test Graph",
|
|
||||||
"source": "http://localhost/na.png",
|
|
||||||
"link": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"model": "extras.graph",
|
|
||||||
"pk": 2,
|
|
||||||
"fields": {
|
|
||||||
"type": 200,
|
|
||||||
"weight": 1000,
|
|
||||||
"name": "Provider Test Graph",
|
|
||||||
"source": "http://localhost/provider_graph.png",
|
|
||||||
"link": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"model": "extras.graph",
|
|
||||||
"pk": 3,
|
|
||||||
"fields": {
|
|
||||||
"type": 100,
|
|
||||||
"weight": 1000,
|
|
||||||
"name": "Interface Test Graph",
|
|
||||||
"source": "http://localhost/interface_graph.png",
|
|
||||||
"link": ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
@ -1,18 +1,16 @@
|
|||||||
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 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, FilterChoiceField, JSONField, SlugField, StaticSelect2,
|
||||||
SlugField, StaticSelect2, BOOLEAN_WITH_BLANK_CHOICES,
|
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 +19,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 +88,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 +110,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 +129,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)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -254,8 +205,8 @@ 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 = {
|
widgets = {
|
||||||
'regions': APISelectMultiple(
|
'regions': APISelectMultiple(
|
||||||
@ -270,6 +221,12 @@ class ConfigContextForm(BootstrapMixin, forms.ModelForm):
|
|||||||
'platforms': APISelectMultiple(
|
'platforms': APISelectMultiple(
|
||||||
api_url="/api/dcim/platforms/"
|
api_url="/api/dcim/platforms/"
|
||||||
),
|
),
|
||||||
|
'cluster_groups': APISelectMultiple(
|
||||||
|
api_url="/api/virtualization/cluster-groups/"
|
||||||
|
),
|
||||||
|
'clusters': APISelectMultiple(
|
||||||
|
api_url="/api/virtualization/clusters/"
|
||||||
|
),
|
||||||
'tenant_groups': APISelectMultiple(
|
'tenant_groups': APISelectMultiple(
|
||||||
api_url="/api/tenancy/tenant-groups/"
|
api_url="/api/tenancy/tenant-groups/"
|
||||||
),
|
),
|
||||||
@ -340,6 +297,21 @@ class ConfigContextFilterForm(BootstrapMixin, forms.Form):
|
|||||||
value_field="slug",
|
value_field="slug",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
cluster_group = FilterChoiceField(
|
||||||
|
queryset=ClusterGroup.objects.all(),
|
||||||
|
to_field_name='slug',
|
||||||
|
widget=APISelectMultiple(
|
||||||
|
api_url="/api/virtualization/cluster-groups/",
|
||||||
|
value_field="slug",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
cluster_id = FilterChoiceField(
|
||||||
|
queryset=Cluster.objects.all(),
|
||||||
|
label='Cluster',
|
||||||
|
widget=APISelectMultiple(
|
||||||
|
api_url="/api/virtualization/clusters/",
|
||||||
|
)
|
||||||
|
)
|
||||||
tenant_group = FilterChoiceField(
|
tenant_group = FilterChoiceField(
|
||||||
queryset=TenantGroup.objects.all(),
|
queryset=TenantGroup.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
|
24
netbox/extras/migrations/0037_configcontexts_clusters.py
Normal file
24
netbox/extras/migrations/0037_configcontexts_clusters.py
Normal 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'),
|
||||||
|
),
|
||||||
|
]
|
@ -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='+',
|
||||||
|
@ -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),
|
||||||
|
@ -53,14 +53,15 @@ class ScriptVariable:
|
|||||||
# Initialize field attributes
|
# Initialize field attributes
|
||||||
if not hasattr(self, 'field_attrs'):
|
if not hasattr(self, 'field_attrs'):
|
||||||
self.field_attrs = {}
|
self.field_attrs = {}
|
||||||
if description:
|
|
||||||
self.field_attrs['help_text'] = description
|
|
||||||
if label:
|
if label:
|
||||||
self.field_attrs['label'] = label
|
self.field_attrs['label'] = label
|
||||||
|
if description:
|
||||||
|
self.field_attrs['help_text'] = description
|
||||||
if default:
|
if default:
|
||||||
self.field_attrs['initial'] = default
|
self.field_attrs['initial'] = default
|
||||||
if required:
|
self.field_attrs['required'] = required
|
||||||
self.field_attrs['required'] = True
|
|
||||||
|
# Initialize the list of optional validators if none have already been defined
|
||||||
if 'validators' not in self.field_attrs:
|
if 'validators' not in self.field_attrs:
|
||||||
self.field_attrs['validators'] = []
|
self.field_attrs['validators'] = []
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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]}
|
||||||
|
@ -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 StandardTestCases, TestCase
|
||||||
|
|
||||||
|
|
||||||
class TagTestCase(TestCase):
|
class TagTestCase(StandardTestCases.Views):
|
||||||
|
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(StandardTestCases.Views):
|
||||||
|
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)
|
||||||
|
@ -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/<slug:slug>/', views.TagView.as_view(), name='tag'),
|
path('tags/<str:slug>/', views.TagView.as_view(), name='tag'),
|
||||||
path(r'tags/<slug:slug>/edit/', views.TagEditView.as_view(), name='tag_edit'),
|
path('tags/<str:slug>/edit/', views.TagEditView.as_view(), name='tag_edit'),
|
||||||
path(r'tags/<slug:slug>/delete/', views.TagDeleteView.as_view(), name='tag_delete'),
|
path('tags/<str:slug>/delete/', views.TagDeleteView.as_view(), name='tag_delete'),
|
||||||
path(r'tags/<slug: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'),
|
||||||
|
|
||||||
]
|
]
|
||||||
|
@ -37,7 +37,8 @@ class TagListView(PermissionRequiredMixin, ObjectListView):
|
|||||||
template_name = 'extras/tag_list.html'
|
template_name = 'extras/tag_list.html'
|
||||||
|
|
||||||
|
|
||||||
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):
|
||||||
|
@ -3,6 +3,7 @@ import hashlib
|
|||||||
import hmac
|
import hmac
|
||||||
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
from extras.models import Webhook
|
from extras.models import Webhook
|
||||||
from utilities.api import get_serializer_for_model
|
from utilities.api import get_serializer_for_model
|
||||||
@ -62,7 +63,7 @@ def enqueue_webhooks(instance, user, request_id, action):
|
|||||||
serializer.data,
|
serializer.data,
|
||||||
instance._meta.model_name,
|
instance._meta.model_name,
|
||||||
action,
|
action,
|
||||||
str(datetime.datetime.now()),
|
str(timezone.now()),
|
||||||
user.username,
|
user.username,
|
||||||
request_id
|
request_id
|
||||||
)
|
)
|
||||||
|
@ -237,7 +237,7 @@ class AvailableIPSerializer(serializers.Serializer):
|
|||||||
# Services
|
# Services
|
||||||
#
|
#
|
||||||
|
|
||||||
class ServiceSerializer(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)
|
||||||
@ -247,10 +247,11 @@ class ServiceSerializer(CustomFieldModelSerializer):
|
|||||||
required=False,
|
required=False,
|
||||||
many=True
|
many=True
|
||||||
)
|
)
|
||||||
|
tags = TagListSerializerField(required=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Service
|
model = Service
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'device', 'virtual_machine', 'name', 'port', 'protocol', 'ipaddresses', 'description',
|
'id', 'device', 'virtual_machine', 'name', 'port', 'protocol', 'ipaddresses', 'description', 'tags',
|
||||||
'custom_fields', 'created', 'last_updated',
|
'custom_fields', 'created', 'last_updated',
|
||||||
]
|
]
|
||||||
|
@ -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
|
||||||
|
@ -4,10 +4,34 @@ from .choices import IPAddressRoleChoices
|
|||||||
BGP_ASN_MIN = 1
|
BGP_ASN_MIN = 1
|
||||||
BGP_ASN_MAX = 2**32 - 1
|
BGP_ASN_MAX = 2**32 - 1
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# IP addresses
|
# VRFs
|
||||||
#
|
#
|
||||||
|
|
||||||
|
# Per RFC 4364 section 4.2, a route distinguisher may be encoded as one of the following:
|
||||||
|
# * Type 0 (16-bit AS number : 32-bit integer)
|
||||||
|
# * Type 1 (32-bit IPv4 address : 16-bit integer)
|
||||||
|
# * Type 2 (32-bit AS number : 16-bit integer)
|
||||||
|
# 21 characters are sufficient to convey the longest possible string value (255.255.255.255:65535)
|
||||||
|
VRF_RD_MAX_LENGTH = 21
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Prefixes
|
||||||
|
#
|
||||||
|
|
||||||
|
PREFIX_LENGTH_MIN = 1
|
||||||
|
PREFIX_LENGTH_MAX = 127 # IPv6
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# IPAddresses
|
||||||
|
#
|
||||||
|
|
||||||
|
IPADDRESS_MASK_LENGTH_MIN = 1
|
||||||
|
IPADDRESS_MASK_LENGTH_MAX = 128 # IPv6
|
||||||
|
|
||||||
IPADDRESS_ROLES_NONUNIQUE = (
|
IPADDRESS_ROLES_NONUNIQUE = (
|
||||||
# IPAddress roles which are exempt from unique address enforcement
|
# IPAddress roles which are exempt from unique address enforcement
|
||||||
IPAddressRoleChoices.ROLE_ANYCAST,
|
IPAddressRoleChoices.ROLE_ANYCAST,
|
||||||
@ -17,3 +41,21 @@ IPADDRESS_ROLES_NONUNIQUE = (
|
|||||||
IPAddressRoleChoices.ROLE_GLBP,
|
IPAddressRoleChoices.ROLE_GLBP,
|
||||||
IPAddressRoleChoices.ROLE_CARP,
|
IPAddressRoleChoices.ROLE_CARP,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# VLANs
|
||||||
|
#
|
||||||
|
|
||||||
|
# 12-bit VLAN ID (values 0 and 4095 are reserved)
|
||||||
|
VLAN_VID_MIN = 1
|
||||||
|
VLAN_VID_MAX = 4094
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Services
|
||||||
|
#
|
||||||
|
|
||||||
|
# 16-bit port number
|
||||||
|
SERVICE_PORT_MIN = 1
|
||||||
|
SERVICE_PORT_MAX = 65535
|
||||||
|
@ -1,329 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"model": "ipam.rir",
|
|
||||||
"pk": 1,
|
|
||||||
"fields": {
|
|
||||||
"name": "RFC1918",
|
|
||||||
"slug": "rfc1918"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"model": "ipam.aggregate",
|
|
||||||
"pk": 1,
|
|
||||||
"fields": {
|
|
||||||
"created": "2016-06-23",
|
|
||||||
"last_updated": "2016-06-23T03:19:56.521Z",
|
|
||||||
"family": 4,
|
|
||||||
"prefix": "10.0.0.0/8",
|
|
||||||
"rir": 1,
|
|
||||||
"date_added": null,
|
|
||||||
"description": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"model": "ipam.role",
|
|
||||||
"pk": 1,
|
|
||||||
"fields": {
|
|
||||||
"name": "Lab Network",
|
|
||||||
"slug": "lab-network",
|
|
||||||
"weight": 1000
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"model": "ipam.prefix",
|
|
||||||
"pk": 1,
|
|
||||||
"fields": {
|
|
||||||
"created": "2016-06-23",
|
|
||||||
"last_updated": "2016-06-23T03:19:56.521Z",
|
|
||||||
"family": 4,
|
|
||||||
"prefix": "10.1.1.0/24",
|
|
||||||
"site": 1,
|
|
||||||
"vrf": null,
|
|
||||||
"vlan": null,
|
|
||||||
"status": "active",
|
|
||||||
"role": 1,
|
|
||||||
"description": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"model": "ipam.prefix",
|
|
||||||
"pk": 2,
|
|
||||||
"fields": {
|
|
||||||
"created": "2016-06-23",
|
|
||||||
"last_updated": "2016-06-23T03:19:56.521Z",
|
|
||||||
"family": 4,
|
|
||||||
"prefix": "10.0.255.0/24",
|
|
||||||
"site": 1,
|
|
||||||
"vrf": null,
|
|
||||||
"vlan": null,
|
|
||||||
"status": "active",
|
|
||||||
"role": 1,
|
|
||||||
"description": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"model": "ipam.ipaddress",
|
|
||||||
"pk": 1,
|
|
||||||
"fields": {
|
|
||||||
"created": "2016-06-23",
|
|
||||||
"last_updated": "2016-06-23T03:19:56.521Z",
|
|
||||||
"family": 4,
|
|
||||||
"address": "10.0.255.1/32",
|
|
||||||
"vrf": null,
|
|
||||||
"interface_id": 3,
|
|
||||||
"nat_inside": null,
|
|
||||||
"description": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"model": "ipam.ipaddress",
|
|
||||||
"pk": 2,
|
|
||||||
"fields": {
|
|
||||||
"created": "2016-06-23",
|
|
||||||
"last_updated": "2016-06-23T03:19:56.521Z",
|
|
||||||
"family": 4,
|
|
||||||
"address": "169.254.254.1/31",
|
|
||||||
"vrf": null,
|
|
||||||
"interface_id": 4,
|
|
||||||
"nat_inside": null,
|
|
||||||
"description": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"model": "ipam.ipaddress",
|
|
||||||
"pk": 3,
|
|
||||||
"fields": {
|
|
||||||
"created": "2016-06-23",
|
|
||||||
"last_updated": "2016-06-23T03:19:56.521Z",
|
|
||||||
"family": 4,
|
|
||||||
"address": "10.0.255.2/32",
|
|
||||||
"vrf": null,
|
|
||||||
"interface_id": 185,
|
|
||||||
"nat_inside": null,
|
|
||||||
"description": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"model": "ipam.ipaddress",
|
|
||||||
"pk": 4,
|
|
||||||
"fields": {
|
|
||||||
"created": "2016-06-23",
|
|
||||||
"last_updated": "2016-06-23T03:19:56.521Z",
|
|
||||||
"family": 4,
|
|
||||||
"address": "169.254.1.1/31",
|
|
||||||
"vrf": null,
|
|
||||||
"interface_id": 213,
|
|
||||||
"nat_inside": null,
|
|
||||||
"description": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"model": "ipam.ipaddress",
|
|
||||||
"pk": 5,
|
|
||||||
"fields": {
|
|
||||||
"created": "2016-06-23",
|
|
||||||
"last_updated": "2016-06-23T03:19:56.521Z",
|
|
||||||
"family": 4,
|
|
||||||
"address": "10.0.254.1/24",
|
|
||||||
"vrf": null,
|
|
||||||
"interface_id": 12,
|
|
||||||
"nat_inside": null,
|
|
||||||
"description": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"model": "ipam.ipaddress",
|
|
||||||
"pk": 8,
|
|
||||||
"fields": {
|
|
||||||
"created": "2016-06-23",
|
|
||||||
"last_updated": "2016-06-23T03:19:56.521Z",
|
|
||||||
"family": 4,
|
|
||||||
"address": "10.15.21.1/31",
|
|
||||||
"vrf": null,
|
|
||||||
"interface_id": 218,
|
|
||||||
"nat_inside": null,
|
|
||||||
"description": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"model": "ipam.ipaddress",
|
|
||||||
"pk": 9,
|
|
||||||
"fields": {
|
|
||||||
"created": "2016-06-23",
|
|
||||||
"last_updated": "2016-06-23T03:19:56.521Z",
|
|
||||||
"family": 4,
|
|
||||||
"address": "10.15.21.2/31",
|
|
||||||
"vrf": null,
|
|
||||||
"interface_id": 9,
|
|
||||||
"nat_inside": null,
|
|
||||||
"description": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"model": "ipam.ipaddress",
|
|
||||||
"pk": 10,
|
|
||||||
"fields": {
|
|
||||||
"created": "2016-06-23",
|
|
||||||
"last_updated": "2016-06-23T03:19:56.521Z",
|
|
||||||
"family": 4,
|
|
||||||
"address": "10.15.22.1/31",
|
|
||||||
"vrf": null,
|
|
||||||
"interface_id": 8,
|
|
||||||
"nat_inside": null,
|
|
||||||
"description": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"model": "ipam.ipaddress",
|
|
||||||
"pk": 11,
|
|
||||||
"fields": {
|
|
||||||
"created": "2016-06-23",
|
|
||||||
"last_updated": "2016-06-23T03:19:56.521Z",
|
|
||||||
"family": 4,
|
|
||||||
"address": "10.15.20.1/31",
|
|
||||||
"vrf": null,
|
|
||||||
"interface_id": 7,
|
|
||||||
"nat_inside": null,
|
|
||||||
"description": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"model": "ipam.ipaddress",
|
|
||||||
"pk": 12,
|
|
||||||
"fields": {
|
|
||||||
"created": "2016-06-23",
|
|
||||||
"last_updated": "2016-06-23T03:19:56.521Z",
|
|
||||||
"family": 4,
|
|
||||||
"address": "10.16.20.1/31",
|
|
||||||
"vrf": null,
|
|
||||||
"interface_id": 216,
|
|
||||||
"nat_inside": null,
|
|
||||||
"description": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"model": "ipam.ipaddress",
|
|
||||||
"pk": 13,
|
|
||||||
"fields": {
|
|
||||||
"created": "2016-06-23",
|
|
||||||
"last_updated": "2016-06-23T03:19:56.521Z",
|
|
||||||
"family": 4,
|
|
||||||
"address": "10.15.22.2/31",
|
|
||||||
"vrf": null,
|
|
||||||
"interface_id": 206,
|
|
||||||
"nat_inside": null,
|
|
||||||
"description": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"model": "ipam.ipaddress",
|
|
||||||
"pk": 14,
|
|
||||||
"fields": {
|
|
||||||
"created": "2016-06-23",
|
|
||||||
"last_updated": "2016-06-23T03:19:56.521Z",
|
|
||||||
"family": 4,
|
|
||||||
"address": "10.16.22.1/31",
|
|
||||||
"vrf": null,
|
|
||||||
"interface_id": 217,
|
|
||||||
"nat_inside": null,
|
|
||||||
"description": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"model": "ipam.ipaddress",
|
|
||||||
"pk": 15,
|
|
||||||
"fields": {
|
|
||||||
"created": "2016-06-23",
|
|
||||||
"last_updated": "2016-06-23T03:19:56.521Z",
|
|
||||||
"family": 4,
|
|
||||||
"address": "10.16.22.2/31",
|
|
||||||
"vrf": null,
|
|
||||||
"interface_id": 205,
|
|
||||||
"nat_inside": null,
|
|
||||||
"description": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"model": "ipam.ipaddress",
|
|
||||||
"pk": 16,
|
|
||||||
"fields": {
|
|
||||||
"created": "2016-06-23",
|
|
||||||
"last_updated": "2016-06-23T03:19:56.521Z",
|
|
||||||
"family": 4,
|
|
||||||
"address": "10.16.20.2/31",
|
|
||||||
"vrf": null,
|
|
||||||
"interface_id": 211,
|
|
||||||
"nat_inside": null,
|
|
||||||
"description": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"model": "ipam.ipaddress",
|
|
||||||
"pk": 17,
|
|
||||||
"fields": {
|
|
||||||
"created": "2016-06-23",
|
|
||||||
"last_updated": "2016-06-23T03:19:56.521Z",
|
|
||||||
"family": 4,
|
|
||||||
"address": "10.15.22.2/31",
|
|
||||||
"vrf": null,
|
|
||||||
"interface_id": 212,
|
|
||||||
"nat_inside": null,
|
|
||||||
"description": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"model": "ipam.ipaddress",
|
|
||||||
"pk": 19,
|
|
||||||
"fields": {
|
|
||||||
"created": "2016-06-23",
|
|
||||||
"last_updated": "2016-06-23T03:19:56.521Z",
|
|
||||||
"family": 4,
|
|
||||||
"address": "10.0.254.2/32",
|
|
||||||
"vrf": null,
|
|
||||||
"interface_id": 188,
|
|
||||||
"nat_inside": null,
|
|
||||||
"description": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"model": "ipam.ipaddress",
|
|
||||||
"pk": 20,
|
|
||||||
"fields": {
|
|
||||||
"created": "2016-06-23",
|
|
||||||
"last_updated": "2016-06-23T03:19:56.521Z",
|
|
||||||
"family": 4,
|
|
||||||
"address": "169.254.1.1/31",
|
|
||||||
"vrf": null,
|
|
||||||
"interface_id": 200,
|
|
||||||
"nat_inside": null,
|
|
||||||
"description": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"model": "ipam.ipaddress",
|
|
||||||
"pk": 21,
|
|
||||||
"fields": {
|
|
||||||
"created": "2016-06-23",
|
|
||||||
"last_updated": "2016-06-23T03:19:56.521Z",
|
|
||||||
"family": 4,
|
|
||||||
"address": "169.254.1.2/31",
|
|
||||||
"vrf": null,
|
|
||||||
"interface_id": 194,
|
|
||||||
"nat_inside": null,
|
|
||||||
"description": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"model": "ipam.vlan",
|
|
||||||
"pk": 1,
|
|
||||||
"fields": {
|
|
||||||
"created": "2016-06-23",
|
|
||||||
"last_updated": "2016-06-23T03:19:56.521Z",
|
|
||||||
"site": 1,
|
|
||||||
"vid": 999,
|
|
||||||
"name": "TEST",
|
|
||||||
"status": "active",
|
|
||||||
"role": 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
@ -4,33 +4,36 @@ 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, ChainedModelChoiceField,
|
||||||
CSVChoiceField, DatePicker, ExpandableIPAddressField, FilterChoiceField, FlexibleModelChoiceField, ReturnURLForm,
|
CSVChoiceField, DatePicker, ExpandableIPAddressField, FilterChoiceField, FlexibleModelChoiceField, ReturnURLForm,
|
||||||
SlugField, StaticSelect2, StaticSelect2Multiple, BOOLEAN_WITH_BLANK_CHOICES
|
SlugField, StaticSelect2, StaticSelect2Multiple, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES
|
||||||
)
|
)
|
||||||
from virtualization.models import VirtualMachine
|
from virtualization.models import VirtualMachine
|
||||||
|
from .constants import *
|
||||||
from .choices import *
|
from .choices import *
|
||||||
from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
|
from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
|
||||||
|
|
||||||
IP_FAMILY_CHOICES = [
|
|
||||||
('', 'All'),
|
|
||||||
(4, 'IPv4'),
|
|
||||||
(6, 'IPv6'),
|
|
||||||
]
|
|
||||||
|
|
||||||
PREFIX_MASK_LENGTH_CHOICES = add_blank_choice([(i, i) for i in range(1, 128)])
|
PREFIX_MASK_LENGTH_CHOICES = add_blank_choice([
|
||||||
IPADDRESS_MASK_LENGTH_CHOICES = add_blank_choice([(i, i) for i in range(1, 129)])
|
(i, i) for i in range(PREFIX_LENGTH_MIN, PREFIX_LENGTH_MAX + 1)
|
||||||
|
])
|
||||||
|
|
||||||
|
IPADDRESS_MASK_LENGTH_CHOICES = add_blank_choice([
|
||||||
|
(i, i) for i in range(IPADDRESS_MASK_LENGTH_MIN, IPADDRESS_MASK_LENGTH_MAX + 1)
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# VRFs
|
# VRFs
|
||||||
#
|
#
|
||||||
|
|
||||||
class VRFForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
class VRFForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||||
tags = TagField(
|
tags = TagField(
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
@ -48,7 +51,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,
|
||||||
@ -102,6 +105,7 @@ class VRFFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
|
|||||||
required=False,
|
required=False,
|
||||||
label='Search'
|
label='Search'
|
||||||
)
|
)
|
||||||
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -143,7 +147,7 @@ class RIRFilterForm(BootstrapMixin, forms.Form):
|
|||||||
# Aggregates
|
# Aggregates
|
||||||
#
|
#
|
||||||
|
|
||||||
class AggregateForm(BootstrapMixin, CustomFieldForm):
|
class AggregateForm(BootstrapMixin, CustomFieldModelForm):
|
||||||
tags = TagField(
|
tags = TagField(
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
@ -165,7 +169,7 @@ class AggregateForm(BootstrapMixin, CustomFieldForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
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',
|
||||||
@ -218,7 +222,7 @@ class AggregateFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
|||||||
)
|
)
|
||||||
family = forms.ChoiceField(
|
family = forms.ChoiceField(
|
||||||
required=False,
|
required=False,
|
||||||
choices=IP_FAMILY_CHOICES,
|
choices=add_blank_choice(IPAddressFamilyChoices),
|
||||||
label='Address family',
|
label='Address family',
|
||||||
widget=StaticSelect2()
|
widget=StaticSelect2()
|
||||||
)
|
)
|
||||||
@ -231,6 +235,7 @@ class AggregateFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
|||||||
value_field="slug",
|
value_field="slug",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -262,7 +267,7 @@ class RoleCSVForm(forms.ModelForm):
|
|||||||
# Prefixes
|
# Prefixes
|
||||||
#
|
#
|
||||||
|
|
||||||
class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||||
site = forms.ModelChoiceField(
|
site = forms.ModelChoiceField(
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
@ -340,7 +345,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',
|
||||||
@ -450,8 +455,8 @@ class PrefixBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
prefix_length = forms.IntegerField(
|
prefix_length = forms.IntegerField(
|
||||||
min_value=1,
|
min_value=PREFIX_LENGTH_MIN,
|
||||||
max_value=127,
|
max_value=PREFIX_LENGTH_MAX,
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
tenant = forms.ModelChoiceField(
|
tenant = forms.ModelChoiceField(
|
||||||
@ -510,7 +515,7 @@ class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm)
|
|||||||
)
|
)
|
||||||
family = forms.ChoiceField(
|
family = forms.ChoiceField(
|
||||||
required=False,
|
required=False,
|
||||||
choices=IP_FAMILY_CHOICES,
|
choices=add_blank_choice(IPAddressFamilyChoices),
|
||||||
label='Address family',
|
label='Address family',
|
||||||
widget=StaticSelect2()
|
widget=StaticSelect2()
|
||||||
)
|
)
|
||||||
@ -577,13 +582,14 @@ 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
|
||||||
@ -634,6 +640,17 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm)
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
nat_vrf = forms.ModelChoiceField(
|
||||||
|
queryset=VRF.objects.all(),
|
||||||
|
required=False,
|
||||||
|
label='VRF',
|
||||||
|
widget=APISelect(
|
||||||
|
api_url="/api/ipam/vrfs/",
|
||||||
|
filter_for={
|
||||||
|
'nat_inside': 'vrf_id'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
nat_inside = ChainedModelChoiceField(
|
nat_inside = ChainedModelChoiceField(
|
||||||
queryset=IPAddress.objects.all(),
|
queryset=IPAddress.objects.all(),
|
||||||
chains=(
|
chains=(
|
||||||
@ -739,7 +756,7 @@ class IPAddressBulkCreateForm(BootstrapMixin, forms.Form):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class IPAddressBulkAddForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
class IPAddressBulkAddForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = IPAddress
|
model = IPAddress
|
||||||
@ -759,7 +776,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',
|
||||||
@ -896,8 +913,8 @@ class IPAddressBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEd
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
mask_length = forms.IntegerField(
|
mask_length = forms.IntegerField(
|
||||||
min_value=1,
|
min_value=IPADDRESS_MASK_LENGTH_MIN,
|
||||||
max_value=128,
|
max_value=IPADDRESS_MASK_LENGTH_MAX,
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
tenant = forms.ModelChoiceField(
|
tenant = forms.ModelChoiceField(
|
||||||
@ -969,7 +986,7 @@ class IPAddressFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterFo
|
|||||||
)
|
)
|
||||||
family = forms.ChoiceField(
|
family = forms.ChoiceField(
|
||||||
required=False,
|
required=False,
|
||||||
choices=IP_FAMILY_CHOICES,
|
choices=add_blank_choice(IPAddressFamilyChoices),
|
||||||
label='Address family',
|
label='Address family',
|
||||||
widget=StaticSelect2()
|
widget=StaticSelect2()
|
||||||
)
|
)
|
||||||
@ -1005,6 +1022,7 @@ class IPAddressFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterFo
|
|||||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -1075,7 +1093,7 @@ class VLANGroupFilterForm(BootstrapMixin, forms.Form):
|
|||||||
# VLANs
|
# VLANs
|
||||||
#
|
#
|
||||||
|
|
||||||
class VLANForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
class VLANForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||||
site = forms.ModelChoiceField(
|
site = forms.ModelChoiceField(
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
@ -1123,7 +1141,7 @@ class VLANForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
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,
|
||||||
@ -1292,16 +1310,17 @@ class VLANFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
|
|||||||
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=1,
|
min_value=SERVICE_PORT_MIN,
|
||||||
max_value=65535
|
max_value=SERVICE_PORT_MAX
|
||||||
)
|
)
|
||||||
tags = TagField(
|
tags = TagField(
|
||||||
required=False
|
required=False
|
||||||
@ -1352,6 +1371,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):
|
||||||
@ -1378,5 +1398,5 @@ class ServiceBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
nullable_fields = [
|
nullable_fields = [
|
||||||
'site', 'tenant', 'role', 'description',
|
'description',
|
||||||
]
|
]
|
||||||
|
@ -2,10 +2,10 @@ from django.db import migrations, models
|
|||||||
|
|
||||||
|
|
||||||
IPADDRESS_STATUS_CHOICES = (
|
IPADDRESS_STATUS_CHOICES = (
|
||||||
(0, 'container'),
|
|
||||||
(1, 'active'),
|
(1, 'active'),
|
||||||
(2, 'reserved'),
|
(2, 'reserved'),
|
||||||
(3, 'deprecated'),
|
(3, 'deprecated'),
|
||||||
|
(5, 'dhcp'),
|
||||||
)
|
)
|
||||||
|
|
||||||
IPADDRESS_ROLE_CHOICES = (
|
IPADDRESS_ROLE_CHOICES = (
|
||||||
|
21
netbox/ipam/migrations/0034_fix_ipaddress_status_dhcp.py
Normal file
21
netbox/ipam/migrations/0034_fix_ipaddress_status_dhcp.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
def ipaddress_status_dhcp_to_slug(apps, schema_editor):
|
||||||
|
IPAddress = apps.get_model('ipam', 'IPAddress')
|
||||||
|
IPAddress.objects.filter(status='5').update(status='dhcp')
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('ipam', '0033_deterministic_ordering'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
# Fixes a missed integer substitution from #3569; see bug #4027. The original migration has also been fixed,
|
||||||
|
# so this can be omitted when squashing in the future.
|
||||||
|
migrations.RunPython(
|
||||||
|
code=ipaddress_status_dhcp_to_slug
|
||||||
|
),
|
||||||
|
]
|
@ -14,7 +14,7 @@ from utilities.models import ChangeLoggedModel
|
|||||||
from utilities.utils import serialize_object
|
from utilities.utils import serialize_object
|
||||||
from virtualization.models import VirtualMachine
|
from virtualization.models import VirtualMachine
|
||||||
from .choices import *
|
from .choices import *
|
||||||
from .constants import IPADDRESS_ROLES_NONUNIQUE
|
from .constants import *
|
||||||
from .fields import IPNetworkField, IPAddressField
|
from .fields import IPNetworkField, IPAddressField
|
||||||
from .managers import IPAddressManager
|
from .managers import IPAddressManager
|
||||||
from .querysets import PrefixQuerySet
|
from .querysets import PrefixQuerySet
|
||||||
@ -44,7 +44,7 @@ class VRF(ChangeLoggedModel, CustomFieldModel):
|
|||||||
max_length=50
|
max_length=50
|
||||||
)
|
)
|
||||||
rd = models.CharField(
|
rd = models.CharField(
|
||||||
max_length=21,
|
max_length=VRF_RD_MAX_LENGTH,
|
||||||
unique=True,
|
unique=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
@ -1006,7 +1006,7 @@ class Service(ChangeLoggedModel, CustomFieldModel):
|
|||||||
choices=ServiceProtocolChoices
|
choices=ServiceProtocolChoices
|
||||||
)
|
)
|
||||||
port = models.PositiveIntegerField(
|
port = models.PositiveIntegerField(
|
||||||
validators=[MinValueValidator(1), MaxValueValidator(65535)],
|
validators=[MinValueValidator(SERVICE_PORT_MIN), MaxValueValidator(SERVICE_PORT_MAX)],
|
||||||
verbose_name='Port number'
|
verbose_name='Port number'
|
||||||
)
|
)
|
||||||
ipaddresses = models.ManyToManyField(
|
ipaddresses = models.ManyToManyField(
|
||||||
|
@ -1064,6 +1064,7 @@ class ServiceTest(APITestCase):
|
|||||||
'name': 'Test Service 4',
|
'name': 'Test Service 4',
|
||||||
'protocol': ServiceProtocolChoices.PROTOCOL_TCP,
|
'protocol': ServiceProtocolChoices.PROTOCOL_TCP,
|
||||||
'port': 4,
|
'port': 4,
|
||||||
|
'tags': ['Foo', 'Bar'],
|
||||||
}
|
}
|
||||||
|
|
||||||
url = reverse('ipam-api:service-list')
|
url = reverse('ipam-api:service-list')
|
||||||
@ -1076,6 +1077,8 @@ class ServiceTest(APITestCase):
|
|||||||
self.assertEqual(service4.name, data['name'])
|
self.assertEqual(service4.name, data['name'])
|
||||||
self.assertEqual(service4.protocol, data['protocol'])
|
self.assertEqual(service4.protocol, data['protocol'])
|
||||||
self.assertEqual(service4.port, data['port'])
|
self.assertEqual(service4.port, data['port'])
|
||||||
|
tags = [tag.name for tag in service4.tags.all()]
|
||||||
|
self.assertEqual(sorted(tags), sorted(data['tags']))
|
||||||
|
|
||||||
def test_create_service_bulk(self):
|
def test_create_service_bulk(self):
|
||||||
|
|
||||||
|
@ -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 StandardTestCases
|
||||||
|
|
||||||
|
|
||||||
class VRFTestCase(TestCase):
|
class VRFTestCase(StandardTestCases.Views):
|
||||||
|
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,39 @@ 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(StandardTestCases.Views):
|
||||||
|
model = RIR
|
||||||
|
|
||||||
def setUp(self):
|
# Disable inapplicable tests
|
||||||
user = create_test_user(
|
test_get_object = None
|
||||||
permissions=[
|
test_delete_object = None
|
||||||
'ipam.view_rir',
|
test_bulk_edit_objects = None
|
||||||
'ipam.add_rir',
|
|
||||||
]
|
@classmethod
|
||||||
)
|
def setUpTestData(cls):
|
||||||
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 +60,71 @@ 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(StandardTestCases.Views):
|
||||||
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(StandardTestCases.Views):
|
||||||
|
model = Role
|
||||||
|
|
||||||
def setUp(self):
|
# Disable inapplicable tests
|
||||||
user = create_test_user(
|
test_get_object = None
|
||||||
permissions=[
|
test_delete_object = None
|
||||||
'ipam.view_role',
|
test_bulk_edit_objects = None
|
||||||
'ipam.add_role',
|
|
||||||
]
|
@classmethod
|
||||||
)
|
def setUpTestData(cls):
|
||||||
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 +132,140 @@ 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(StandardTestCases.Views):
|
||||||
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(StandardTestCases.Views):
|
||||||
|
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(StandardTestCases.Views):
|
||||||
|
model = VLANGroup
|
||||||
|
|
||||||
def setUp(self):
|
# Disable inapplicable tests
|
||||||
user = create_test_user(
|
test_get_object = None
|
||||||
permissions=[
|
test_delete_object = None
|
||||||
'ipam.view_vlangroup',
|
test_bulk_edit_objects = None
|
||||||
'ipam.add_vlangroup',
|
|
||||||
]
|
|
||||||
)
|
|
||||||
self.client = Client()
|
|
||||||
self.client.force_login(user)
|
|
||||||
|
|
||||||
site = Site(name='Site 1', slug='site-1')
|
@classmethod
|
||||||
site.save()
|
def setUpTestData(cls):
|
||||||
|
|
||||||
|
site = Site.objects.create(name='Site 1', slug='site-1')
|
||||||
|
|
||||||
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 +273,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(StandardTestCases.Views):
|
||||||
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(StandardTestCases.Views):
|
||||||
|
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 +370,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)
|
|
||||||
|
@ -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}),
|
||||||
|
|
||||||
]
|
]
|
||||||
|
@ -15,6 +15,7 @@ from utilities.views import (
|
|||||||
from virtualization.models import VirtualMachine
|
from virtualization.models import VirtualMachine
|
||||||
from . import filters, forms, tables
|
from . import filters, forms, tables
|
||||||
from .choices import *
|
from .choices import *
|
||||||
|
from .constants import *
|
||||||
from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
|
from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
|
||||||
|
|
||||||
|
|
||||||
@ -86,23 +87,20 @@ def add_available_vlans(vlan_group, vlans):
|
|||||||
"""
|
"""
|
||||||
Create fake records for all gaps between used VLANs
|
Create fake records for all gaps between used VLANs
|
||||||
"""
|
"""
|
||||||
MIN_VLAN = 1
|
|
||||||
MAX_VLAN = 4094
|
|
||||||
|
|
||||||
if not vlans:
|
if not vlans:
|
||||||
return [{'vid': MIN_VLAN, 'available': MAX_VLAN - MIN_VLAN + 1}]
|
return [{'vid': VLAN_VID_MIN, 'available': VLAN_VID_MAX - VLAN_VID_MIN + 1}]
|
||||||
|
|
||||||
prev_vid = MAX_VLAN
|
prev_vid = VLAN_VID_MAX
|
||||||
new_vlans = []
|
new_vlans = []
|
||||||
for vlan in vlans:
|
for vlan in vlans:
|
||||||
if vlan.vid - prev_vid > 1:
|
if vlan.vid - prev_vid > 1:
|
||||||
new_vlans.append({'vid': prev_vid + 1, 'available': vlan.vid - prev_vid - 1})
|
new_vlans.append({'vid': prev_vid + 1, 'available': vlan.vid - prev_vid - 1})
|
||||||
prev_vid = vlan.vid
|
prev_vid = vlan.vid
|
||||||
|
|
||||||
if vlans[0].vid > MIN_VLAN:
|
if vlans[0].vid > VLAN_VID_MIN:
|
||||||
new_vlans.append({'vid': MIN_VLAN, 'available': vlans[0].vid - MIN_VLAN})
|
new_vlans.append({'vid': VLAN_VID_MIN, 'available': vlans[0].vid - VLAN_VID_MIN})
|
||||||
if prev_vid < MAX_VLAN:
|
if prev_vid < VLAN_VID_MAX:
|
||||||
new_vlans.append({'vid': prev_vid + 1, 'available': MAX_VLAN - prev_vid})
|
new_vlans.append({'vid': prev_vid + 1, 'available': VLAN_VID_MAX - prev_vid})
|
||||||
|
|
||||||
vlans = list(vlans) + new_vlans
|
vlans = list(vlans) + new_vlans
|
||||||
vlans.sort(key=lambda v: v.vid if type(v) == VLAN else v['vid'])
|
vlans.sort(key=lambda v: v.vid if type(v) == VLAN else v['vid'])
|
||||||
|
@ -12,7 +12,7 @@ from django.core.exceptions import ImproperlyConfigured
|
|||||||
# Environment setup
|
# Environment setup
|
||||||
#
|
#
|
||||||
|
|
||||||
VERSION = '2.7.3-dev'
|
VERSION = '2.7.5-dev'
|
||||||
|
|
||||||
# Hostname
|
# Hostname
|
||||||
HOSTNAME = platform.node()
|
HOSTNAME = platform.node()
|
||||||
@ -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', [])
|
||||||
@ -503,6 +504,7 @@ SWAGGER_SETTINGS = {
|
|||||||
'utilities.custom_inspectors.IdInFilterInspector',
|
'utilities.custom_inspectors.IdInFilterInspector',
|
||||||
'drf_yasg.inspectors.CoreAPICompatInspector',
|
'drf_yasg.inspectors.CoreAPICompatInspector',
|
||||||
],
|
],
|
||||||
|
'DEFAULT_INFO': 'netbox.urls.openapi_info',
|
||||||
'DEFAULT_MODEL_DEPTH': 1,
|
'DEFAULT_MODEL_DEPTH': 1,
|
||||||
'DEFAULT_PAGINATOR_INSPECTORS': [
|
'DEFAULT_PAGINATOR_INSPECTORS': [
|
||||||
'utilities.custom_inspectors.NullablePaginatorInspector',
|
'utilities.custom_inspectors.NullablePaginatorInspector',
|
||||||
|
0
netbox/netbox/tests/__init__.py
Normal file
0
netbox/netbox/tests/__init__.py
Normal file
13
netbox/netbox/tests/test_api.py
Normal file
13
netbox/netbox/tests/test_api.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from utilities.testing import APITestCase
|
||||||
|
|
||||||
|
|
||||||
|
class AppTest(APITestCase):
|
||||||
|
|
||||||
|
def test_root(self):
|
||||||
|
|
||||||
|
url = reverse('api-root')
|
||||||
|
response = self.client.get('{}?format=api'.format(url), **self.header)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
24
netbox/netbox/tests/test_views.py
Normal file
24
netbox/netbox/tests/test_views.py
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import urllib.parse
|
||||||
|
|
||||||
|
from utilities.testing import TestCase
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
|
||||||
|
class HomeViewTestCase(TestCase):
|
||||||
|
|
||||||
|
def test_home(self):
|
||||||
|
|
||||||
|
url = reverse('home')
|
||||||
|
|
||||||
|
response = self.client.get(url)
|
||||||
|
self.assertHttpStatus(response, 200)
|
||||||
|
|
||||||
|
def test_search(self):
|
||||||
|
|
||||||
|
url = reverse('search')
|
||||||
|
params = {
|
||||||
|
'q': 'foo',
|
||||||
|
}
|
||||||
|
|
||||||
|
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
|
||||||
|
self.assertHttpStatus(response, 200)
|
@ -9,14 +9,16 @@ from netbox.views import APIRootView, HomeView, SearchView
|
|||||||
from users.views import LoginView, LogoutView
|
from users.views import LoginView, LogoutView
|
||||||
from .admin import admin_site
|
from .admin import admin_site
|
||||||
|
|
||||||
schema_view = get_schema_view(
|
openapi_info = openapi.Info(
|
||||||
openapi.Info(
|
|
||||||
title="NetBox API",
|
title="NetBox API",
|
||||||
default_version='v2',
|
default_version='v2',
|
||||||
description="API to access NetBox",
|
description="API to access NetBox",
|
||||||
terms_of_service="https://github.com/netbox-community/netbox",
|
terms_of_service="https://github.com/netbox-community/netbox",
|
||||||
license=openapi.License(name="Apache v2 License"),
|
license=openapi.License(name="Apache v2 License"),
|
||||||
),
|
)
|
||||||
|
|
||||||
|
schema_view = get_schema_view(
|
||||||
|
openapi_info,
|
||||||
validators=['flex', 'ssv'],
|
validators=['flex', 'ssv'],
|
||||||
public=True,
|
public=True,
|
||||||
)
|
)
|
||||||
@ -24,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:
|
||||||
@ -76,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'
|
||||||
|
@ -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]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
11
netbox/project-static/js/configcontext.js
Normal file
11
netbox/project-static/js/configcontext.js
Normal 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();
|
||||||
|
}
|
||||||
|
});
|
@ -158,15 +158,18 @@ $(document).ready(function() {
|
|||||||
|
|
||||||
filter_for_elements.each(function(index, filter_for_element) {
|
filter_for_elements.each(function(index, filter_for_element) {
|
||||||
var param_name = $(filter_for_element).attr(attr_name);
|
var param_name = $(filter_for_element).attr(attr_name);
|
||||||
|
var is_required = $(filter_for_element).attr("required");
|
||||||
var is_nullable = $(filter_for_element).attr("nullable");
|
var is_nullable = $(filter_for_element).attr("nullable");
|
||||||
var is_visible = $(filter_for_element).is(":visible");
|
var is_visible = $(filter_for_element).is(":visible");
|
||||||
var value = $(filter_for_element).val();
|
var value = $(filter_for_element).val();
|
||||||
|
|
||||||
if (param_name && is_visible && value) {
|
if (param_name && is_visible) {
|
||||||
|
if (value) {
|
||||||
parameters[param_name] = value;
|
parameters[param_name] = value;
|
||||||
} else if (param_name && is_visible && is_nullable) {
|
} else if (is_required && is_nullable) {
|
||||||
parameters[param_name] = "null";
|
parameters[param_name] = "null";
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Conditional query params
|
// Conditional query params
|
||||||
|
@ -2,9 +2,9 @@
|
|||||||
$('button.toggle-ips').click(function() {
|
$('button.toggle-ips').click(function() {
|
||||||
var selected = $(this).attr('selected');
|
var selected = $(this).attr('selected');
|
||||||
if (selected) {
|
if (selected) {
|
||||||
$('#interfaces_table tr.ipaddresses').hide();
|
$('#interfaces_table tr.interface:visible + tr.ipaddresses').hide();
|
||||||
} else {
|
} else {
|
||||||
$('#interfaces_table tr.ipaddresses').show();
|
$('#interfaces_table tr.interface:visible + tr.ipaddresses').show();
|
||||||
}
|
}
|
||||||
$(this).attr('selected', !selected);
|
$(this).attr('selected', !selected);
|
||||||
$(this).children('span').toggleClass('glyphicon-check glyphicon-unchecked');
|
$(this).children('span').toggleClass('glyphicon-check glyphicon-unchecked');
|
||||||
@ -14,17 +14,22 @@ $('button.toggle-ips').click(function() {
|
|||||||
// Inteface filtering
|
// Inteface filtering
|
||||||
$('input.interface-filter').on('input', function() {
|
$('input.interface-filter').on('input', function() {
|
||||||
var filter = new RegExp(this.value);
|
var filter = new RegExp(this.value);
|
||||||
|
var interface;
|
||||||
|
|
||||||
for (interface of $(this).closest('div.panel').find('tbody > tr')) {
|
for (interface of $('#interfaces_table > tbody > tr.interface')) {
|
||||||
// Slice off 'interface_' at the start of the ID
|
// Slice off 'interface_' at the start of the ID
|
||||||
if (filter && filter.test(interface.id.slice(10))) {
|
if (filter.test(interface.id.slice(10))) {
|
||||||
// Match the toggle in case the filter now matches the interface
|
// Match the toggle in case the filter now matches the interface
|
||||||
$(interface).find('input:checkbox[name=pk]').prop('checked', $('input.toggle').prop('checked'));
|
$(interface).find('input:checkbox[name=pk]').prop('checked', $('input.toggle').prop('checked'));
|
||||||
$(interface).show();
|
$(interface).show();
|
||||||
|
if ($('button.toggle-ips').attr('selected')) {
|
||||||
|
$(interface).next('tr.ipaddresses').show();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Uncheck to prevent actions from including it when it doesn't match
|
// Uncheck to prevent actions from including it when it doesn't match
|
||||||
$(interface).find('input:checkbox[name=pk]').prop('checked', false);
|
$(interface).find('input:checkbox[name=pk]').prop('checked', false);
|
||||||
$(interface).hide();
|
$(interface).hide();
|
||||||
|
$(interface).next('tr.ipaddresses').hide();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -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
|
||||||
|
5
netbox/secrets/constants.py
Normal file
5
netbox/secrets/constants.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
#
|
||||||
|
# Secrets
|
||||||
|
#
|
||||||
|
|
||||||
|
SECRET_PLAINTEXT_MAX_LENGTH = 65535
|
@ -4,11 +4,14 @@ 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, FilterChoiceField, FlexibleModelChoiceField, SlugField,
|
||||||
StaticSelect2Multiple
|
StaticSelect2Multiple, TagFilterField
|
||||||
)
|
)
|
||||||
|
from .constants import *
|
||||||
from .models import Secret, SecretRole, UserKey
|
from .models import Secret, SecretRole, UserKey
|
||||||
|
|
||||||
|
|
||||||
@ -67,9 +70,9 @@ class SecretRoleCSVForm(forms.ModelForm):
|
|||||||
# Secrets
|
# Secrets
|
||||||
#
|
#
|
||||||
|
|
||||||
class SecretForm(BootstrapMixin, CustomFieldForm):
|
class SecretForm(BootstrapMixin, CustomFieldModelForm):
|
||||||
plaintext = forms.CharField(
|
plaintext = forms.CharField(
|
||||||
max_length=65535,
|
max_length=SECRET_PLAINTEXT_MAX_LENGTH,
|
||||||
required=False,
|
required=False,
|
||||||
label='Plaintext',
|
label='Plaintext',
|
||||||
widget=forms.PasswordInput(
|
widget=forms.PasswordInput(
|
||||||
@ -79,7 +82,7 @@ class SecretForm(BootstrapMixin, CustomFieldForm):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
plaintext2 = forms.CharField(
|
plaintext2 = forms.CharField(
|
||||||
max_length=65535,
|
max_length=SECRET_PLAINTEXT_MAX_LENGTH,
|
||||||
required=False,
|
required=False,
|
||||||
label='Plaintext (verify)',
|
label='Plaintext (verify)',
|
||||||
widget=forms.PasswordInput()
|
widget=forms.PasswordInput()
|
||||||
@ -115,7 +118,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',
|
||||||
@ -186,6 +189,7 @@ class SecretFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
|||||||
value_field="slug",
|
value_field="slug",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
@ -1,26 +1,23 @@
|
|||||||
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 StandardTestCases
|
||||||
from .constants import PRIVATE_KEY, PUBLIC_KEY
|
from .constants import PRIVATE_KEY, PUBLIC_KEY
|
||||||
|
|
||||||
|
|
||||||
class SecretRoleTestCase(TestCase):
|
class SecretRoleTestCase(StandardTestCases.Views):
|
||||||
|
model = SecretRole
|
||||||
|
|
||||||
def setUp(self):
|
# Disable inapplicable tests
|
||||||
user = create_test_user(
|
test_get_object = None
|
||||||
permissions=[
|
test_delete_object = None
|
||||||
'secrets.view_secretrole',
|
test_bulk_edit_objects = None
|
||||||
'secrets.add_secretrole',
|
|
||||||
]
|
@classmethod
|
||||||
)
|
def setUpTestData(cls):
|
||||||
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 +25,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(StandardTestCases.Views):
|
||||||
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 +116,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)
|
||||||
|
@ -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}),
|
||||||
|
|
||||||
]
|
]
|
||||||
|
@ -16,7 +16,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-md-3 noprint">
|
<div class="col-md-3 noprint">
|
||||||
{% include 'inc/search_panel.html' %}
|
{% include 'inc/search_panel.html' %}
|
||||||
{% include 'inc/tags_panel.html' %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -16,7 +16,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-md-3 noprint">
|
<div class="col-md-3 noprint">
|
||||||
{% include 'inc/search_panel.html' %}
|
{% include 'inc/search_panel.html' %}
|
||||||
{% include 'inc/tags_panel.html' %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -32,7 +32,7 @@
|
|||||||
{% if cable.label %}<code>{{ cable.label }}</code>{% else %}Cable #{{ cable.pk }}{% endif %}
|
{% if cable.label %}<code>{{ cable.label }}</code>{% else %}Cable #{{ cable.pk }}{% endif %}
|
||||||
</a>
|
</a>
|
||||||
</h4>
|
</h4>
|
||||||
<p><span class="label label-{% if cable.status %}success{% else %}info{% endif %}">{{ cable.get_status_display }}</span></p>
|
<p><span class="label label-{{ cable.get_status_class }}">{{ cable.get_status_display }}</span></p>
|
||||||
<p>{{ cable.get_type_display|default:"" }}</p>
|
<p>{{ cable.get_type_display|default:"" }}</p>
|
||||||
{% if cable.length %}{{ cable.length }} {{ cable.get_length_unit_display }}{% endif %}
|
{% if cable.length %}{{ cable.length }} {{ cable.get_length_unit_display }}{% endif %}
|
||||||
{% if cable.color %}
|
{% if cable.color %}
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -14,7 +14,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-md-3 noprint">
|
<div class="col-md-3 noprint">
|
||||||
{% include 'inc/search_panel.html' %}
|
{% include 'inc/search_panel.html' %}
|
||||||
{% include 'inc/tags_panel.html' %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -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>
|
||||||
|
@ -16,7 +16,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-md-3 noprint">
|
<div class="col-md-3 noprint">
|
||||||
{% include 'inc/search_panel.html' %}
|
{% include 'inc/search_panel.html' %}
|
||||||
{% include 'inc/tags_panel.html' %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -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 %}
|
||||||
|
@ -16,7 +16,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-md-3 noprint">
|
<div class="col-md-3 noprint">
|
||||||
{% include 'inc/search_panel.html' %}
|
{% include 'inc/search_panel.html' %}
|
||||||
{% include 'inc/tags_panel.html' %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
{% if perms.dcim.change_cable %}
|
{% if perms.dcim.change_cable %}
|
||||||
{% if cable.status %}
|
{% if cable.status == 'connected' %}
|
||||||
<a href="#" class="btn btn-warning btn-xs cable-toggle connected" title="Mark planned" data="{{ cable.pk }}">
|
<a href="#" class="btn btn-warning btn-xs cable-toggle connected" title="Mark planned" data="{{ cable.pk }}">
|
||||||
<i class="glyphicon glyphicon-ban-circle" aria-hidden="true"></i>
|
<i class="glyphicon glyphicon-ban-circle" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<tr class="consoleport{% if cp.cable.status %} success{% elif cp.cable %} info{% endif %}">
|
<tr class="consoleport{% if cp.cable %} {{ cp.cable.get_status_class }}{% endif %}">
|
||||||
|
|
||||||
{# Name #}
|
{# Name #}
|
||||||
<td>
|
<td>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{% load helpers %}
|
{% load helpers %}
|
||||||
|
|
||||||
<tr class="consoleserverport{% if csp.cable.status %} success{% elif csp.cable %} info{% endif %}">
|
<tr class="consoleserverport{% if csp.cable %} {{ csp.cable.get_status_class }}{% endif %}">
|
||||||
|
|
||||||
{# Checkbox #}
|
{# Checkbox #}
|
||||||
{% if perms.dcim.change_consoleserverport or perms.dcim.delete_consoleserverport %}
|
{% if perms.dcim.change_consoleserverport or perms.dcim.delete_consoleserverport %}
|
||||||
|
@ -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>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
{% load helpers %}
|
{% load helpers %}
|
||||||
<tr class="frontport{% if frontport.cable.status %} success{% elif frontport.cable %} info{% endif %}">
|
<tr class="frontport{% if frontport.cable %} {{ frontport.cable.get_status_class }}{% endif %}">
|
||||||
|
|
||||||
{# Checkbox #}
|
{# Checkbox #}
|
||||||
{% if perms.dcim.change_frontport or perms.dcim.delete_frontport %}
|
{% if perms.dcim.change_frontport or perms.dcim.delete_frontport %}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
{% load helpers %}
|
{% load helpers %}
|
||||||
<tr class="interface{% if not iface.enabled %} danger{% elif iface.cable.status %} success{% elif iface.cable %} info{% elif iface.is_virtual %} warning{% endif %}" id="interface_{{ iface.name }}">
|
<tr class="interface{% if not iface.enabled %} danger{% elif iface.cable %} {{ iface.cable.get_status_class }}{% elif iface.is_virtual %} warning{% endif %}" id="interface_{{ iface.name }}">
|
||||||
|
|
||||||
{# Checkbox #}
|
{# Checkbox #}
|
||||||
{% if perms.dcim.change_interface or perms.dcim.delete_interface %}
|
{% if perms.dcim.change_interface or perms.dcim.delete_interface %}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{% load helpers %}
|
{% load helpers %}
|
||||||
|
|
||||||
<tr class="poweroutlet{% if po.cable.status %} success{% elif po.cable %} info{% endif %}">
|
<tr class="poweroutlet{% if po.cable %} {{ po.cable.get_status_class }}{% endif %}">
|
||||||
|
|
||||||
{# Checkbox #}
|
{# Checkbox #}
|
||||||
{% if perms.dcim.change_poweroutlet or perms.dcim.delete_poweroutlet %}
|
{% if perms.dcim.change_poweroutlet or perms.dcim.delete_poweroutlet %}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<tr class="powerport{% if pp.cable.status %} success{% elif pp.cable %} info{% endif %}">
|
<tr class="powerport{% if pp.cable %} {{ pp.cable.get_status_class }}{% endif %}">
|
||||||
|
|
||||||
{# Name #}
|
{# Name #}
|
||||||
<td>
|
<td>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
{% load helpers %}
|
{% load helpers %}
|
||||||
<tr class="rearport{% if rearport.cable.status %} success{% elif rearport.cable %} info{% endif %}">
|
<tr class="rearport{% if rearport.cable %} {{ rearport.cable.get_status_class }}{% endif %}">
|
||||||
|
|
||||||
{# Checkbox #}
|
{# Checkbox #}
|
||||||
{% if perms.dcim.change_rearport or perms.dcim.delete_rearport %}
|
{% if perms.dcim.change_rearport or perms.dcim.delete_rearport %}
|
||||||
|
@ -16,7 +16,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-md-3 noprint">
|
<div class="col-md-3 noprint">
|
||||||
{% include 'inc/search_panel.html' %}
|
{% include 'inc/search_panel.html' %}
|
||||||
{% include 'inc/tags_panel.html' %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -3,8 +3,8 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="btn-group pull-right noprint" role="group">
|
<div class="btn-group pull-right noprint" role="group">
|
||||||
<a href="{% url 'dcim:rack_elevation_list' %}{% querystring request face=0 %}" class="btn btn-default{% if request.GET.face != '1' %} active{% endif %}">Front</a>
|
<a href="{% url 'dcim:rack_elevation_list' %}{% querystring request face='front' %}" class="btn btn-default{% if rack_face == 'front' %} active{% endif %}">Front</a>
|
||||||
<a href="{% url 'dcim:rack_elevation_list' %}{% querystring request face=1 %}" class="btn btn-default{% if request.GET.face == '1' %} active{% endif %}">Rear</a>
|
<a href="{% url 'dcim:rack_elevation_list' %}{% querystring request face='rear' %}" class="btn btn-default{% if rack_face == 'rear' %} active{% endif %}">Rear</a>
|
||||||
</div>
|
</div>
|
||||||
<h1>{% block title %}Rack Elevations{% endblock %}</h1>
|
<h1>{% block title %}Rack Elevations{% endblock %}</h1>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@ -17,11 +17,7 @@
|
|||||||
<strong><a href="{% url 'dcim:rack' pk=rack.pk %}">{{ rack.name|truncatechars:"25" }}</a></strong>
|
<strong><a href="{% url 'dcim:rack' pk=rack.pk %}">{{ rack.name|truncatechars:"25" }}</a></strong>
|
||||||
<p><small class="text-muted">{{ rack.facility_id|truncatechars:"30" }}</small></p>
|
<p><small class="text-muted">{{ rack.facility_id|truncatechars:"30" }}</small></p>
|
||||||
</div>
|
</div>
|
||||||
{% if face_id %}
|
{% include 'dcim/inc/rack_elevation.html' with face=rack_face %}
|
||||||
{% include 'dcim/inc/rack_elevation.html' with face='rear' %}
|
|
||||||
{% else %}
|
|
||||||
{% include 'dcim/inc/rack_elevation.html' with face='front' %}
|
|
||||||
{% endif %}
|
|
||||||
<div class="clearfix"></div>
|
<div class="clearfix"></div>
|
||||||
<div class="rack_header">
|
<div class="rack_header">
|
||||||
<strong><a href="{% url 'dcim:rack' pk=rack.pk %}">{{ rack.name|truncatechars:"25" }}</a></strong>
|
<strong><a href="{% url 'dcim:rack' pk=rack.pk %}">{{ rack.name|truncatechars:"25" }}</a></strong>
|
||||||
|
@ -16,7 +16,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-md-3 noprint">
|
<div class="col-md-3 noprint">
|
||||||
{% include 'inc/search_panel.html' %}
|
{% include 'inc/search_panel.html' %}
|
||||||
{% include 'inc/tags_panel.html' %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -16,7 +16,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-md-3 noprint">
|
<div class="col-md-3 noprint">
|
||||||
{% include 'inc/search_panel.html' %}
|
{% include 'inc/search_panel.html' %}
|
||||||
{% include 'inc/tags_panel.html' %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -13,7 +13,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-md-3 noprint">
|
<div class="col-md-3 noprint">
|
||||||
{% include 'inc/search_panel.html' %}
|
{% include 'inc/search_panel.html' %}
|
||||||
{% include 'inc/tags_panel.html' %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
{% extends '_base.html' %}
|
{% extends '_base.html' %}
|
||||||
{% load helpers %}
|
{% load helpers %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
{% block header %}
|
{% block header %}
|
||||||
<div class="row noprint">
|
<div class="row noprint">
|
||||||
@ -134,6 +135,34 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Cluster Groups</td>
|
||||||
|
<td>
|
||||||
|
{% if configcontext.cluster_groups.all %}
|
||||||
|
<ul>
|
||||||
|
{% for cluster_group in configcontext.cluster_groups.all %}
|
||||||
|
<li><a href="{{ cluster_group.get_absolute_url }}">{{ cluster_group }}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">None</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Clusters</td>
|
||||||
|
<td>
|
||||||
|
{% if configcontext.clusters.all %}
|
||||||
|
<ul>
|
||||||
|
{% for cluster in configcontext.clusters.all %}
|
||||||
|
<li><a href="{{ cluster.get_absolute_url }}">{{ cluster }}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">None</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Tenant Groups</td>
|
<td>Tenant Groups</td>
|
||||||
<td>
|
<td>
|
||||||
@ -183,11 +212,16 @@
|
|||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<strong>Data</strong>
|
<strong>Data</strong>
|
||||||
|
{% include 'extras/inc/configcontext_format.html' %}
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<pre>{{ configcontext.data|render_json }}</pre>
|
{% include 'extras/inc/configcontext_data.html' with data=configcontext.data %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block javascript %}
|
||||||
|
<script src="{% static 'js/configcontext.js' %}?v{{ settings.VERSION }}"></script>
|
||||||
|
{% endblock %}
|
||||||
|
@ -18,6 +18,8 @@
|
|||||||
{% render_field form.sites %}
|
{% render_field form.sites %}
|
||||||
{% render_field form.roles %}
|
{% render_field form.roles %}
|
||||||
{% render_field form.platforms %}
|
{% render_field form.platforms %}
|
||||||
|
{% render_field form.cluster_groups %}
|
||||||
|
{% render_field form.clusters %}
|
||||||
{% render_field form.tenant_groups %}
|
{% render_field form.tenant_groups %}
|
||||||
{% render_field form.tenants %}
|
{% render_field form.tenants %}
|
||||||
{% render_field form.tags %}
|
{% render_field form.tags %}
|
||||||
|
8
netbox/templates/extras/inc/configcontext_data.html
Normal file
8
netbox/templates/extras/inc/configcontext_data.html
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{% load helpers %}
|
||||||
|
|
||||||
|
<div class="rendered-context-data" data-format="json">
|
||||||
|
<pre>{{ data|render_json }}</pre>
|
||||||
|
</div>
|
||||||
|
<div class="rendered-context-data" data-format="yaml" style="display: none;">
|
||||||
|
<pre>{{ data|render_yaml }}</pre>
|
||||||
|
</div>
|
6
netbox/templates/extras/inc/configcontext_format.html
Normal file
6
netbox/templates/extras/inc/configcontext_format.html
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<div class="pull-right">
|
||||||
|
<div class="btn-group btn-group-xs" role="group">
|
||||||
|
<span class="btn btn-default rendered-context-format active" data-format="json">JSON</span>
|
||||||
|
<span class="btn btn-default rendered-context-format" data-format="yaml">YAML</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -1,5 +1,6 @@
|
|||||||
{% extends base_template %}
|
{% extends base_template %}
|
||||||
{% load helpers %}
|
{% load helpers %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
{% block title %}{{ block.super }} - Config Context{% endblock %}
|
{% block title %}{{ block.super }} - Config Context{% endblock %}
|
||||||
|
|
||||||
@ -9,9 +10,10 @@
|
|||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<strong>Rendered Context</strong>
|
<strong>Rendered Context</strong>
|
||||||
|
{% include 'extras/inc/configcontext_format.html' %}
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<pre>{{ rendered_context|render_json }}</pre>
|
{% include 'extras/inc/configcontext_data.html' with data=rendered_context %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -22,7 +24,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
{% if obj.local_context_data %}
|
{% if obj.local_context_data %}
|
||||||
<pre>{{ obj.local_context_data|render_json }}</pre>
|
{% include 'extras/inc/configcontext_data.html' with data=obj.local_context_data %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="text-muted">None</span>
|
<span class="text-muted">None</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -47,7 +49,7 @@
|
|||||||
{% if context.description %}
|
{% if context.description %}
|
||||||
<br /><small>{{ context.description }}</small>
|
<br /><small>{{ context.description }}</small>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<pre>{{ context.data|render_json }}</pre>
|
{% include 'extras/inc/configcontext_data.html' with data=context.data %}
|
||||||
</div>
|
</div>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
@ -58,3 +60,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block javascript %}
|
||||||
|
<script src="{% static 'js/configcontext.js' %}?v{{ settings.VERSION }}"></script>
|
||||||
|
{% endblock %}
|
||||||
|
@ -9,13 +9,13 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td>{{ field }}</td>
|
<td>{{ field }}</td>
|
||||||
<td>
|
<td>
|
||||||
{% if field.type == 300 and value == True %}
|
{% if field.type == 'boolean' and value == True %}
|
||||||
<i class="glyphicon glyphicon-ok text-success" title="True"></i>
|
<i class="glyphicon glyphicon-ok text-success" title="True"></i>
|
||||||
{% elif field.type == 300 and value == False %}
|
{% elif field.type == 'boolean' and value == False %}
|
||||||
<i class="glyphicon glyphicon-remove text-danger" title="False"></i>
|
<i class="glyphicon glyphicon-remove text-danger" title="False"></i>
|
||||||
{% elif field.type == 500 and value %}
|
{% elif field.type == 'url' and value %}
|
||||||
<a href="{{ value }}">{{ value|truncatechars:70 }}</a>
|
<a href="{{ value }}">{{ value|truncatechars:70 }}</a>
|
||||||
{% elif field.type == 200 or value %}
|
{% elif field.type == 'integer' or value %}
|
||||||
{{ value }}
|
{{ value }}
|
||||||
{% elif field.required %}
|
{% elif field.required %}
|
||||||
<span class="text-warning">Not defined</span>
|
<span class="text-warning">Not defined</span>
|
||||||
|
@ -1,13 +0,0 @@
|
|||||||
{% load helpers %}
|
|
||||||
|
|
||||||
<div class="panel panel-default">
|
|
||||||
<div class="panel-heading">
|
|
||||||
<span class="fa fa-tags" aria-hidden="true"></span>
|
|
||||||
<strong>Tags</strong>
|
|
||||||
</div>
|
|
||||||
<div class="panel-body text-center">
|
|
||||||
{% for tag in tags %}
|
|
||||||
<a href="{% querystring request tag=tag.slug %}" class="btn btn-sm {% if tag.slug in request.GET.tag %}btn-primary{% else %}btn-link{% endif %}">{{ tag }} <span class="badge">{{ tag.count }}</span></a>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user