mirror of
https://github.com/netbox-community/netbox.git
synced 2026-01-01 18:17:45 -06:00
Compare commits
287 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c249a40a0 | ||
|
|
7e81d5fe11 | ||
|
|
6b4858303b | ||
|
|
5000f7f8d7 | ||
|
|
f643af13d7 | ||
|
|
a6e859d9b7 | ||
|
|
5bf68493df | ||
|
|
43f3488270 | ||
|
|
62efe0621f | ||
|
|
161f03217e | ||
|
|
40b56d7f62 | ||
|
|
4eb731cfae | ||
|
|
35786966c6 | ||
|
|
c3b64164ba | ||
|
|
644b4aa42d | ||
|
|
838291c3f3 | ||
|
|
3296298d21 | ||
|
|
211311be9f | ||
|
|
9a532b1eb2 | ||
|
|
1fbd3a2c26 | ||
|
|
99038ffc44 | ||
|
|
81d001d49e | ||
|
|
cc4a22b2d1 | ||
|
|
4d1749cc71 | ||
|
|
f7b620c6a2 | ||
|
|
76138f3080 | ||
|
|
36f8d6d259 | ||
|
|
909971f237 | ||
|
|
4e76e4ec9c | ||
|
|
87d90adaef | ||
|
|
cfd813772d | ||
|
|
2b484955aa | ||
|
|
d9dcc92300 | ||
|
|
d0c82c23bd | ||
|
|
6cdb86931e | ||
|
|
23d0241865 | ||
|
|
61c0a4cc61 | ||
|
|
b97d3b0716 | ||
|
|
25d126d4ff | ||
|
|
2b75e05ea7 | ||
|
|
1a997610c7 | ||
|
|
04ee55a40c | ||
|
|
1c72d75b62 | ||
|
|
9128dc961c | ||
|
|
12602a95ea | ||
|
|
11d012de4e | ||
|
|
45cdac6f36 | ||
|
|
2c4136f514 | ||
|
|
a14c7980f6 | ||
|
|
329740d2a7 | ||
|
|
8ffba6a279 | ||
|
|
5a02dc457c | ||
|
|
cf312e9690 | ||
|
|
20b36f910f | ||
|
|
0b3111c47f | ||
|
|
493d68a57a | ||
|
|
5092641157 | ||
|
|
2b134ea0f0 | ||
|
|
58ff08be4e | ||
|
|
682fd40fff | ||
|
|
cdcc63fdf6 | ||
|
|
c0052eb416 | ||
|
|
74e3e2e5e1 | ||
|
|
214470fb84 | ||
|
|
e76ea2a03c | ||
|
|
6c272adb0e | ||
|
|
51f7b7a5bf | ||
|
|
b81622222d | ||
|
|
e8a8b39c47 | ||
|
|
c78d30d47e | ||
|
|
ba6562a5db | ||
|
|
b28729baff | ||
|
|
adf9221bab | ||
|
|
d2157a3423 | ||
|
|
e07ed3de93 | ||
|
|
584539d0a3 | ||
|
|
322b328584 | ||
|
|
b38eeaebc9 | ||
|
|
66fa79741d | ||
|
|
09805ddc4a | ||
|
|
1130f6b9f0 | ||
|
|
7a53e24f97 | ||
|
|
f05c7be394 | ||
|
|
2cf990bd92 | ||
|
|
21473548a5 | ||
|
|
626513a8b2 | ||
|
|
5871640701 | ||
|
|
8cfb5ac5c6 | ||
|
|
ae1767b5d0 | ||
|
|
84d078a539 | ||
|
|
2a1de0202f | ||
|
|
4ea8967c2d | ||
|
|
a456cbb26c | ||
|
|
5b505b21c8 | ||
|
|
89ab6553d6 | ||
|
|
faa22cb637 | ||
|
|
1a8eea5aa9 | ||
|
|
2de8d8b73d | ||
|
|
440f754fec | ||
|
|
815a46bfbe | ||
|
|
182fddddd2 | ||
|
|
ce89fa74b9 | ||
|
|
7ce1289bb2 | ||
|
|
e4df02887b | ||
|
|
6bc7be7ba5 | ||
|
|
7aba8e3ec4 | ||
|
|
8d5ea5d005 | ||
|
|
ec0f45e20d | ||
|
|
0c8ad45976 | ||
|
|
e431ef09e5 | ||
|
|
03a7f6bbda | ||
|
|
a4705fa73a | ||
|
|
0a8d39cfe4 | ||
|
|
1d72436bfc | ||
|
|
598d23fc03 | ||
|
|
472a45ddec | ||
|
|
0863145c7f | ||
|
|
909323663e | ||
|
|
8212c8f6fc | ||
|
|
8df9bb6fb4 | ||
|
|
ff952fb221 | ||
|
|
4d50cad6ed | ||
|
|
120cbb0159 | ||
|
|
08ce024473 | ||
|
|
807c2f048d | ||
|
|
fafcdf7def | ||
|
|
92fab048d1 | ||
|
|
6884404957 | ||
|
|
88c917231d | ||
|
|
a054aff3c4 | ||
|
|
8fd809ac5e | ||
|
|
fff657cd5a | ||
|
|
4ef15e4dc8 | ||
|
|
c5f74cce80 | ||
|
|
35498c17d7 | ||
|
|
874e59b01a | ||
|
|
72f0e31b84 | ||
|
|
ba9a2956a8 | ||
|
|
3538eeda14 | ||
|
|
0c89534bfb | ||
|
|
47b15aacef | ||
|
|
3e0ab79977 | ||
|
|
344fa72357 | ||
|
|
617fc7659f | ||
|
|
0d91b6b74b | ||
|
|
335343642b | ||
|
|
bc7f5fb33a | ||
|
|
ca56fc709a | ||
|
|
a08ee68033 | ||
|
|
0d57cb0033 | ||
|
|
53804d39bb | ||
|
|
1e221cd9bb | ||
|
|
0c942f18c1 | ||
|
|
00d32f0a7d | ||
|
|
df3fef8bb1 | ||
|
|
5cc24c055b | ||
|
|
4064c32a7f | ||
|
|
6d7c5d51fe | ||
|
|
64c0059dd8 | ||
|
|
d0ece2e48d | ||
|
|
139f18b2e5 | ||
|
|
8eea0331bf | ||
|
|
62d6e02d6b | ||
|
|
a8601bb1fd | ||
|
|
fe452735be | ||
|
|
3b1128f8f3 | ||
|
|
0402323ef9 | ||
|
|
5bf85597ed | ||
|
|
e4b910fe87 | ||
|
|
24db573764 | ||
|
|
5befa533c6 | ||
|
|
15bc731f61 | ||
|
|
8fb4988fa1 | ||
|
|
ab378ed218 | ||
|
|
56bb053146 | ||
|
|
3c3cca8ec1 | ||
|
|
908586c93a | ||
|
|
2e83ce76ed | ||
|
|
2ab382eec5 | ||
|
|
2503978555 | ||
|
|
55886d6793 | ||
|
|
009c0ba31c | ||
|
|
ec53e1c74c | ||
|
|
7177fcfa61 | ||
|
|
fb56d5bc66 | ||
|
|
221805a63e | ||
|
|
da68968d75 | ||
|
|
ca795f729f | ||
|
|
ff4e6bd166 | ||
|
|
5ea30c8628 | ||
|
|
a54fcda781 | ||
|
|
7388fa3556 | ||
|
|
a966a4c8ac | ||
|
|
ebef48e472 | ||
|
|
26ca6b4a84 | ||
|
|
3da6f22479 | ||
|
|
d4789b7c9e | ||
|
|
5008526db1 | ||
|
|
009fc4f301 | ||
|
|
55f5ede970 | ||
|
|
5ddfde2214 | ||
|
|
505cb9cab8 | ||
|
|
d5c4a9d159 | ||
|
|
26ddd96e30 | ||
|
|
f0c83e168e | ||
|
|
885ea8a4d5 | ||
|
|
202a0a0e73 | ||
|
|
5bfd65b5fe | ||
|
|
7c74d2ca65 | ||
|
|
9adeed55fb | ||
|
|
12c7d83a91 | ||
|
|
dc1b7874ff | ||
|
|
c72a353733 | ||
|
|
35511cfdc1 | ||
|
|
099c446f38 | ||
|
|
705c352885 | ||
|
|
12d09e2274 | ||
|
|
b271fd32bd | ||
|
|
b3c2b78e8a | ||
|
|
97a89948c8 | ||
|
|
1e61fcb485 | ||
|
|
4cc9f2f67d | ||
|
|
52257467c3 | ||
|
|
4563749fd9 | ||
|
|
6d242ec348 | ||
|
|
d0e00162ed | ||
|
|
21f2e0b131 | ||
|
|
6ac8d41323 | ||
|
|
bb9e1ad857 | ||
|
|
98de88de90 | ||
|
|
c571aa68be | ||
|
|
091d860ae5 | ||
|
|
b5344b0aa7 | ||
|
|
17e0054941 | ||
|
|
1b5969a5ee | ||
|
|
3378287b0c | ||
|
|
077d692d6d | ||
|
|
5620fdc63e | ||
|
|
f7ca97d51f | ||
|
|
d400f92ee8 | ||
|
|
c1792653cc | ||
|
|
aebfb143e0 | ||
|
|
ef4ea06f5d | ||
|
|
85729f3df8 | ||
|
|
a2475ee501 | ||
|
|
71601aad39 | ||
|
|
c1c8b5e816 | ||
|
|
2296cdc222 | ||
|
|
070b41e694 | ||
|
|
d04626e75f | ||
|
|
68738e683a | ||
|
|
3f2c74f5e7 | ||
|
|
a58bbccfd3 | ||
|
|
b1e78fa3c4 | ||
|
|
0d3ff664b6 | ||
|
|
b0c0ad7c82 | ||
|
|
0ad613e6b4 | ||
|
|
75906f7591 | ||
|
|
c49d977379 | ||
|
|
6b9fa5e76f | ||
|
|
57a0cf0a33 | ||
|
|
f8ce67c69f | ||
|
|
d0295f089d | ||
|
|
f805b57778 | ||
|
|
3e79b9d26a | ||
|
|
c1639b7781 | ||
|
|
fca347e49e | ||
|
|
32623148dc | ||
|
|
8e9a0eeef0 | ||
|
|
ace8fac2c1 | ||
|
|
ae95b159bc | ||
|
|
ff822743cc | ||
|
|
d30d79b4e3 | ||
|
|
23155551d1 | ||
|
|
22228b58f1 | ||
|
|
084a68f6d1 | ||
|
|
deb653cbf3 | ||
|
|
6ce38ffa0f | ||
|
|
09faaff849 | ||
|
|
2684f86594 | ||
|
|
f052b90ec3 | ||
|
|
a30e50ecc4 | ||
|
|
c31c8b1a25 | ||
|
|
c8997868ce | ||
|
|
02cf39c85b | ||
|
|
201416ba52 | ||
|
|
9d846d7b87 |
9
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
9
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
# Reference: https://help.github.com/en/github/building-a-strong-community/configuring-issue-templates-for-your-repository#configuring-the-template-chooser
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: 📖 Contributing Policy
|
||||
url: https://github.com/netbox-community/netbox/blob/develop/CONTRIBUTING.md
|
||||
about: Please read through our contributing policy before opening an issue or pull request
|
||||
- name: 💬 Discussion Group
|
||||
url: https://groups.google.com/forum/#!forum/netbox-discuss
|
||||
about: Join our discussion group for assistance with installation issues and other problems
|
||||
3
.github/stale.yml
vendored
3
.github/stale.yml
vendored
@@ -1,5 +1,8 @@
|
||||
# Configuration for Stale (https://github.com/apps/stale)
|
||||
|
||||
# Pull requests are exempt from being marked as stale
|
||||
only: issues
|
||||
|
||||
# Number of days of inactivity before an issue becomes stale
|
||||
daysUntilStale: 14
|
||||
|
||||
|
||||
@@ -26,8 +26,12 @@ or join us in the #netbox Slack channel on [NetworkToCode](https://networktocode
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||

|
||||
|
||||
# Installation
|
||||
|
||||
@@ -22,6 +22,10 @@ django-filter
|
||||
# https://github.com/django-mptt/django-mptt
|
||||
django-mptt
|
||||
|
||||
# Context managers for PostgreSQL advisory locks
|
||||
# https://github.com/Xof/django-pglocks
|
||||
django-pglocks
|
||||
|
||||
# Prometheus metrics library for Django
|
||||
# https://github.com/korfuri/django-prometheus
|
||||
django-prometheus
|
||||
|
||||
@@ -27,11 +27,17 @@ class MyScript(Script):
|
||||
var2 = IntegerVar(...)
|
||||
var3 = ObjectVar(...)
|
||||
|
||||
def run(self, data):
|
||||
def run(self, data, commit):
|
||||
...
|
||||
```
|
||||
|
||||
The `run()` method is passed a single argument: a dictionary containing all of the variable data passed via the web form. Your script can reference this data during execution.
|
||||
The `run()` method should accept two arguments:
|
||||
|
||||
* `data` - A dictionary containing all of the variable data passed via the web form.
|
||||
* `commit` - A boolean indicating whether database changes will be committed.
|
||||
|
||||
!!! note
|
||||
The `commit` argument was introduced in NetBox v2.7.8. Backward compatibility is maintained for scripts which accept only the `data` argument, however moving forward scripts should accept both arguments.
|
||||
|
||||
Defining variables is optional: You may create a script with only a `run()` method if no user input is needed.
|
||||
|
||||
@@ -177,10 +183,11 @@ An IPv4 or IPv6 network with a mask. Returns a `netaddr.IPNetwork` object. Two a
|
||||
|
||||
All variables support the following default options:
|
||||
|
||||
* `label` - The name of the form field
|
||||
* `description` - A brief description of the field
|
||||
* `default` - The field's default value
|
||||
* `description` - A brief description of the field
|
||||
* `label` - The name of the form field
|
||||
* `required` - Indicates whether the field is mandatory (default: true)
|
||||
* `widget` - The class of form widget to use (see the [Django documentation](https://docs.djangoproject.com/en/stable/ref/forms/widgets/))
|
||||
|
||||
## Example
|
||||
|
||||
@@ -195,7 +202,7 @@ These variables are presented as a web form to be completed by the user. Once su
|
||||
```
|
||||
from django.utils.text import slugify
|
||||
|
||||
from dcim.constants import *
|
||||
from dcim.choices import DeviceStatusChoices, SiteStatusChoices
|
||||
from dcim.models import Device, DeviceRole, DeviceType, Site
|
||||
from extras.scripts import *
|
||||
|
||||
@@ -221,13 +228,13 @@ class NewBranchScript(Script):
|
||||
)
|
||||
)
|
||||
|
||||
def run(self, data):
|
||||
def run(self, data, commit):
|
||||
|
||||
# Create the new site
|
||||
site = Site(
|
||||
name=data['site_name'],
|
||||
slug=slugify(data['site_name']),
|
||||
status=SITE_STATUS_PLANNED
|
||||
status=SiteStatusChoices.STATUS_PLANNED
|
||||
)
|
||||
site.save()
|
||||
self.log_success("Created new site: {}".format(site))
|
||||
@@ -239,7 +246,7 @@ class NewBranchScript(Script):
|
||||
device_type=data['switch_model'],
|
||||
name='{}-switch{}'.format(site.slug, i),
|
||||
site=site,
|
||||
status=DEVICE_STATUS_PLANNED,
|
||||
status=DeviceStatusChoices.STATUS_PLANNED,
|
||||
device_role=switch_role
|
||||
)
|
||||
switch.save()
|
||||
|
||||
@@ -32,7 +32,8 @@ class DeviceIPsReport(Report):
|
||||
Within each report class, we'll create a number of test methods to execute our report's logic. In DeviceConnectionsReport, for instance, we want to ensure that every live device has a console connection, an out-of-band management connection, and two power connections.
|
||||
|
||||
```
|
||||
from dcim.constants import CONNECTION_STATUS_PLANNED, DEVICE_STATUS_ACTIVE
|
||||
from dcim.choices import DeviceStatusChoices
|
||||
from dcim.constants import CONNECTION_STATUS_PLANNED
|
||||
from dcim.models import ConsolePort, Device, PowerPort
|
||||
from extras.reports import Report
|
||||
|
||||
@@ -43,7 +44,8 @@ class DeviceConnectionsReport(Report):
|
||||
def test_console_connection(self):
|
||||
|
||||
# Check that every console port for every active device has a connection defined.
|
||||
for console_port in ConsolePort.objects.prefetch_related('device').filter(device__status=DEVICE_STATUS_ACTIVE):
|
||||
active = DeviceStatusChoices.STATUS_ACTIVE
|
||||
for console_port in ConsolePort.objects.prefetch_related('device').filter(device__status=active):
|
||||
if console_port.connected_endpoint is None:
|
||||
self.log_failure(
|
||||
console_port.device,
|
||||
@@ -60,7 +62,7 @@ class DeviceConnectionsReport(Report):
|
||||
def test_power_connections(self):
|
||||
|
||||
# Check that every active device has at least two connected power supplies.
|
||||
for device in Device.objects.filter(status=DEVICE_STATUS_ACTIVE):
|
||||
for device in Device.objects.filter(status=DeviceStatusChoices.STATUS_ACTIVE):
|
||||
connected_ports = 0
|
||||
for power_port in PowerPort.objects.filter(device=device):
|
||||
if power_port.connected_endpoint is not None:
|
||||
|
||||
@@ -1,61 +1,73 @@
|
||||
# Webhooks
|
||||
|
||||
A webhook defines an HTTP request that is sent to an external application when certain types of objects are created, updated, and/or deleted in NetBox. When a webhook is triggered, a POST request is sent to its configured URL. This request will include a full representation of the object being modified for consumption by the receiver. Webhooks are configured via the admin UI under Extras > Webhooks.
|
||||
A webhook is a mechanism for conveying to some external system a change that took place in NetBox. For example, you may want to notify a monitoring system whenever a device status is changed in NetBox. This can be done by creating a webhook for the device model in NetBox. When NetBox detects a change to a device, an HTTP request containing the details of the change and who made it be sent to the specified receiver. Webhooks are configured in the admin UI under Extras > Webhooks.
|
||||
|
||||
An optional secret key can be configured for each webhook. This will append a `X-Hook-Signature` header to the request, consisting of a HMAC (SHA-512) hex digest of the request body using the secret as the key. This digest can be used by the receiver to authenticate the request's content.
|
||||
## Configuration
|
||||
|
||||
## Requests
|
||||
* **Name** - A unique name for the webhook. The name is not included with outbound messages.
|
||||
* **Object type(s)** - The type or types of NetBox object that will trigger the webhook.
|
||||
* **Enabled** - If unchecked, the webhook will be inactive.
|
||||
* **Events** - A webhook may trigger on any combination of create, update, and delete events. At least one event type must be selected.
|
||||
* **HTTP method** - The type of HTTP request to send. Options include GET, POST, PUT, PATCH, and DELETE.
|
||||
* **URL** - The fuly-qualified URL of the request to be sent. This may specify a destination port number if needed.
|
||||
* **HTTP content type** - The value of the request's `Content-Type` header. (Defaults to `application/json`)
|
||||
* **Additional headers** - Any additional headers to include with the request (optional). Add one header per line in the format `Name: Value`. Jinja2 templating is supported for this field (see below).
|
||||
* **Body template** - The content of the request being sent (optional). Jinja2 templating is supported for this field (see below). If blank, NetBox will populate the request body with a raw dump of the webhook context. (If the HTTP cotent type is set to `application/json`, this will be formatted as a JSON object.)
|
||||
* **Secret** - A secret string used to prove authenticity of the request (optional). This will append a `X-Hook-Signature` header to the request, consisting of a HMAC (SHA-512) hex digest of the request body using the secret as the key.
|
||||
* **SSL verification** - Uncheck this option to disable validation of the receiver's SSL certificate. (Disable with caution!)
|
||||
* **CA file path** - The file path to a particular certificate authority (CA) file to use when validating the receiver's SSL certificate (optional).
|
||||
|
||||
The webhook POST request is structured as so (assuming `application/json` as the Content-Type):
|
||||
## Jinja2 Template Support
|
||||
|
||||
[Jinja2 templating](https://jinja.palletsprojects.com/) is supported for the `additional_headers` and `body_template` fields. This enables the user to convey change data in the request headers as well as to craft a customized request body. Request content can be crafted to enable the direct interaction with external systems by ensuring the outgoing message is in a format the receiver expects and understands.
|
||||
|
||||
For example, you might create a NetBox webhook to [trigger a Slack message](https://api.slack.com/messaging/webhooks) any time an IP address is created. You can accomplish this using the following configuration:
|
||||
|
||||
* Object type: IPAM > IP address
|
||||
* HTTP method: POST
|
||||
* URL: <Slack incoming webhook URL>
|
||||
* HTTP content type: `application/json`
|
||||
* Body template: `{"text": "IP address {{ data['address'] }} was created by {{ username }}!"}`
|
||||
|
||||
### Available Context
|
||||
|
||||
The following data is available as context for Jinja2 templates:
|
||||
|
||||
* `event` - The type of event which triggered the webhook: created, updated, or deleted.
|
||||
* `model` - The NetBox model which triggered the change.
|
||||
* `timestamp` - The time at which the event occurred (in [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) format).
|
||||
* `username` - The name of the user account associated with the change.
|
||||
* `request_id` - The unique request ID. This may be used to correlate multiple changes associated with a single request.
|
||||
* `data` - A serialized representation of the object _after_ the change was made. This is typically equivalent to the model's representation in NetBox's REST API.
|
||||
|
||||
### Default Request Body
|
||||
|
||||
If no body template is specified, the request body will be populated with a JSON object containing the context data. For example, a newly created site might appear as follows:
|
||||
|
||||
```no-highlight
|
||||
{
|
||||
"event": "created",
|
||||
"timestamp": "2019-10-12 12:51:29.746944",
|
||||
"username": "admin",
|
||||
"timestamp": "2020-02-25 15:10:26.010582+00:00",
|
||||
"model": "site",
|
||||
"request_id": "43d8e212-94c7-4f67-b544-0dcde4fc0f43",
|
||||
"username": "jstretch",
|
||||
"request_id": "fdbca812-3142-4783-b364-2e2bd5c16c6a",
|
||||
"data": {
|
||||
"id": 19,
|
||||
"name": "Site 1",
|
||||
"slug": "site-1",
|
||||
"status":
|
||||
"value": "active",
|
||||
"label": "Active",
|
||||
"id": 1
|
||||
},
|
||||
"region": null,
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`data` is the serialized representation of the model instance(s) from the event. The same serializers from the NetBox API are used. So an example of the payload for a Site delete event would be:
|
||||
## Webhook Processing
|
||||
|
||||
```no-highlight
|
||||
{
|
||||
"event": "deleted",
|
||||
"timestamp": "2019-10-12 12:55:44.030750",
|
||||
"username": "johnsmith",
|
||||
"model": "site",
|
||||
"request_id": "e9bb83b2-ebe4-4346-b13f-07144b1a00b4",
|
||||
"data": {
|
||||
"asn": None,
|
||||
"comments": "",
|
||||
"contact_email": "",
|
||||
"contact_name": "",
|
||||
"contact_phone": "",
|
||||
"count_circuits": 0,
|
||||
"count_devices": 0,
|
||||
"count_prefixes": 0,
|
||||
"count_racks": 0,
|
||||
"count_vlans": 0,
|
||||
"custom_fields": {},
|
||||
"facility": "",
|
||||
"id": 54,
|
||||
"name": "test",
|
||||
"physical_address": "",
|
||||
"region": None,
|
||||
"shipping_address": "",
|
||||
"slug": "test",
|
||||
"tenant": None
|
||||
}
|
||||
}
|
||||
```
|
||||
When a change is detected, any resulting webhooks are placed into a Redis queue for processing. This allows the user's request to complete without needing to wait for the outgoing webhook(s) to be processed. The webhooks are then extracted from the queue by the `rqworker` process and HTTP requests are sent to their respective destinations. The current webhook queue and any failed webhooks can be inspected in the admin UI under Django RQ > Queues.
|
||||
|
||||
A request is considered successful if the response status code is any one of a list of "good" statuses defined in the [requests library](https://github.com/requests/requests/blob/205755834d34a8a6ecf2b0b5b2e9c3e6a7f4e4b6/requests/models.py#L688), otherwise the request is marked as having failed. The user may manually retry a failed request.
|
||||
|
||||
## Backend Status
|
||||
|
||||
Django-rq includes a status page in the admin site which can be used to view the result of processed webhooks and manually retry any failed webhooks. Access it from http://netbox.local/admin/webhook-backend-status/.
|
||||
A request is considered successful if the response has a 2XX status code; otherwise, the request is marked as having failed. Failed requests may be retried manually via the admin UI.
|
||||
|
||||
@@ -109,6 +109,20 @@ In order to send email, NetBox needs an email server configured. The following i
|
||||
* TIMEOUT - Amount of time to wait for a connection (seconds)
|
||||
* FROM_EMAIL - Sender address for emails sent by NetBox
|
||||
|
||||
Email is sent from NetBox only for critical events. If you would like to test the email server configuration please use the django function [send_mail()](https://docs.djangoproject.com/en/stable/topics/email/#send-mail):
|
||||
|
||||
```
|
||||
# python ./manage.py nbshell
|
||||
>>> from django.core.mail import send_mail
|
||||
>>> send_mail(
|
||||
'Test Email Subject',
|
||||
'Test Email Body',
|
||||
'noreply-netbox@example.com',
|
||||
['users@example.com'],
|
||||
fail_silently=False
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## EXEMPT_VIEW_PERMISSIONS
|
||||
|
||||
@@ -21,7 +21,7 @@ NetBox requires access to a PostgreSQL database service to store data. This serv
|
||||
* `PASSWORD` - PostgreSQL password
|
||||
* `HOST` - Name or IP address of the database server (use `localhost` if running locally)
|
||||
* `PORT` - TCP port of the PostgreSQL service; leave blank for default port (5432)
|
||||
* `CONN_MAX_AGE` - Number in seconds for Netbox to keep database connections open. 150-300 seconds is typically a good starting point ([more info](https://docs.djangoproject.com/en/stable/ref/databases/#persistent-connections)).
|
||||
* `CONN_MAX_AGE` - Lifetime of a [persistent database connection](https://docs.djangoproject.com/en/stable/ref/databases/#persistent-connections), in seconds (150-300 is recommended)
|
||||
|
||||
Example:
|
||||
|
||||
@@ -36,6 +36,9 @@ DATABASE = {
|
||||
}
|
||||
```
|
||||
|
||||
!!! note
|
||||
NetBox supports all PostgreSQL database options supported by the underlying Django framework. For a complete list of available parameters, please see [the Django documentation](https://docs.djangoproject.com/en/stable/ref/settings/#databases).
|
||||
|
||||
---
|
||||
|
||||
## REDIS
|
||||
@@ -77,14 +80,56 @@ REDIS = {
|
||||
}
|
||||
```
|
||||
|
||||
!!! note:
|
||||
!!! note
|
||||
If you are upgrading from a version prior to v2.7, please note that the Redis connection configuration settings have
|
||||
changed. Manual modification to bring the `REDIS` section inline with the above specification is necessary
|
||||
|
||||
!!! warning:
|
||||
!!! note
|
||||
It is highly recommended to keep the webhook and cache databases separate. Using the same database number on the
|
||||
same Redis instance for both may result in webhook processing data being lost during cache flushing events.
|
||||
|
||||
### Using Redis Sentinel
|
||||
|
||||
If you are using [Redis Sentinel](https://redis.io/topics/sentinel) for high-availability purposes, there is minimal
|
||||
configuration necessary to convert NetBox to recognize it. It requires the removal of the `HOST` and `PORT` keys from
|
||||
above and the addition of two new keys.
|
||||
|
||||
* `SENTINELS`: List of tuples or tuple of tuples with each inner tuple containing the name or IP address
|
||||
of the Redis server and port for each sentinel instance to connect to
|
||||
* `SENTINEL_SERVICE`: Name of the master / service to connect to
|
||||
|
||||
Example:
|
||||
|
||||
```python
|
||||
REDIS = {
|
||||
'webhooks': {
|
||||
'SENTINELS': [('mysentinel.redis.example.com', 6379)],
|
||||
'SENTINEL_SERVICE': 'netbox',
|
||||
'PASSWORD': '',
|
||||
'DATABASE': 0,
|
||||
'DEFAULT_TIMEOUT': 300,
|
||||
'SSL': False,
|
||||
},
|
||||
'caching': {
|
||||
'SENTINELS': [
|
||||
('mysentinel.redis.example.com', 6379),
|
||||
('othersentinel.redis.example.com', 6379)
|
||||
],
|
||||
'SENTINEL_SERVICE': 'netbox',
|
||||
'PASSWORD': '',
|
||||
'DATABASE': 1,
|
||||
'DEFAULT_TIMEOUT': 300,
|
||||
'SSL': False,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
!!! note
|
||||
It is possible to have only one or the other Redis configurations to use Sentinel functionality. It is possible
|
||||
for example to have the webhook use sentinel via `HOST`/`PORT` and for caching to use Sentinel via
|
||||
`SENTINELS`/`SENTINEL_SERVICE`.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## SECRET_KEY
|
||||
|
||||
@@ -32,7 +32,7 @@ pycodestyle --ignore=W504,E501 netbox/
|
||||
|
||||
The introduction of a new dependency is best avoided unless it is absolutely necessary. For small features, it's generally preferable to replicate functionality within the NetBox code base rather than to introduce reliance on an external project. This reduces both the burden of tracking new releases and our exposure to outside bugs and attacks.
|
||||
|
||||
If there's a strong case for introducing a new depdency, it must meet the following criteria:
|
||||
If there's a strong case for introducing a new dependency, it must meet the following criteria:
|
||||
|
||||
* Its complete source code must be published and freely accessible without registration.
|
||||
* Its license must be conducive to inclusion in an open source project.
|
||||
@@ -45,10 +45,18 @@ When adding a new dependency, a short description of the package and the URL of
|
||||
|
||||
* When in doubt, remain consistent: It is better to be consistently incorrect than inconsistently correct. If you notice in the course of unrelated work a pattern that should be corrected, continue to follow the pattern for now and open a bug so that the entire code base can be evaluated at a later point.
|
||||
|
||||
* Prioritize readability over concision. Python is a very flexible language that typically gives us several options for expressing a given piece of logic, but some may be more friendly to the reader than others. (List comprehensions are particularly vulnerable to over-optimization.) Always remain considerate of the future reader who may need to interpret your code without the benefit of the context within which you are writing it.
|
||||
|
||||
* No easter eggs. While they can be fun, NetBox must be considered as a business-critical tool. The potential, however minor, for introducing a bug caused by unnecessary logic is best avoided entirely.
|
||||
|
||||
* Constants (variables which generally do not change) should be declared in `constants.py` within each app. Wildcard imports from the file are acceptable.
|
||||
|
||||
* Every model should have a docstring. Every custom method should include an expalantion of its function.
|
||||
* Every model should have a docstring. Every custom method should include an explanation of its function.
|
||||
|
||||
* Nested API serializers generate minimal representations of an object. These are stored separately from the primary serializers to avoid circular dependencies. Always import nested serializers from other apps directly. For example, from within the DCIM app you would write `from ipam.api.nested_serializers import NestedIPAddressSerializer`.
|
||||
|
||||
## Branding
|
||||
|
||||
* When referring to NetBox in writing, use the proper form "NetBox," with the letters N and B capitalized. The lowercase form "netbox" should be used in code, filenames, etc. But never "Netbox" or any other deviation.
|
||||
|
||||
* There is an SVG form of the NetBox logo at [docs/netbox_logo.svg](../netbox_logo.svg). It is preferred to use this logo for all purposes as it scales to arbitrary sizes without loss of resolution. If a raster image is required, the SVG logo should be converted to a PNG image of the prescribed size.
|
||||
|
||||
@@ -99,6 +99,9 @@ Save the contents of the above example in `/etc/apache2/sites-available/netbox.c
|
||||
|
||||
To enable SSL, consider this guide on [securing Apache with Let's Encrypt](https://www.digitalocean.com/community/tutorials/how-to-secure-apache-with-let-s-encrypt-on-ubuntu-16-04).
|
||||
|
||||
!!! note
|
||||
Certain components of NetBox (such as the display of rack elevation diagrams) rely on the use of embedded objects. Ensure that your HTTP server configuration does not override the `X-Frame-Options` response header set by NetBox.
|
||||
|
||||
# gunicorn Installation
|
||||
|
||||
Install gunicorn:
|
||||
|
||||
@@ -4,7 +4,7 @@ The following sections detail how to set up a new instance of NetBox:
|
||||
|
||||
1. [PostgreSQL database](1-postgresql.md)
|
||||
2. [NetBox components](2-netbox.md)
|
||||
3. [HTTP dameon](3-http-daemon.md)
|
||||
3. [HTTP daemon](3-http-daemon.md)
|
||||
4. [LDAP authentication](4-ldap.md) (optional)
|
||||
|
||||
# Upgrading
|
||||
|
||||
@@ -88,7 +88,7 @@ Finally, restart the WSGI services to run the new code. If you followed this gui
|
||||
|
||||
```no-highlight
|
||||
# sudo systemctl restart netbox
|
||||
# sudo systemctl restart netbox-rqworker
|
||||
# sudo systemctl restart netbox-rq
|
||||
```
|
||||
|
||||
!!! note
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 336 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 134 KiB After Width: | Height: | Size: 336 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 339 KiB |
@@ -1,3 +1,111 @@
|
||||
# v2.7.8 (2020-02-25)
|
||||
|
||||
## Enhancements
|
||||
|
||||
* [#3145](https://github.com/netbox-community/netbox/issues/3145) - Add a "decommissioning" cable status
|
||||
* [#4173](https://github.com/netbox-community/netbox/issues/4173) - Return graceful error message when webhook queuing fails
|
||||
* [#4227](https://github.com/netbox-community/netbox/issues/4227) - Omit internal fields from the change log data
|
||||
* [#4237](https://github.com/netbox-community/netbox/issues/4237) - Support Jinja2 templating for webhook payload and headers
|
||||
* [#4262](https://github.com/netbox-community/netbox/issues/4262) - Extend custom scripts to pass the `commit` value via `run()`
|
||||
* [#4267](https://github.com/netbox-community/netbox/issues/4267) - Denote rack role on rack elevations list
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
* [#4221](https://github.com/netbox-community/netbox/issues/4221) - Fix exception when deleting a device with interface connections when an interfaces webhook is defined
|
||||
* [#4222](https://github.com/netbox-community/netbox/issues/4222) - Escape double quotes on encapsulated values during CSV export
|
||||
* [#4224](https://github.com/netbox-community/netbox/issues/4224) - Fix display of rear device image if front image is not defined
|
||||
* [#4228](https://github.com/netbox-community/netbox/issues/4228) - Improve fit of device images in rack elevations
|
||||
* [#4230](https://github.com/netbox-community/netbox/issues/4230) - Fix rack units filtering on elevation endpoint
|
||||
* [#4232](https://github.com/netbox-community/netbox/issues/4232) - Enforce consistent background striping in rack elevations
|
||||
* [#4235](https://github.com/netbox-community/netbox/issues/4235) - Fix API representation of `content_type` for export templates
|
||||
* [#4239](https://github.com/netbox-community/netbox/issues/4239) - Fix exception when selecting all filtered objects during bulk edit
|
||||
* [#4240](https://github.com/netbox-community/netbox/issues/4240) - Fix exception when filtering foreign keys by NULL
|
||||
* [#4241](https://github.com/netbox-community/netbox/issues/4241) - Correct IP address hyperlinks on interface view
|
||||
* [#4246](https://github.com/netbox-community/netbox/issues/4246) - Fix duplication of field attributes when multiple IPNetworkVars are present in a script
|
||||
* [#4252](https://github.com/netbox-community/netbox/issues/4252) - Fix power port assignment for power outlet templates created via REST API
|
||||
* [#4272](https://github.com/netbox-community/netbox/issues/4272) - Interface type should be required by API serializer
|
||||
|
||||
---
|
||||
|
||||
# v2.7.7 (2020-02-20)
|
||||
|
||||
**Note:** This release fixes a bug affecting the natural ordering of interfaces. If any interfaces appear unordered in
|
||||
NetBox, run the following management command to recalculate their naturalized values after upgrading:
|
||||
|
||||
```
|
||||
python3 manage.py renaturalize dcim.Interface
|
||||
```
|
||||
|
||||
## Enhancements
|
||||
|
||||
* [#1529](https://github.com/netbox-community/netbox/issues/1529) - Enable display of device images in rack elevations
|
||||
* [#2511](https://github.com/netbox-community/netbox/issues/2511) - Compare object change to the previous change
|
||||
* [#3810](https://github.com/netbox-community/netbox/issues/3810) - Preserve slug value when editing existing objects
|
||||
* [#3840](https://github.com/netbox-community/netbox/issues/3840) - Enhance search function when selecting VLANs for interface assignment
|
||||
* [#4170](https://github.com/netbox-community/netbox/issues/4170) - Improve color contrast in rack elevation drawings
|
||||
* [#4206](https://github.com/netbox-community/netbox/issues/4206) - Add RJ-11 console port type
|
||||
* [#4209](https://github.com/netbox-community/netbox/issues/4209) - Enable filtering interfaces list view by enabled
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
* [#2519](https://github.com/netbox-community/netbox/issues/2519) - Avoid race condition when provisioning "next available" IPs/prefixes via the API
|
||||
* [#3967](https://github.com/netbox-community/netbox/issues/3967) - Fix missing migration for interface templates of type "other"
|
||||
* [#4168](https://github.com/netbox-community/netbox/issues/4168) - Role is not required when creating a virtual machine
|
||||
* [#4175](https://github.com/netbox-community/netbox/issues/4175) - Fix potential exception when bulk editing objects from a filtered list
|
||||
* [#4179](https://github.com/netbox-community/netbox/issues/4179) - Site is required when creating a rack group or power panel
|
||||
* [#4183](https://github.com/netbox-community/netbox/issues/4183) - Fix representation of NaturalOrderingField values in change log
|
||||
* [#4194](https://github.com/netbox-community/netbox/issues/4194) - Role field should not be required when searching/filtering secrets
|
||||
* [#4196](https://github.com/netbox-community/netbox/issues/4196) - Fix exception when viewing LLDP neighbors page
|
||||
* [#4202](https://github.com/netbox-community/netbox/issues/4202) - Prevent reassignment to master device when bulk editing VC member interfaces
|
||||
* [#4204](https://github.com/netbox-community/netbox/issues/4204) - Fix assignment of mask length when bulk editing prefixes
|
||||
* [#4211](https://github.com/netbox-community/netbox/issues/4211) - Include trailing text when naturalizing interface names
|
||||
* [#4213](https://github.com/netbox-community/netbox/issues/4213) - Restore display of tags and custom fields on power feed view
|
||||
|
||||
---
|
||||
|
||||
# v2.7.6 (2020-02-13)
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
* [#4166](https://github.com/netbox-community/netbox/issues/4166) - Fix schema migrations to enforce maximum character length for naturalized fields
|
||||
|
||||
---
|
||||
|
||||
# v2.7.5 (2020-02-13)
|
||||
|
||||
**Note:** This release includes several database schema migrations that calculate and store copies of names for certain objects to improve natural ordering performance (see [#3799](https://github.com/netbox-community/netbox/issues/3799)). These migrations may take a few minutes to run if you have a very large number of objects defined in NetBox.
|
||||
|
||||
## Enhancements
|
||||
|
||||
* [#3766](https://github.com/netbox-community/netbox/issues/3766) - Allow custom script authors to specify the form widget for each variable
|
||||
* [#3799](https://github.com/netbox-community/netbox/issues/3799) - Greatly improve performance when ordering device components
|
||||
* [#3984](https://github.com/netbox-community/netbox/issues/3984) - Add support for Redis Sentinel
|
||||
* [#3986](https://github.com/netbox-community/netbox/issues/3986) - Include position numbers in SVG image when rendering rack elevations
|
||||
* [#4093](https://github.com/netbox-community/netbox/issues/4093) - Add more status choices for virtual machines
|
||||
* [#4100](https://github.com/netbox-community/netbox/issues/4100) - Add device filter to component list views
|
||||
* [#4113](https://github.com/netbox-community/netbox/issues/4113) - Add bulk edit functionality for device type components
|
||||
* [#4116](https://github.com/netbox-community/netbox/issues/4116) - Enable bulk edit and delete functions for device component list views
|
||||
* [#4129](https://github.com/netbox-community/netbox/issues/4129) - Add buttons to delete individual device type components
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
* [#3507](https://github.com/netbox-community/netbox/issues/3507) - Fix filtering IP addresses by multiple devices
|
||||
* [#3995](https://github.com/netbox-community/netbox/issues/3995) - Make dropdown menus in the navigation bar scrollable on small screens
|
||||
* [#4083](https://github.com/netbox-community/netbox/issues/4083) - Permit nullifying applicable choice fields via API requests
|
||||
* [#4089](https://github.com/netbox-community/netbox/issues/4089) - Selection of power outlet type during bulk update is optional
|
||||
* [#4090](https://github.com/netbox-community/netbox/issues/4090) - Render URL custom fields as links under object view
|
||||
* [#4091](https://github.com/netbox-community/netbox/issues/4091) - Fix filtering of objects by custom fields using UI search form
|
||||
* [#4099](https://github.com/netbox-community/netbox/issues/4099) - Linkify interfaces on global interfaces list
|
||||
* [#4108](https://github.com/netbox-community/netbox/issues/4108) - Avoid extraneous database queries when rendering search forms
|
||||
* [#4134](https://github.com/netbox-community/netbox/issues/4134) - Device power ports and outlets should inherit type from the parent device type
|
||||
* [#4137](https://github.com/netbox-community/netbox/issues/4137) - Disable occupied terminations when connecting a cable to a circuit
|
||||
* [#4138](https://github.com/netbox-community/netbox/issues/4138) - Restore device bay counts in rack elevation diagrams
|
||||
* [#4146](https://github.com/netbox-community/netbox/issues/4146) - Fix enforcement of secret role assignment for secret decryption
|
||||
* [#4150](https://github.com/netbox-community/netbox/issues/4150) - Correct YAML rendering of config contexts
|
||||
* [#4159](https://github.com/netbox-community/netbox/issues/4159) - Fix implementation of Redis caching configuration
|
||||
|
||||
---
|
||||
|
||||
# v2.7.4 (2020-02-04)
|
||||
|
||||
## Enhancements
|
||||
|
||||
@@ -41,7 +41,6 @@ pages:
|
||||
- Prometheus Metrics: 'additional-features/prometheus-metrics.md'
|
||||
- Reports: 'additional-features/reports.md'
|
||||
- Tags: 'additional-features/tags.md'
|
||||
- Topology Maps: 'additional-features/topology-maps.md'
|
||||
- Webhooks: 'additional-features/webhooks.md'
|
||||
- Administration:
|
||||
- Replicating NetBox: 'administration/replicating-netbox.md'
|
||||
|
||||
@@ -15,15 +15,15 @@ router = routers.DefaultRouter()
|
||||
router.APIRootView = CircuitsRootView
|
||||
|
||||
# Field choices
|
||||
router.register(r'_choices', views.CircuitsFieldChoicesViewSet, basename='field-choice')
|
||||
router.register('_choices', views.CircuitsFieldChoicesViewSet, basename='field-choice')
|
||||
|
||||
# Providers
|
||||
router.register(r'providers', views.ProviderViewSet)
|
||||
router.register('providers', views.ProviderViewSet)
|
||||
|
||||
# Circuits
|
||||
router.register(r'circuit-types', views.CircuitTypeViewSet)
|
||||
router.register(r'circuits', views.CircuitViewSet)
|
||||
router.register(r'circuit-terminations', views.CircuitTerminationViewSet)
|
||||
router.register('circuit-types', views.CircuitTypeViewSet)
|
||||
router.register('circuits', views.CircuitViewSet)
|
||||
router.register('circuit-terminations', views.CircuitTerminationViewSet)
|
||||
|
||||
app_name = 'circuits-api'
|
||||
urlpatterns = router.urls
|
||||
|
||||
@@ -9,7 +9,8 @@ from tenancy.forms import TenancyFilterForm, TenancyForm
|
||||
from tenancy.models import Tenant
|
||||
from utilities.forms import (
|
||||
APISelect, APISelectMultiple, add_blank_choice, BootstrapMixin, CommentField, CSVChoiceField, DatePicker,
|
||||
FilterChoiceField, SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple, TagFilterField
|
||||
DynamicModelChoiceField, DynamicModelMultipleChoiceField, SmallTextarea, SlugField, StaticSelect2,
|
||||
StaticSelect2Multiple, TagFilterField,
|
||||
)
|
||||
from .choices import CircuitStatusChoices
|
||||
from .models import Circuit, CircuitTermination, CircuitType, Provider
|
||||
@@ -107,7 +108,7 @@ class ProviderFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
required=False,
|
||||
label='Search'
|
||||
)
|
||||
region = FilterChoiceField(
|
||||
region = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
to_field_name='slug',
|
||||
required=False,
|
||||
@@ -119,9 +120,10 @@ class ProviderFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
}
|
||||
)
|
||||
)
|
||||
site = FilterChoiceField(
|
||||
site = DynamicModelMultipleChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
required=False,
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/dcim/sites/",
|
||||
value_field="slug",
|
||||
@@ -164,6 +166,18 @@ class CircuitTypeCSVForm(forms.ModelForm):
|
||||
#
|
||||
|
||||
class CircuitForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||
provider = DynamicModelChoiceField(
|
||||
queryset=Provider.objects.all(),
|
||||
widget=APISelect(
|
||||
api_url="/api/circuits/providers/"
|
||||
)
|
||||
)
|
||||
type = DynamicModelChoiceField(
|
||||
queryset=CircuitType.objects.all(),
|
||||
widget=APISelect(
|
||||
api_url="/api/circuits/circuit-types/"
|
||||
)
|
||||
)
|
||||
comments = CommentField()
|
||||
tags = TagField(
|
||||
required=False
|
||||
@@ -180,12 +194,6 @@ class CircuitForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||
'commit_rate': "Committed rate",
|
||||
}
|
||||
widgets = {
|
||||
'provider': APISelect(
|
||||
api_url="/api/circuits/providers/"
|
||||
),
|
||||
'type': APISelect(
|
||||
api_url="/api/circuits/circuit-types/"
|
||||
),
|
||||
'status': StaticSelect2(),
|
||||
'install_date': DatePicker(),
|
||||
}
|
||||
@@ -235,14 +243,14 @@ class CircuitBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEdit
|
||||
queryset=Circuit.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
)
|
||||
type = forms.ModelChoiceField(
|
||||
type = DynamicModelChoiceField(
|
||||
queryset=CircuitType.objects.all(),
|
||||
required=False,
|
||||
widget=APISelect(
|
||||
api_url="/api/circuits/circuit-types/"
|
||||
)
|
||||
)
|
||||
provider = forms.ModelChoiceField(
|
||||
provider = DynamicModelChoiceField(
|
||||
queryset=Provider.objects.all(),
|
||||
required=False,
|
||||
widget=APISelect(
|
||||
@@ -255,7 +263,7 @@ class CircuitBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEdit
|
||||
initial='',
|
||||
widget=StaticSelect2()
|
||||
)
|
||||
tenant = forms.ModelChoiceField(
|
||||
tenant = DynamicModelChoiceField(
|
||||
queryset=Tenant.objects.all(),
|
||||
required=False,
|
||||
widget=APISelect(
|
||||
@@ -290,17 +298,19 @@ class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm
|
||||
required=False,
|
||||
label='Search'
|
||||
)
|
||||
type = FilterChoiceField(
|
||||
type = DynamicModelMultipleChoiceField(
|
||||
queryset=CircuitType.objects.all(),
|
||||
to_field_name='slug',
|
||||
required=False,
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/circuits/circuit-types/",
|
||||
value_field="slug",
|
||||
)
|
||||
)
|
||||
provider = FilterChoiceField(
|
||||
provider = DynamicModelMultipleChoiceField(
|
||||
queryset=Provider.objects.all(),
|
||||
to_field_name='slug',
|
||||
required=False,
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/circuits/providers/",
|
||||
value_field="slug",
|
||||
@@ -311,7 +321,7 @@ class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm
|
||||
required=False,
|
||||
widget=StaticSelect2Multiple()
|
||||
)
|
||||
region = forms.ModelMultipleChoiceField(
|
||||
region = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
to_field_name='slug',
|
||||
required=False,
|
||||
@@ -323,9 +333,10 @@ class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm
|
||||
}
|
||||
)
|
||||
)
|
||||
site = FilterChoiceField(
|
||||
site = DynamicModelMultipleChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
required=False,
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/dcim/sites/",
|
||||
value_field="slug",
|
||||
|
||||
@@ -6,7 +6,7 @@ from utilities.tables import BaseTable, ToggleColumn
|
||||
from .models import Circuit, CircuitType, Provider
|
||||
|
||||
CIRCUITTYPE_ACTIONS = """
|
||||
<a href="{% url 'circuits:circuittype_changelog' slug=record.slug %}" class="btn btn-default btn-xs" title="Changelog">
|
||||
<a href="{% url 'circuits:circuittype_changelog' slug=record.slug %}" class="btn btn-default btn-xs" title="Change log">
|
||||
<i class="fa fa-history"></i>
|
||||
</a>
|
||||
{% if perms.circuit.change_circuittype %}
|
||||
|
||||
@@ -2,10 +2,10 @@ import datetime
|
||||
|
||||
from circuits.choices import *
|
||||
from circuits.models import Circuit, CircuitType, Provider
|
||||
from utilities.testing import StandardTestCases
|
||||
from utilities.testing import ViewTestCases
|
||||
|
||||
|
||||
class ProviderTestCase(StandardTestCases.Views):
|
||||
class ProviderTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
model = Provider
|
||||
|
||||
@classmethod
|
||||
@@ -46,14 +46,9 @@ class ProviderTestCase(StandardTestCases.Views):
|
||||
}
|
||||
|
||||
|
||||
class CircuitTypeTestCase(StandardTestCases.Views):
|
||||
class CircuitTypeTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
||||
model = CircuitType
|
||||
|
||||
# Disable inapplicable tests
|
||||
test_get_object = None
|
||||
test_delete_object = None
|
||||
test_bulk_edit_objects = None
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
@@ -77,7 +72,7 @@ class CircuitTypeTestCase(StandardTestCases.Views):
|
||||
)
|
||||
|
||||
|
||||
class CircuitTestCase(StandardTestCases.Views):
|
||||
class CircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
model = Circuit
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -9,42 +9,42 @@ app_name = 'circuits'
|
||||
urlpatterns = [
|
||||
|
||||
# Providers
|
||||
path(r'providers/', views.ProviderListView.as_view(), name='provider_list'),
|
||||
path(r'providers/add/', views.ProviderCreateView.as_view(), name='provider_add'),
|
||||
path(r'providers/import/', views.ProviderBulkImportView.as_view(), name='provider_import'),
|
||||
path(r'providers/edit/', views.ProviderBulkEditView.as_view(), name='provider_bulk_edit'),
|
||||
path(r'providers/delete/', views.ProviderBulkDeleteView.as_view(), name='provider_bulk_delete'),
|
||||
path(r'providers/<slug:slug>/', views.ProviderView.as_view(), name='provider'),
|
||||
path(r'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(r'providers/<slug:slug>/changelog/', ObjectChangeLogView.as_view(), name='provider_changelog', kwargs={'model': Provider}),
|
||||
path('providers/', views.ProviderListView.as_view(), name='provider_list'),
|
||||
path('providers/add/', views.ProviderCreateView.as_view(), name='provider_add'),
|
||||
path('providers/import/', views.ProviderBulkImportView.as_view(), name='provider_import'),
|
||||
path('providers/edit/', views.ProviderBulkEditView.as_view(), name='provider_bulk_edit'),
|
||||
path('providers/delete/', views.ProviderBulkDeleteView.as_view(), name='provider_bulk_delete'),
|
||||
path('providers/<slug:slug>/', views.ProviderView.as_view(), name='provider'),
|
||||
path('providers/<slug:slug>/edit/', views.ProviderEditView.as_view(), name='provider_edit'),
|
||||
path('providers/<slug:slug>/delete/', views.ProviderDeleteView.as_view(), name='provider_delete'),
|
||||
path('providers/<slug:slug>/changelog/', ObjectChangeLogView.as_view(), name='provider_changelog', kwargs={'model': Provider}),
|
||||
|
||||
# Circuit types
|
||||
path(r'circuit-types/', views.CircuitTypeListView.as_view(), name='circuittype_list'),
|
||||
path(r'circuit-types/add/', views.CircuitTypeCreateView.as_view(), name='circuittype_add'),
|
||||
path(r'circuit-types/import/', views.CircuitTypeBulkImportView.as_view(), name='circuittype_import'),
|
||||
path(r'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(r'circuit-types/<slug:slug>/changelog/', ObjectChangeLogView.as_view(), name='circuittype_changelog', kwargs={'model': CircuitType}),
|
||||
path('circuit-types/', views.CircuitTypeListView.as_view(), name='circuittype_list'),
|
||||
path('circuit-types/add/', views.CircuitTypeCreateView.as_view(), name='circuittype_add'),
|
||||
path('circuit-types/import/', views.CircuitTypeBulkImportView.as_view(), name='circuittype_import'),
|
||||
path('circuit-types/delete/', views.CircuitTypeBulkDeleteView.as_view(), name='circuittype_bulk_delete'),
|
||||
path('circuit-types/<slug:slug>/edit/', views.CircuitTypeEditView.as_view(), name='circuittype_edit'),
|
||||
path('circuit-types/<slug:slug>/changelog/', ObjectChangeLogView.as_view(), name='circuittype_changelog', kwargs={'model': CircuitType}),
|
||||
|
||||
# Circuits
|
||||
path(r'circuits/', views.CircuitListView.as_view(), name='circuit_list'),
|
||||
path(r'circuits/add/', views.CircuitCreateView.as_view(), name='circuit_add'),
|
||||
path(r'circuits/import/', views.CircuitBulkImportView.as_view(), name='circuit_import'),
|
||||
path(r'circuits/edit/', views.CircuitBulkEditView.as_view(), name='circuit_bulk_edit'),
|
||||
path(r'circuits/delete/', views.CircuitBulkDeleteView.as_view(), name='circuit_bulk_delete'),
|
||||
path(r'circuits/<int:pk>/', views.CircuitView.as_view(), name='circuit'),
|
||||
path(r'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(r'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/', views.CircuitListView.as_view(), name='circuit_list'),
|
||||
path('circuits/add/', views.CircuitCreateView.as_view(), name='circuit_add'),
|
||||
path('circuits/import/', views.CircuitBulkImportView.as_view(), name='circuit_import'),
|
||||
path('circuits/edit/', views.CircuitBulkEditView.as_view(), name='circuit_bulk_edit'),
|
||||
path('circuits/delete/', views.CircuitBulkDeleteView.as_view(), name='circuit_bulk_delete'),
|
||||
path('circuits/<int:pk>/', views.CircuitView.as_view(), name='circuit'),
|
||||
path('circuits/<int:pk>/edit/', views.CircuitEditView.as_view(), name='circuit_edit'),
|
||||
path('circuits/<int:pk>/delete/', views.CircuitDeleteView.as_view(), name='circuit_delete'),
|
||||
path('circuits/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='circuit_changelog', kwargs={'model': Circuit}),
|
||||
path('circuits/<int:pk>/terminations/swap/', views.circuit_terminations_swap, name='circuit_terminations_swap'),
|
||||
|
||||
# Circuit terminations
|
||||
|
||||
path(r'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(r'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(r'circuit-terminations/<int:pk>/trace/', CableTraceView.as_view(), name='circuittermination_trace', kwargs={'model': CircuitTermination}),
|
||||
path('circuits/<int:circuit>/terminations/add/', views.CircuitTerminationCreateView.as_view(), name='circuittermination_add'),
|
||||
path('circuit-terminations/<int:pk>/edit/', views.CircuitTerminationEditView.as_view(), name='circuittermination_edit'),
|
||||
path('circuit-terminations/<int:pk>/delete/', views.CircuitTerminationDeleteView.as_view(), name='circuittermination_delete'),
|
||||
path('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:pk>/trace/', CableTraceView.as_view(), name='circuittermination_trace', kwargs={'model': CircuitTermination}),
|
||||
|
||||
]
|
||||
|
||||
@@ -29,7 +29,6 @@ class ProviderListView(PermissionRequiredMixin, ObjectListView):
|
||||
filterset = filters.ProviderFilterSet
|
||||
filterset_form = forms.ProviderFilterForm
|
||||
table = tables.ProviderDetailTable
|
||||
template_name = 'circuits/provider_list.html'
|
||||
|
||||
|
||||
class ProviderView(PermissionRequiredMixin, View):
|
||||
@@ -107,7 +106,6 @@ class CircuitTypeListView(PermissionRequiredMixin, ObjectListView):
|
||||
permission_required = 'circuits.view_circuittype'
|
||||
queryset = CircuitType.objects.annotate(circuit_count=Count('circuits'))
|
||||
table = tables.CircuitTypeTable
|
||||
template_name = 'circuits/circuittype_list.html'
|
||||
|
||||
|
||||
class CircuitTypeCreateView(PermissionRequiredMixin, ObjectEditView):
|
||||
@@ -151,7 +149,6 @@ class CircuitListView(PermissionRequiredMixin, ObjectListView):
|
||||
filterset = filters.CircuitFilterSet
|
||||
filterset_form = forms.CircuitFilterForm
|
||||
table = tables.CircuitTable
|
||||
template_name = 'circuits/circuit_list.html'
|
||||
|
||||
|
||||
class CircuitView(PermissionRequiredMixin, View):
|
||||
|
||||
@@ -3,8 +3,8 @@ from rest_framework import serializers
|
||||
from dcim.constants import CONNECTION_STATUS_CHOICES
|
||||
from dcim.models import (
|
||||
Cable, ConsolePort, ConsoleServerPort, Device, DeviceBay, DeviceType, DeviceRole, FrontPort, FrontPortTemplate,
|
||||
Interface, Manufacturer, Platform, PowerFeed, PowerOutlet, PowerPanel, PowerPort, Rack, RackGroup, RackRole,
|
||||
RearPort, RearPortTemplate, Region, Site, VirtualChassis,
|
||||
Interface, Manufacturer, Platform, PowerFeed, PowerOutlet, PowerPanel, PowerPort, PowerPortTemplate, Rack,
|
||||
RackGroup, RackRole, RearPort, RearPortTemplate, Region, Site, VirtualChassis,
|
||||
)
|
||||
from utilities.api import ChoiceField, WritableNestedSerializer
|
||||
|
||||
@@ -25,6 +25,7 @@ __all__ = [
|
||||
'NestedPowerOutletSerializer',
|
||||
'NestedPowerPanelSerializer',
|
||||
'NestedPowerPortSerializer',
|
||||
'NestedPowerPortTemplateSerializer',
|
||||
'NestedRackGroupSerializer',
|
||||
'NestedRackRoleSerializer',
|
||||
'NestedRackSerializer',
|
||||
@@ -111,6 +112,14 @@ class NestedDeviceTypeSerializer(WritableNestedSerializer):
|
||||
fields = ['id', 'url', 'manufacturer', 'model', 'slug', 'display_name', 'device_count']
|
||||
|
||||
|
||||
class NestedPowerPortTemplateSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerporttemplate-detail')
|
||||
|
||||
class Meta:
|
||||
model = PowerPortTemplate
|
||||
fields = ['id', 'url', 'name']
|
||||
|
||||
|
||||
class NestedRearPortTemplateSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearporttemplate-detail')
|
||||
|
||||
|
||||
@@ -117,9 +117,9 @@ class RackSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||
status = ChoiceField(choices=RackStatusChoices, required=False)
|
||||
role = NestedRackRoleSerializer(required=False, allow_null=True)
|
||||
type = ChoiceField(choices=RackTypeChoices, required=False, allow_null=True)
|
||||
type = ChoiceField(choices=RackTypeChoices, allow_blank=True, required=False)
|
||||
width = ChoiceField(choices=RackWidthChoices, required=False)
|
||||
outer_unit = ChoiceField(choices=RackDimensionUnitChoices, required=False)
|
||||
outer_unit = ChoiceField(choices=RackDimensionUnitChoices, allow_blank=True, required=False)
|
||||
tags = TagListSerializerField(required=False)
|
||||
device_count = serializers.IntegerField(read_only=True)
|
||||
powerfeed_count = serializers.IntegerField(read_only=True)
|
||||
@@ -172,6 +172,10 @@ class RackReservationSerializer(ValidatedModelSerializer):
|
||||
|
||||
|
||||
class RackElevationDetailFilterSerializer(serializers.Serializer):
|
||||
q = serializers.CharField(
|
||||
required=False,
|
||||
default=None
|
||||
)
|
||||
face = serializers.ChoiceField(
|
||||
choices=DeviceFaceChoices,
|
||||
default=DeviceFaceChoices.FACE_FRONT
|
||||
@@ -186,6 +190,9 @@ class RackElevationDetailFilterSerializer(serializers.Serializer):
|
||||
unit_height = serializers.IntegerField(
|
||||
default=RACK_ELEVATION_UNIT_HEIGHT_DEFAULT
|
||||
)
|
||||
legend_width = serializers.IntegerField(
|
||||
default=RACK_ELEVATION_LEGEND_WIDTH_DEFAULT
|
||||
)
|
||||
exclude = serializers.IntegerField(
|
||||
required=False,
|
||||
default=None
|
||||
@@ -194,6 +201,10 @@ class RackElevationDetailFilterSerializer(serializers.Serializer):
|
||||
required=False,
|
||||
default=True
|
||||
)
|
||||
include_images = serializers.BooleanField(
|
||||
required=False,
|
||||
default=True
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
@@ -212,7 +223,7 @@ class ManufacturerSerializer(ValidatedModelSerializer):
|
||||
|
||||
class DeviceTypeSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
manufacturer = NestedManufacturerSerializer()
|
||||
subdevice_role = ChoiceField(choices=SubdeviceRoleChoices, required=False, allow_null=True)
|
||||
subdevice_role = ChoiceField(choices=SubdeviceRoleChoices, allow_blank=True, required=False)
|
||||
tags = TagListSerializerField(required=False)
|
||||
device_count = serializers.IntegerField(read_only=True)
|
||||
|
||||
@@ -220,7 +231,8 @@ class DeviceTypeSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
model = DeviceType
|
||||
fields = [
|
||||
'id', 'manufacturer', 'model', 'slug', 'display_name', 'part_number', 'u_height', 'is_full_depth',
|
||||
'subdevice_role', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'device_count',
|
||||
'subdevice_role', 'front_image', 'rear_image', 'comments', 'tags', 'custom_fields', 'created',
|
||||
'last_updated', 'device_count',
|
||||
]
|
||||
|
||||
|
||||
@@ -228,6 +240,7 @@ class ConsolePortTemplateSerializer(ValidatedModelSerializer):
|
||||
device_type = NestedDeviceTypeSerializer()
|
||||
type = ChoiceField(
|
||||
choices=ConsolePortTypeChoices,
|
||||
allow_blank=True,
|
||||
required=False
|
||||
)
|
||||
|
||||
@@ -240,6 +253,7 @@ class ConsoleServerPortTemplateSerializer(ValidatedModelSerializer):
|
||||
device_type = NestedDeviceTypeSerializer()
|
||||
type = ChoiceField(
|
||||
choices=ConsolePortTypeChoices,
|
||||
allow_blank=True,
|
||||
required=False
|
||||
)
|
||||
|
||||
@@ -252,6 +266,7 @@ class PowerPortTemplateSerializer(ValidatedModelSerializer):
|
||||
device_type = NestedDeviceTypeSerializer()
|
||||
type = ChoiceField(
|
||||
choices=PowerPortTypeChoices,
|
||||
allow_blank=True,
|
||||
required=False
|
||||
)
|
||||
|
||||
@@ -264,15 +279,16 @@ class PowerOutletTemplateSerializer(ValidatedModelSerializer):
|
||||
device_type = NestedDeviceTypeSerializer()
|
||||
type = ChoiceField(
|
||||
choices=PowerOutletTypeChoices,
|
||||
allow_blank=True,
|
||||
required=False
|
||||
)
|
||||
power_port = PowerPortTemplateSerializer(
|
||||
power_port = NestedPowerPortTemplateSerializer(
|
||||
required=False
|
||||
)
|
||||
feed_leg = ChoiceField(
|
||||
choices=PowerOutletFeedLegChoices,
|
||||
required=False,
|
||||
allow_null=True
|
||||
allow_blank=True,
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
@@ -282,7 +298,7 @@ class PowerOutletTemplateSerializer(ValidatedModelSerializer):
|
||||
|
||||
class InterfaceTemplateSerializer(ValidatedModelSerializer):
|
||||
device_type = NestedDeviceTypeSerializer()
|
||||
type = ChoiceField(choices=InterfaceTypeChoices, required=False)
|
||||
type = ChoiceField(choices=InterfaceTypeChoices)
|
||||
|
||||
class Meta:
|
||||
model = InterfaceTemplate
|
||||
@@ -351,7 +367,7 @@ class DeviceSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
platform = NestedPlatformSerializer(required=False, allow_null=True)
|
||||
site = NestedSiteSerializer()
|
||||
rack = NestedRackSerializer(required=False, allow_null=True)
|
||||
face = ChoiceField(choices=DeviceFaceChoices, required=False, allow_null=True)
|
||||
face = ChoiceField(choices=DeviceFaceChoices, allow_blank=True, required=False)
|
||||
status = ChoiceField(choices=DeviceStatusChoices, required=False)
|
||||
primary_ip = NestedIPAddressSerializer(read_only=True)
|
||||
primary_ip4 = NestedIPAddressSerializer(required=False, allow_null=True)
|
||||
@@ -420,6 +436,7 @@ class ConsoleServerPortSerializer(TaggitSerializer, ConnectedEndpointSerializer)
|
||||
device = NestedDeviceSerializer()
|
||||
type = ChoiceField(
|
||||
choices=ConsolePortTypeChoices,
|
||||
allow_blank=True,
|
||||
required=False
|
||||
)
|
||||
cable = NestedCableSerializer(read_only=True)
|
||||
@@ -437,6 +454,7 @@ class ConsolePortSerializer(TaggitSerializer, ConnectedEndpointSerializer):
|
||||
device = NestedDeviceSerializer()
|
||||
type = ChoiceField(
|
||||
choices=ConsolePortTypeChoices,
|
||||
allow_blank=True,
|
||||
required=False
|
||||
)
|
||||
cable = NestedCableSerializer(read_only=True)
|
||||
@@ -454,6 +472,7 @@ class PowerOutletSerializer(TaggitSerializer, ConnectedEndpointSerializer):
|
||||
device = NestedDeviceSerializer()
|
||||
type = ChoiceField(
|
||||
choices=PowerOutletTypeChoices,
|
||||
allow_blank=True,
|
||||
required=False
|
||||
)
|
||||
power_port = NestedPowerPortSerializer(
|
||||
@@ -461,8 +480,8 @@ class PowerOutletSerializer(TaggitSerializer, ConnectedEndpointSerializer):
|
||||
)
|
||||
feed_leg = ChoiceField(
|
||||
choices=PowerOutletFeedLegChoices,
|
||||
required=False,
|
||||
allow_null=True
|
||||
allow_blank=True,
|
||||
required=False
|
||||
)
|
||||
cable = NestedCableSerializer(
|
||||
read_only=True
|
||||
@@ -483,6 +502,7 @@ class PowerPortSerializer(TaggitSerializer, ConnectedEndpointSerializer):
|
||||
device = NestedDeviceSerializer()
|
||||
type = ChoiceField(
|
||||
choices=PowerPortTypeChoices,
|
||||
allow_blank=True,
|
||||
required=False
|
||||
)
|
||||
cable = NestedCableSerializer(read_only=True)
|
||||
@@ -498,9 +518,9 @@ class PowerPortSerializer(TaggitSerializer, ConnectedEndpointSerializer):
|
||||
|
||||
class InterfaceSerializer(TaggitSerializer, ConnectedEndpointSerializer):
|
||||
device = NestedDeviceSerializer()
|
||||
type = ChoiceField(choices=InterfaceTypeChoices, required=False)
|
||||
type = ChoiceField(choices=InterfaceTypeChoices)
|
||||
lag = NestedInterfaceSerializer(required=False, allow_null=True)
|
||||
mode = ChoiceField(choices=InterfaceModeChoices, required=False, allow_null=True)
|
||||
mode = ChoiceField(choices=InterfaceModeChoices, allow_blank=True, required=False)
|
||||
untagged_vlan = NestedVLANSerializer(required=False, allow_null=True)
|
||||
tagged_vlans = SerializedPKRelatedField(
|
||||
queryset=VLAN.objects.all(),
|
||||
@@ -617,7 +637,7 @@ class CableSerializer(ValidatedModelSerializer):
|
||||
termination_a = serializers.SerializerMethodField(read_only=True)
|
||||
termination_b = serializers.SerializerMethodField(read_only=True)
|
||||
status = ChoiceField(choices=CableStatusChoices, required=False)
|
||||
length_unit = ChoiceField(choices=CableLengthUnitChoices, required=False, allow_null=True)
|
||||
length_unit = ChoiceField(choices=CableLengthUnitChoices, allow_blank=True, required=False)
|
||||
|
||||
class Meta:
|
||||
model = Cable
|
||||
|
||||
@@ -15,65 +15,65 @@ router = routers.DefaultRouter()
|
||||
router.APIRootView = DCIMRootView
|
||||
|
||||
# Field choices
|
||||
router.register(r'_choices', views.DCIMFieldChoicesViewSet, basename='field-choice')
|
||||
router.register('_choices', views.DCIMFieldChoicesViewSet, basename='field-choice')
|
||||
|
||||
# Sites
|
||||
router.register(r'regions', views.RegionViewSet)
|
||||
router.register(r'sites', views.SiteViewSet)
|
||||
router.register('regions', views.RegionViewSet)
|
||||
router.register('sites', views.SiteViewSet)
|
||||
|
||||
# Racks
|
||||
router.register(r'rack-groups', views.RackGroupViewSet)
|
||||
router.register(r'rack-roles', views.RackRoleViewSet)
|
||||
router.register(r'racks', views.RackViewSet)
|
||||
router.register(r'rack-reservations', views.RackReservationViewSet)
|
||||
router.register('rack-groups', views.RackGroupViewSet)
|
||||
router.register('rack-roles', views.RackRoleViewSet)
|
||||
router.register('racks', views.RackViewSet)
|
||||
router.register('rack-reservations', views.RackReservationViewSet)
|
||||
|
||||
# Device types
|
||||
router.register(r'manufacturers', views.ManufacturerViewSet)
|
||||
router.register(r'device-types', views.DeviceTypeViewSet)
|
||||
router.register('manufacturers', views.ManufacturerViewSet)
|
||||
router.register('device-types', views.DeviceTypeViewSet)
|
||||
|
||||
# Device type components
|
||||
router.register(r'console-port-templates', views.ConsolePortTemplateViewSet)
|
||||
router.register(r'console-server-port-templates', views.ConsoleServerPortTemplateViewSet)
|
||||
router.register(r'power-port-templates', views.PowerPortTemplateViewSet)
|
||||
router.register(r'power-outlet-templates', views.PowerOutletTemplateViewSet)
|
||||
router.register(r'interface-templates', views.InterfaceTemplateViewSet)
|
||||
router.register(r'front-port-templates', views.FrontPortTemplateViewSet)
|
||||
router.register(r'rear-port-templates', views.RearPortTemplateViewSet)
|
||||
router.register(r'device-bay-templates', views.DeviceBayTemplateViewSet)
|
||||
router.register('console-port-templates', views.ConsolePortTemplateViewSet)
|
||||
router.register('console-server-port-templates', views.ConsoleServerPortTemplateViewSet)
|
||||
router.register('power-port-templates', views.PowerPortTemplateViewSet)
|
||||
router.register('power-outlet-templates', views.PowerOutletTemplateViewSet)
|
||||
router.register('interface-templates', views.InterfaceTemplateViewSet)
|
||||
router.register('front-port-templates', views.FrontPortTemplateViewSet)
|
||||
router.register('rear-port-templates', views.RearPortTemplateViewSet)
|
||||
router.register('device-bay-templates', views.DeviceBayTemplateViewSet)
|
||||
|
||||
# Devices
|
||||
router.register(r'device-roles', views.DeviceRoleViewSet)
|
||||
router.register(r'platforms', views.PlatformViewSet)
|
||||
router.register(r'devices', views.DeviceViewSet)
|
||||
router.register('device-roles', views.DeviceRoleViewSet)
|
||||
router.register('platforms', views.PlatformViewSet)
|
||||
router.register('devices', views.DeviceViewSet)
|
||||
|
||||
# Device components
|
||||
router.register(r'console-ports', views.ConsolePortViewSet)
|
||||
router.register(r'console-server-ports', views.ConsoleServerPortViewSet)
|
||||
router.register(r'power-ports', views.PowerPortViewSet)
|
||||
router.register(r'power-outlets', views.PowerOutletViewSet)
|
||||
router.register(r'interfaces', views.InterfaceViewSet)
|
||||
router.register(r'front-ports', views.FrontPortViewSet)
|
||||
router.register(r'rear-ports', views.RearPortViewSet)
|
||||
router.register(r'device-bays', views.DeviceBayViewSet)
|
||||
router.register(r'inventory-items', views.InventoryItemViewSet)
|
||||
router.register('console-ports', views.ConsolePortViewSet)
|
||||
router.register('console-server-ports', views.ConsoleServerPortViewSet)
|
||||
router.register('power-ports', views.PowerPortViewSet)
|
||||
router.register('power-outlets', views.PowerOutletViewSet)
|
||||
router.register('interfaces', views.InterfaceViewSet)
|
||||
router.register('front-ports', views.FrontPortViewSet)
|
||||
router.register('rear-ports', views.RearPortViewSet)
|
||||
router.register('device-bays', views.DeviceBayViewSet)
|
||||
router.register('inventory-items', views.InventoryItemViewSet)
|
||||
|
||||
# Connections
|
||||
router.register(r'console-connections', views.ConsoleConnectionViewSet, basename='consoleconnections')
|
||||
router.register(r'power-connections', views.PowerConnectionViewSet, basename='powerconnections')
|
||||
router.register(r'interface-connections', views.InterfaceConnectionViewSet, basename='interfaceconnections')
|
||||
router.register('console-connections', views.ConsoleConnectionViewSet, basename='consoleconnections')
|
||||
router.register('power-connections', views.PowerConnectionViewSet, basename='powerconnections')
|
||||
router.register('interface-connections', views.InterfaceConnectionViewSet, basename='interfaceconnections')
|
||||
|
||||
# Cables
|
||||
router.register(r'cables', views.CableViewSet)
|
||||
router.register('cables', views.CableViewSet)
|
||||
|
||||
# Virtual chassis
|
||||
router.register(r'virtual-chassis', views.VirtualChassisViewSet)
|
||||
router.register('virtual-chassis', views.VirtualChassisViewSet)
|
||||
|
||||
# Power
|
||||
router.register(r'power-panels', views.PowerPanelViewSet)
|
||||
router.register(r'power-feeds', views.PowerFeedViewSet)
|
||||
router.register('power-panels', views.PowerPanelViewSet)
|
||||
router.register('power-feeds', views.PowerFeedViewSet)
|
||||
|
||||
# Miscellaneous
|
||||
router.register(r'connected-device', views.ConnectedDeviceViewSet, basename='connected-device')
|
||||
router.register('connected-device', views.ConnectedDeviceViewSet, basename='connected-device')
|
||||
|
||||
app_name = 'dcim-api'
|
||||
urlpatterns = router.urls
|
||||
|
||||
@@ -220,7 +220,13 @@ class RackViewSet(CustomFieldModelViewSet):
|
||||
|
||||
if data['render'] == 'svg':
|
||||
# Render and return the elevation as an SVG drawing with the correct content type
|
||||
drawing = rack.get_elevation_svg(data['face'], data['unit_width'], data['unit_height'])
|
||||
drawing = rack.get_elevation_svg(
|
||||
face=data['face'],
|
||||
unit_width=data['unit_width'],
|
||||
unit_height=data['unit_height'],
|
||||
legend_width=data['legend_width'],
|
||||
include_images=data['include_images']
|
||||
)
|
||||
return HttpResponse(drawing.tostring(), content_type='image/svg+xml')
|
||||
|
||||
else:
|
||||
@@ -231,6 +237,11 @@ class RackViewSet(CustomFieldModelViewSet):
|
||||
expand_devices=data['expand_devices']
|
||||
)
|
||||
|
||||
# Enable filtering rack units by ID
|
||||
q = data['q']
|
||||
if q:
|
||||
elevation = [u for u in elevation if q in str(u['id']) or q in str(u['name'])]
|
||||
|
||||
page = self.paginate_queryset(elevation)
|
||||
if page is not None:
|
||||
rack_units = serializers.RackUnitSerializer(page, many=True, context={'request': request})
|
||||
|
||||
@@ -195,6 +195,7 @@ class ConsolePortTypeChoices(ChoiceSet):
|
||||
|
||||
TYPE_DE9 = 'de-9'
|
||||
TYPE_DB25 = 'db-25'
|
||||
TYPE_RJ11 = 'rj-11'
|
||||
TYPE_RJ12 = 'rj-12'
|
||||
TYPE_RJ45 = 'rj-45'
|
||||
TYPE_USB_A = 'usb-a'
|
||||
@@ -210,6 +211,7 @@ class ConsolePortTypeChoices(ChoiceSet):
|
||||
('Serial', (
|
||||
(TYPE_DE9, 'DE-9'),
|
||||
(TYPE_DB25, 'DB-25'),
|
||||
(TYPE_RJ11, 'RJ-11'),
|
||||
(TYPE_RJ12, 'RJ-12'),
|
||||
(TYPE_RJ45, 'RJ-45'),
|
||||
)),
|
||||
@@ -971,10 +973,12 @@ class CableStatusChoices(ChoiceSet):
|
||||
|
||||
STATUS_CONNECTED = 'connected'
|
||||
STATUS_PLANNED = 'planned'
|
||||
STATUS_DECOMMISSIONING = 'decommissioning'
|
||||
|
||||
CHOICES = (
|
||||
(STATUS_CONNECTED, 'Connected'),
|
||||
(STATUS_PLANNED, 'Planned'),
|
||||
(STATUS_DECOMMISSIONING, 'Decommissioning'),
|
||||
)
|
||||
|
||||
LEGACY_MAP = {
|
||||
|
||||
@@ -9,8 +9,10 @@ from .choices import InterfaceTypeChoices
|
||||
|
||||
RACK_U_HEIGHT_DEFAULT = 42
|
||||
|
||||
RACK_ELEVATION_UNIT_WIDTH_DEFAULT = 230
|
||||
RACK_ELEVATION_UNIT_HEIGHT_DEFAULT = 20
|
||||
RACK_ELEVATION_BORDER_WIDTH = 2
|
||||
RACK_ELEVATION_LEGEND_WIDTH_DEFAULT = 30
|
||||
RACK_ELEVATION_UNIT_WIDTH_DEFAULT = 220
|
||||
RACK_ELEVATION_UNIT_HEIGHT_DEFAULT = 22
|
||||
|
||||
|
||||
#
|
||||
@@ -59,13 +61,10 @@ POWERFEED_MAX_UTILIZATION_DEFAULT = 80 # Percentage
|
||||
# Cabling and connections
|
||||
#
|
||||
|
||||
# TODO: Replace with CableStatusChoices?
|
||||
# Console/power/interface connection statuses
|
||||
CONNECTION_STATUS_PLANNED = False
|
||||
CONNECTION_STATUS_CONNECTED = True
|
||||
CONNECTION_STATUS_CHOICES = [
|
||||
[CONNECTION_STATUS_PLANNED, 'Planned'],
|
||||
[CONNECTION_STATUS_CONNECTED, 'Connected'],
|
||||
[False, 'Not Connected'],
|
||||
[True, 'Connected'],
|
||||
]
|
||||
|
||||
# Cable endpoint types
|
||||
|
||||
204
netbox/dcim/elevations.py
Normal file
204
netbox/dcim/elevations.py
Normal file
@@ -0,0 +1,204 @@
|
||||
import svgwrite
|
||||
|
||||
from django.conf import settings
|
||||
from django.urls import reverse
|
||||
from django.utils.http import urlencode
|
||||
|
||||
from utilities.utils import foreground_color
|
||||
from .choices import DeviceFaceChoices
|
||||
from .constants import RACK_ELEVATION_BORDER_WIDTH
|
||||
|
||||
|
||||
class RackElevationSVG:
|
||||
"""
|
||||
Use this class to render a rack elevation as an SVG image.
|
||||
|
||||
:param rack: A NetBox Rack instance
|
||||
:param include_images: If true, the SVG document will embed front/rear device face images, where available
|
||||
"""
|
||||
def __init__(self, rack, include_images=True):
|
||||
self.rack = rack
|
||||
self.include_images = include_images
|
||||
|
||||
@staticmethod
|
||||
def _add_gradient(drawing, id_, color):
|
||||
gradient = drawing.linearGradient(
|
||||
start=(0, 0),
|
||||
end=(0, 25),
|
||||
spreadMethod='repeat',
|
||||
id_=id_,
|
||||
gradientTransform='rotate(45, 0, 0)',
|
||||
gradientUnits='userSpaceOnUse'
|
||||
)
|
||||
gradient.add_stop_color(offset='0%', color='#f7f7f7')
|
||||
gradient.add_stop_color(offset='50%', color='#f7f7f7')
|
||||
gradient.add_stop_color(offset='50%', color=color)
|
||||
gradient.add_stop_color(offset='100%', color=color)
|
||||
drawing.defs.add(gradient)
|
||||
|
||||
@staticmethod
|
||||
def _setup_drawing(width, height):
|
||||
drawing = svgwrite.Drawing(size=(width, height))
|
||||
|
||||
# add the stylesheet
|
||||
with open('{}/css/rack_elevation.css'.format(settings.STATICFILES_DIRS[0])) as css_file:
|
||||
drawing.defs.add(drawing.style(css_file.read()))
|
||||
|
||||
# add gradients
|
||||
RackElevationSVG._add_gradient(drawing, 'reserved', '#c7c7ff')
|
||||
RackElevationSVG._add_gradient(drawing, 'occupied', '#d7d7d7')
|
||||
RackElevationSVG._add_gradient(drawing, 'blocked', '#ffc0c0')
|
||||
|
||||
return drawing
|
||||
|
||||
def _draw_device_front(self, drawing, device, start, end, text):
|
||||
name = str(device)
|
||||
if device.devicebay_count:
|
||||
name += ' ({}/{})'.format(device.get_children().count(), device.devicebay_count)
|
||||
|
||||
color = device.device_role.color
|
||||
link = drawing.add(
|
||||
drawing.a(
|
||||
href=reverse('dcim:device', kwargs={'pk': device.pk}),
|
||||
target='_top',
|
||||
fill='black'
|
||||
)
|
||||
)
|
||||
link.set_desc('{} — {} ({}U) {} {}'.format(
|
||||
device.device_role, device.device_type.display_name,
|
||||
device.device_type.u_height, device.asset_tag or '', device.serial or ''
|
||||
))
|
||||
link.add(drawing.rect(start, end, style='fill: #{}'.format(color), class_='slot'))
|
||||
hex_color = '#{}'.format(foreground_color(color))
|
||||
link.add(drawing.text(str(name), insert=text, fill=hex_color))
|
||||
|
||||
# Embed front device type image if one exists
|
||||
if self.include_images and device.device_type.front_image:
|
||||
url = device.device_type.front_image.url
|
||||
image = drawing.image(href=url, insert=start, size=end, class_='device-image')
|
||||
image.fit(scale='slice')
|
||||
link.add(image)
|
||||
|
||||
def _draw_device_rear(self, drawing, device, start, end, text):
|
||||
rect = drawing.rect(start, end, class_="slot blocked")
|
||||
rect.set_desc('{} — {} ({}U) {} {}'.format(
|
||||
device.device_role, device.device_type.display_name,
|
||||
device.device_type.u_height, device.asset_tag or '', device.serial or ''
|
||||
))
|
||||
drawing.add(rect)
|
||||
drawing.add(drawing.text(str(device), insert=text))
|
||||
|
||||
# Embed rear device type image if one exists
|
||||
if self.include_images and device.device_type.rear_image:
|
||||
url = device.device_type.rear_image.url
|
||||
image = drawing.image(href=url, insert=start, size=end, class_='device-image')
|
||||
image.fit(scale='slice')
|
||||
drawing.add(image)
|
||||
|
||||
@staticmethod
|
||||
def _draw_empty(drawing, rack, start, end, text, id_, face_id, class_, reservation):
|
||||
link = drawing.add(
|
||||
drawing.a(
|
||||
href='{}?{}'.format(
|
||||
reverse('dcim:device_add'),
|
||||
urlencode({'rack': rack.pk, 'site': rack.site.pk, 'face': face_id, 'position': id_})
|
||||
),
|
||||
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.text("add device", insert=text, class_='add-device'))
|
||||
|
||||
def merge_elevations(self, face):
|
||||
elevation = self.rack.get_rack_units(face=face, expand_devices=False)
|
||||
if face == DeviceFaceChoices.FACE_REAR:
|
||||
other_face = DeviceFaceChoices.FACE_FRONT
|
||||
else:
|
||||
other_face = DeviceFaceChoices.FACE_REAR
|
||||
other = self.rack.get_rack_units(face=other_face)
|
||||
|
||||
unit_cursor = 0
|
||||
for u in elevation:
|
||||
o = other[unit_cursor]
|
||||
if not u['device'] and o['device']:
|
||||
u['device'] = o['device']
|
||||
u['height'] = 1
|
||||
unit_cursor += u.get('height', 1)
|
||||
|
||||
return elevation
|
||||
|
||||
def render(self, face, unit_width, unit_height, legend_width):
|
||||
"""
|
||||
Return an SVG document representing a rack elevation.
|
||||
"""
|
||||
drawing = self._setup_drawing(
|
||||
unit_width + legend_width + RACK_ELEVATION_BORDER_WIDTH * 2,
|
||||
unit_height * self.rack.u_height + RACK_ELEVATION_BORDER_WIDTH * 2
|
||||
)
|
||||
reserved_units = self.rack.get_reserved_units()
|
||||
|
||||
unit_cursor = 0
|
||||
for ru in range(0, self.rack.u_height):
|
||||
start_y = ru * unit_height
|
||||
position_coordinates = (legend_width / 2, start_y + unit_height / 2 + RACK_ELEVATION_BORDER_WIDTH)
|
||||
unit = ru + 1 if self.rack.desc_units else self.rack.u_height - ru
|
||||
drawing.add(
|
||||
drawing.text(str(unit), position_coordinates, class_="unit")
|
||||
)
|
||||
|
||||
for unit in self.merge_elevations(face):
|
||||
|
||||
# Loop through all units in the elevation
|
||||
device = unit['device']
|
||||
height = unit.get('height', 1)
|
||||
|
||||
# Setup drawing coordinates
|
||||
x_offset = legend_width + RACK_ELEVATION_BORDER_WIDTH
|
||||
y_offset = unit_cursor * unit_height + RACK_ELEVATION_BORDER_WIDTH
|
||||
end_y = unit_height * height
|
||||
start_cordinates = (x_offset, y_offset)
|
||||
end_cordinates = (unit_width, end_y)
|
||||
text_cordinates = (x_offset + (unit_width / 2), y_offset + end_y / 2)
|
||||
|
||||
# Draw the device
|
||||
if device and device.face == face:
|
||||
self._draw_device_front(drawing, device, start_cordinates, end_cordinates, text_cordinates)
|
||||
elif device and device.device_type.is_full_depth:
|
||||
self._draw_device_rear(drawing, device, start_cordinates, end_cordinates, text_cordinates)
|
||||
else:
|
||||
# Draw shallow devices, reservations, or empty units
|
||||
class_ = 'slot'
|
||||
reservation = reserved_units.get(unit["id"])
|
||||
if device:
|
||||
class_ += ' occupied'
|
||||
if reservation:
|
||||
class_ += ' reserved'
|
||||
self._draw_empty(
|
||||
drawing,
|
||||
self.rack,
|
||||
start_cordinates,
|
||||
end_cordinates,
|
||||
text_cordinates,
|
||||
unit["id"],
|
||||
face,
|
||||
class_,
|
||||
reservation
|
||||
)
|
||||
|
||||
unit_cursor += height
|
||||
|
||||
# Wrap the drawing with a border
|
||||
border_width = RACK_ELEVATION_BORDER_WIDTH
|
||||
border_offset = RACK_ELEVATION_BORDER_WIDTH / 2
|
||||
frame = drawing.rect(
|
||||
insert=(legend_width + border_offset, border_offset),
|
||||
size=(unit_width + border_width, self.rack.u_height * unit_height + border_width),
|
||||
class_='rack'
|
||||
)
|
||||
drawing.add(frame)
|
||||
|
||||
return drawing
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,73 +0,0 @@
|
||||
from django.db.models import Manager, QuerySet
|
||||
from django.db.models.expressions import RawSQL
|
||||
|
||||
from .constants import NONCONNECTABLE_IFACE_TYPES
|
||||
|
||||
# Regular expressions for parsing Interface names
|
||||
TYPE_RE = r"SUBSTRING({} FROM '^([^0-9\.:]+)')"
|
||||
SLOT_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(\d{{1,9}})/') AS integer), NULL)"
|
||||
SUBSLOT_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9\.:]+)?\d{{1,9}}/(\d{{1,9}})') AS integer), NULL)"
|
||||
POSITION_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(?:\d{{1,9}}/){{2}}(\d{{1,9}})') AS integer), NULL)"
|
||||
SUBPOSITION_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(?:\d{{1,9}}/){{3}}(\d{{1,9}})') AS integer), NULL)"
|
||||
ID_RE = r"CAST(SUBSTRING({} FROM '^(?:[^0-9\.:]+)?(\d{{1,9}})([^/]|$)') AS integer)"
|
||||
CHANNEL_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^.*:(\d{{1,9}})(\.\d{{1,9}})?$') AS integer), 0)"
|
||||
VC_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^.*\.(\d{{1,9}})$') AS integer), 0)"
|
||||
|
||||
|
||||
class InterfaceQuerySet(QuerySet):
|
||||
|
||||
def connectable(self):
|
||||
"""
|
||||
Return only physical interfaces which are capable of being connected to other interfaces (i.e. not virtual or
|
||||
wireless).
|
||||
"""
|
||||
return self.exclude(type__in=NONCONNECTABLE_IFACE_TYPES)
|
||||
|
||||
|
||||
class InterfaceManager(Manager):
|
||||
|
||||
def get_queryset(self):
|
||||
"""
|
||||
Naturally order interfaces by their type and numeric position. To order interfaces naturally, the `name` field
|
||||
is split into eight distinct components: leading text (type), slot, subslot, position, subposition, ID, channel,
|
||||
and virtual circuit:
|
||||
|
||||
{type}{slot or ID}/{subslot}/{position}/{subposition}:{channel}.{vc}
|
||||
|
||||
Components absent from the interface name are coalesced to zero or null. For example, an interface named
|
||||
GigabitEthernet1/2/3 would be parsed as follows:
|
||||
|
||||
type = 'GigabitEthernet'
|
||||
slot = 1
|
||||
subslot = 2
|
||||
position = 3
|
||||
subposition = None
|
||||
id = None
|
||||
channel = 0
|
||||
vc = 0
|
||||
|
||||
The original `name` field is considered in its entirety to serve as a fallback in the event interfaces do not
|
||||
match any of the prescribed fields.
|
||||
|
||||
The `id` field is included to enforce deterministic ordering of interfaces in similar vein of other device
|
||||
components.
|
||||
"""
|
||||
|
||||
sql_col = '{}.name'.format(self.model._meta.db_table)
|
||||
ordering = [
|
||||
'_slot', '_subslot', '_position', '_subposition', '_type', '_id', '_channel', '_vc', 'name', 'pk'
|
||||
|
||||
]
|
||||
|
||||
fields = {
|
||||
'_type': RawSQL(TYPE_RE.format(sql_col), []),
|
||||
'_id': RawSQL(ID_RE.format(sql_col), []),
|
||||
'_slot': RawSQL(SLOT_RE.format(sql_col), []),
|
||||
'_subslot': RawSQL(SUBSLOT_RE.format(sql_col), []),
|
||||
'_position': RawSQL(POSITION_RE.format(sql_col), []),
|
||||
'_subposition': RawSQL(SUBPOSITION_RE.format(sql_col), []),
|
||||
'_channel': RawSQL(CHANNEL_RE.format(sql_col), []),
|
||||
'_vc': RawSQL(VC_RE.format(sql_col), []),
|
||||
}
|
||||
|
||||
return InterfaceQuerySet(self.model, using=self._db).annotate(**fields).order_by(*ordering)
|
||||
147
netbox/dcim/migrations/0093_device_component_ordering.py
Normal file
147
netbox/dcim/migrations/0093_device_component_ordering.py
Normal file
@@ -0,0 +1,147 @@
|
||||
from django.db import migrations
|
||||
import utilities.fields
|
||||
import utilities.ordering
|
||||
|
||||
|
||||
def _update_model_names(model):
|
||||
# Update each unique field value in bulk
|
||||
for name in model.objects.values_list('name', flat=True).order_by('name').distinct():
|
||||
model.objects.filter(name=name).update(_name=utilities.ordering.naturalize(name, max_length=100))
|
||||
|
||||
|
||||
def naturalize_consoleports(apps, schema_editor):
|
||||
_update_model_names(apps.get_model('dcim', 'ConsolePort'))
|
||||
|
||||
|
||||
def naturalize_consoleserverports(apps, schema_editor):
|
||||
_update_model_names(apps.get_model('dcim', 'ConsoleServerPort'))
|
||||
|
||||
|
||||
def naturalize_powerports(apps, schema_editor):
|
||||
_update_model_names(apps.get_model('dcim', 'PowerPort'))
|
||||
|
||||
|
||||
def naturalize_poweroutlets(apps, schema_editor):
|
||||
_update_model_names(apps.get_model('dcim', 'PowerOutlet'))
|
||||
|
||||
|
||||
def naturalize_frontports(apps, schema_editor):
|
||||
_update_model_names(apps.get_model('dcim', 'FrontPort'))
|
||||
|
||||
|
||||
def naturalize_rearports(apps, schema_editor):
|
||||
_update_model_names(apps.get_model('dcim', 'RearPort'))
|
||||
|
||||
|
||||
def naturalize_devicebays(apps, schema_editor):
|
||||
_update_model_names(apps.get_model('dcim', 'DeviceBay'))
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0092_fix_rack_outer_unit'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='consoleport',
|
||||
options={'ordering': ('device', '_name')},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='consoleserverport',
|
||||
options={'ordering': ('device', '_name')},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='devicebay',
|
||||
options={'ordering': ('device', '_name')},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='frontport',
|
||||
options={'ordering': ('device', '_name')},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='inventoryitem',
|
||||
options={'ordering': ('device__id', 'parent__id', '_name')},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='poweroutlet',
|
||||
options={'ordering': ('device', '_name')},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='powerport',
|
||||
options={'ordering': ('device', '_name')},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='rearport',
|
||||
options={'ordering': ('device', '_name')},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='consoleport',
|
||||
name='_name',
|
||||
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='consoleserverport',
|
||||
name='_name',
|
||||
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='devicebay',
|
||||
name='_name',
|
||||
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='frontport',
|
||||
name='_name',
|
||||
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='inventoryitem',
|
||||
name='_name',
|
||||
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='poweroutlet',
|
||||
name='_name',
|
||||
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='powerport',
|
||||
name='_name',
|
||||
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rearport',
|
||||
name='_name',
|
||||
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=naturalize_consoleports,
|
||||
reverse_code=migrations.RunPython.noop
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=naturalize_consoleserverports,
|
||||
reverse_code=migrations.RunPython.noop
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=naturalize_powerports,
|
||||
reverse_code=migrations.RunPython.noop
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=naturalize_poweroutlets,
|
||||
reverse_code=migrations.RunPython.noop
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=naturalize_frontports,
|
||||
reverse_code=migrations.RunPython.noop
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=naturalize_rearports,
|
||||
reverse_code=migrations.RunPython.noop
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=naturalize_devicebays,
|
||||
reverse_code=migrations.RunPython.noop
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,138 @@
|
||||
from django.db import migrations
|
||||
import utilities.fields
|
||||
import utilities.ordering
|
||||
|
||||
|
||||
def _update_model_names(model):
|
||||
# Update each unique field value in bulk
|
||||
for name in model.objects.values_list('name', flat=True).order_by('name').distinct():
|
||||
model.objects.filter(name=name).update(_name=utilities.ordering.naturalize(name, max_length=100))
|
||||
|
||||
|
||||
def naturalize_consoleporttemplates(apps, schema_editor):
|
||||
_update_model_names(apps.get_model('dcim', 'ConsolePortTemplate'))
|
||||
|
||||
|
||||
def naturalize_consoleserverporttemplates(apps, schema_editor):
|
||||
_update_model_names(apps.get_model('dcim', 'ConsoleServerPortTemplate'))
|
||||
|
||||
|
||||
def naturalize_powerporttemplates(apps, schema_editor):
|
||||
_update_model_names(apps.get_model('dcim', 'PowerPortTemplate'))
|
||||
|
||||
|
||||
def naturalize_poweroutlettemplates(apps, schema_editor):
|
||||
_update_model_names(apps.get_model('dcim', 'PowerOutletTemplate'))
|
||||
|
||||
|
||||
def naturalize_frontporttemplates(apps, schema_editor):
|
||||
_update_model_names(apps.get_model('dcim', 'FrontPortTemplate'))
|
||||
|
||||
|
||||
def naturalize_rearporttemplates(apps, schema_editor):
|
||||
_update_model_names(apps.get_model('dcim', 'RearPortTemplate'))
|
||||
|
||||
|
||||
def naturalize_devicebaytemplates(apps, schema_editor):
|
||||
_update_model_names(apps.get_model('dcim', 'DeviceBayTemplate'))
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0093_device_component_ordering'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='consoleporttemplate',
|
||||
options={'ordering': ('device_type', '_name')},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='consoleserverporttemplate',
|
||||
options={'ordering': ('device_type', '_name')},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='devicebaytemplate',
|
||||
options={'ordering': ('device_type', '_name')},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='frontporttemplate',
|
||||
options={'ordering': ('device_type', '_name')},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='poweroutlettemplate',
|
||||
options={'ordering': ('device_type', '_name')},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='powerporttemplate',
|
||||
options={'ordering': ('device_type', '_name')},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='rearporttemplate',
|
||||
options={'ordering': ('device_type', '_name')},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='consoleporttemplate',
|
||||
name='_name',
|
||||
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='consoleserverporttemplate',
|
||||
name='_name',
|
||||
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='devicebaytemplate',
|
||||
name='_name',
|
||||
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='frontporttemplate',
|
||||
name='_name',
|
||||
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='poweroutlettemplate',
|
||||
name='_name',
|
||||
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='powerporttemplate',
|
||||
name='_name',
|
||||
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rearporttemplate',
|
||||
name='_name',
|
||||
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=naturalize_consoleporttemplates,
|
||||
reverse_code=migrations.RunPython.noop
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=naturalize_consoleserverporttemplates,
|
||||
reverse_code=migrations.RunPython.noop
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=naturalize_powerporttemplates,
|
||||
reverse_code=migrations.RunPython.noop
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=naturalize_poweroutlettemplates,
|
||||
reverse_code=migrations.RunPython.noop
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=naturalize_frontporttemplates,
|
||||
reverse_code=migrations.RunPython.noop
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=naturalize_rearporttemplates,
|
||||
reverse_code=migrations.RunPython.noop
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=naturalize_devicebaytemplates,
|
||||
reverse_code=migrations.RunPython.noop
|
||||
),
|
||||
]
|
||||
70
netbox/dcim/migrations/0095_primary_model_ordering.py
Normal file
70
netbox/dcim/migrations/0095_primary_model_ordering.py
Normal file
@@ -0,0 +1,70 @@
|
||||
from django.db import migrations
|
||||
import utilities.fields
|
||||
import utilities.ordering
|
||||
|
||||
|
||||
def _update_model_names(model):
|
||||
# Update each unique field value in bulk
|
||||
for name in model.objects.values_list('name', flat=True).order_by('name').distinct():
|
||||
model.objects.filter(name=name).update(_name=utilities.ordering.naturalize(name, max_length=100))
|
||||
|
||||
|
||||
def naturalize_sites(apps, schema_editor):
|
||||
_update_model_names(apps.get_model('dcim', 'Site'))
|
||||
|
||||
|
||||
def naturalize_racks(apps, schema_editor):
|
||||
_update_model_names(apps.get_model('dcim', 'Rack'))
|
||||
|
||||
|
||||
def naturalize_devices(apps, schema_editor):
|
||||
_update_model_names(apps.get_model('dcim', 'Device'))
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0094_device_component_template_ordering'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='device',
|
||||
options={'ordering': ('_name', 'pk'), 'permissions': (('napalm_read', 'Read-only access to devices via NAPALM'), ('napalm_write', 'Read/write access to devices via NAPALM'))},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='rack',
|
||||
options={'ordering': ('site', 'group', '_name', 'pk')},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='site',
|
||||
options={'ordering': ('_name',)},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='device',
|
||||
name='_name',
|
||||
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rack',
|
||||
name='_name',
|
||||
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='site',
|
||||
name='_name',
|
||||
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=naturalize_sites,
|
||||
reverse_code=migrations.RunPython.noop
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=naturalize_racks,
|
||||
reverse_code=migrations.RunPython.noop
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=naturalize_devices,
|
||||
reverse_code=migrations.RunPython.noop
|
||||
),
|
||||
]
|
||||
53
netbox/dcim/migrations/0096_interface_ordering.py
Normal file
53
netbox/dcim/migrations/0096_interface_ordering.py
Normal file
@@ -0,0 +1,53 @@
|
||||
from django.db import migrations
|
||||
import utilities.fields
|
||||
import utilities.ordering
|
||||
|
||||
|
||||
def _update_model_names(model):
|
||||
# Update each unique field value in bulk
|
||||
for name in model.objects.values_list('name', flat=True).order_by('name').distinct():
|
||||
model.objects.filter(name=name).update(_name=utilities.ordering.naturalize_interface(name, max_length=100))
|
||||
|
||||
|
||||
def naturalize_interfacetemplates(apps, schema_editor):
|
||||
_update_model_names(apps.get_model('dcim', 'InterfaceTemplate'))
|
||||
|
||||
|
||||
def naturalize_interfaces(apps, schema_editor):
|
||||
_update_model_names(apps.get_model('dcim', 'Interface'))
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0095_primary_model_ordering'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='interface',
|
||||
options={'ordering': ('device', '_name')},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='interfacetemplate',
|
||||
options={'ordering': ('device_type', '_name')},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='interface',
|
||||
name='_name',
|
||||
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize_interface),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='interfacetemplate',
|
||||
name='_name',
|
||||
field=utilities.fields.NaturalOrderingField('target_field', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize_interface),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=naturalize_interfacetemplates,
|
||||
reverse_code=migrations.RunPython.noop
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=naturalize_interfaces,
|
||||
reverse_code=migrations.RunPython.noop
|
||||
),
|
||||
]
|
||||
20
netbox/dcim/migrations/0097_interfacetemplate_type_other.py
Normal file
20
netbox/dcim/migrations/0097_interfacetemplate_type_other.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def interfacetemplate_type_to_slug(apps, schema_editor):
|
||||
InterfaceTemplate = apps.get_model('dcim', 'InterfaceTemplate')
|
||||
InterfaceTemplate.objects.filter(type=32767).update(type='other')
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0096_interface_ordering'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
# Missed type "other" in the initial migration (see #3967)
|
||||
migrations.RunPython(
|
||||
code=interfacetemplate_type_to_slug
|
||||
),
|
||||
]
|
||||
23
netbox/dcim/migrations/0098_devicetype_images.py
Normal file
23
netbox/dcim/migrations/0098_devicetype_images.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 2.2.9 on 2020-02-20 15:11
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0097_interfacetemplate_type_other'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='devicetype',
|
||||
name='front_image',
|
||||
field=models.ImageField(blank=True, upload_to='devicetype-images'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='devicetype',
|
||||
name='rear_image',
|
||||
field=models.ImageField(blank=True, upload_to='devicetype-images'),
|
||||
),
|
||||
]
|
||||
@@ -1,7 +1,6 @@
|
||||
from collections import OrderedDict
|
||||
from itertools import count, groupby
|
||||
|
||||
import svgwrite
|
||||
import yaml
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
@@ -13,7 +12,6 @@ from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
from django.db import models
|
||||
from django.db.models import Count, F, ProtectedError, Sum
|
||||
from django.urls import reverse
|
||||
from django.utils.http import urlencode
|
||||
from mptt.models import MPTTModel, TreeForeignKey
|
||||
from taggit.managers import TaggableManager
|
||||
from timezone_field import TimeZoneField
|
||||
@@ -21,11 +19,11 @@ from timezone_field import TimeZoneField
|
||||
from dcim.choices import *
|
||||
from dcim.constants import *
|
||||
from dcim.fields import ASNField
|
||||
from extras.models import ConfigContextModel, CustomFieldModel, TaggedItem
|
||||
from utilities.fields import ColorField
|
||||
from utilities.managers import NaturalOrderingManager
|
||||
from dcim.elevations import RackElevationSVG
|
||||
from extras.models import ConfigContextModel, CustomFieldModel, ObjectChange, TaggedItem
|
||||
from utilities.fields import ColorField, NaturalOrderingField
|
||||
from utilities.models import ChangeLoggedModel
|
||||
from utilities.utils import foreground_color, to_meters
|
||||
from utilities.utils import serialize_object, to_meters
|
||||
from .device_component_templates import (
|
||||
ConsolePortTemplate, ConsoleServerPortTemplate, DeviceBayTemplate, FrontPortTemplate, InterfaceTemplate,
|
||||
PowerOutletTemplate, PowerPortTemplate, RearPortTemplate,
|
||||
@@ -120,6 +118,15 @@ class Region(MPTTModel, ChangeLoggedModel):
|
||||
Q(region__in=self.get_descendants())
|
||||
).count()
|
||||
|
||||
def to_objectchange(self, action):
|
||||
# Remove MPTT-internal fields
|
||||
return ObjectChange(
|
||||
changed_object=self,
|
||||
object_repr=str(self),
|
||||
action=action,
|
||||
object_data=serialize_object(self, exclude=['level', 'lft', 'rght', 'tree_id'])
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Sites
|
||||
@@ -134,6 +141,11 @@ class Site(ChangeLoggedModel, CustomFieldModel):
|
||||
max_length=50,
|
||||
unique=True
|
||||
)
|
||||
_name = NaturalOrderingField(
|
||||
target_field='name',
|
||||
max_length=100,
|
||||
blank=True
|
||||
)
|
||||
slug = models.SlugField(
|
||||
unique=True
|
||||
)
|
||||
@@ -215,8 +227,6 @@ class Site(ChangeLoggedModel, CustomFieldModel):
|
||||
images = GenericRelation(
|
||||
to='extras.ImageAttachment'
|
||||
)
|
||||
|
||||
objects = NaturalOrderingManager()
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
csv_headers = [
|
||||
@@ -235,7 +245,7 @@ class Site(ChangeLoggedModel, CustomFieldModel):
|
||||
}
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
ordering = ('_name',)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
@@ -348,167 +358,7 @@ class RackRole(ChangeLoggedModel):
|
||||
)
|
||||
|
||||
|
||||
class RackElevationHelperMixin:
|
||||
"""
|
||||
Utility class that renders rack elevations. Contains helper methods for rendering elevations as a list of
|
||||
rack units represented as dictionaries, or an SVG of the elevation.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def _add_gradient(drawing, id_, color):
|
||||
gradient = drawing.linearGradient(
|
||||
start=('0', '0%'),
|
||||
end=('0', '5%'),
|
||||
spreadMethod='repeat',
|
||||
id_=id_,
|
||||
gradientTransform='rotate(45, 0, 0)',
|
||||
gradientUnits='userSpaceOnUse'
|
||||
)
|
||||
gradient.add_stop_color(offset='0%', color='#f7f7f7')
|
||||
gradient.add_stop_color(offset='50%', color='#f7f7f7')
|
||||
gradient.add_stop_color(offset='50%', color=color)
|
||||
gradient.add_stop_color(offset='100%', color=color)
|
||||
drawing.defs.add(gradient)
|
||||
|
||||
@staticmethod
|
||||
def _setup_drawing(width, height):
|
||||
drawing = svgwrite.Drawing(size=(width, height))
|
||||
|
||||
# add the stylesheet
|
||||
with open('{}/css/rack_elevation.css'.format(settings.STATICFILES_DIRS[0])) as css_file:
|
||||
drawing.defs.add(drawing.style(css_file.read()))
|
||||
|
||||
# add gradients
|
||||
RackElevationHelperMixin._add_gradient(drawing, 'reserved', '#c7c7ff')
|
||||
RackElevationHelperMixin._add_gradient(drawing, 'occupied', '#f0f0f0')
|
||||
RackElevationHelperMixin._add_gradient(drawing, 'blocked', '#ffc7c7')
|
||||
|
||||
return drawing
|
||||
|
||||
@staticmethod
|
||||
def _draw_device_front(drawing, device, start, end, text):
|
||||
color = device.device_role.color
|
||||
link = drawing.add(
|
||||
drawing.a(
|
||||
href=reverse('dcim:device', kwargs={'pk': device.pk}),
|
||||
target='_top',
|
||||
fill='black'
|
||||
)
|
||||
)
|
||||
link.set_desc('{} — {} ({}U) {} {}'.format(
|
||||
device.device_role, device.device_type.display_name,
|
||||
device.device_type.u_height, device.asset_tag or '', device.serial or ''
|
||||
))
|
||||
link.add(drawing.rect(start, end, style='fill: #{}'.format(color), class_='slot'))
|
||||
hex_color = '#{}'.format(foreground_color(color))
|
||||
link.add(drawing.text(str(device), insert=text, fill=hex_color))
|
||||
|
||||
@staticmethod
|
||||
def _draw_device_rear(drawing, device, start, end, text):
|
||||
rect = drawing.rect(start, end, class_="slot blocked")
|
||||
rect.set_desc('{} — {} ({}U) {} {}'.format(
|
||||
device.device_role, device.device_type.display_name,
|
||||
device.device_type.u_height, device.asset_tag or '', device.serial or ''
|
||||
))
|
||||
drawing.add(rect)
|
||||
drawing.add(drawing.text(str(device), insert=text))
|
||||
|
||||
@staticmethod
|
||||
def _draw_empty(drawing, rack, start, end, text, id_, face_id, class_, reservation):
|
||||
link = drawing.add(
|
||||
drawing.a(
|
||||
href='{}?{}'.format(
|
||||
reverse('dcim:device_add'),
|
||||
urlencode({'rack': rack.pk, 'site': rack.site.pk, 'face': face_id, 'position': id_})
|
||||
),
|
||||
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.text("add device", insert=text, class_='add-device'))
|
||||
|
||||
def _draw_elevations(self, elevation, reserved_units, face, unit_width, unit_height):
|
||||
|
||||
drawing = self._setup_drawing(unit_width, unit_height * self.u_height)
|
||||
|
||||
unit_cursor = 0
|
||||
for unit in elevation:
|
||||
|
||||
# Loop through all units in the elevation
|
||||
device = unit['device']
|
||||
height = unit.get('height', 1)
|
||||
|
||||
# Setup drawing coordinates
|
||||
start_y = unit_cursor * unit_height
|
||||
end_y = unit_height * height
|
||||
start_cordinates = (0, start_y)
|
||||
end_cordinates = (unit_width, end_y)
|
||||
text_cordinates = (unit_width / 2, start_y + end_y / 2)
|
||||
|
||||
# Draw the device
|
||||
if device and device.face == face:
|
||||
self._draw_device_front(drawing, device, start_cordinates, end_cordinates, text_cordinates)
|
||||
elif device and device.device_type.is_full_depth:
|
||||
self._draw_device_rear(drawing, device, start_cordinates, end_cordinates, text_cordinates)
|
||||
else:
|
||||
# Draw shallow devices, reservations, or empty units
|
||||
class_ = 'slot'
|
||||
reservation = reserved_units.get(unit["id"])
|
||||
if device:
|
||||
class_ += ' occupied'
|
||||
if reservation:
|
||||
class_ += ' reserved'
|
||||
self._draw_empty(
|
||||
drawing, self, start_cordinates, end_cordinates, text_cordinates, unit["id"], face, class_, reservation
|
||||
)
|
||||
|
||||
unit_cursor += height
|
||||
|
||||
# Wrap the drawing with a border
|
||||
drawing.add(drawing.rect((0, 0), (unit_width, self.u_height * unit_height), class_='rack'))
|
||||
|
||||
return drawing
|
||||
|
||||
def merge_elevations(self, face):
|
||||
elevation = self.get_rack_units(face=face, expand_devices=False)
|
||||
other_face = DeviceFaceChoices.FACE_FRONT if face == DeviceFaceChoices.FACE_REAR else DeviceFaceChoices.FACE_REAR
|
||||
other = self.get_rack_units(face=other_face)
|
||||
|
||||
unit_cursor = 0
|
||||
for u in elevation:
|
||||
o = other[unit_cursor]
|
||||
if not u['device'] and o['device']:
|
||||
u['device'] = o['device']
|
||||
u['height'] = 1
|
||||
unit_cursor += u.get('height', 1)
|
||||
|
||||
return elevation
|
||||
|
||||
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
|
||||
|
||||
:param face: Enum of [front, rear] representing the desired side of the rack elevation to render
|
||||
:param width: Width in pixles for the rendered drawing
|
||||
:param unit_height: Height of each rack unit for the rendered drawing. Note this is not the total
|
||||
height of the elevation
|
||||
"""
|
||||
elevation = self.merge_elevations(face)
|
||||
reserved_units = self.get_reserved_units()
|
||||
|
||||
return self._draw_elevations(elevation, reserved_units, face, unit_width, unit_height)
|
||||
|
||||
|
||||
class Rack(ChangeLoggedModel, CustomFieldModel, RackElevationHelperMixin):
|
||||
class Rack(ChangeLoggedModel, CustomFieldModel):
|
||||
"""
|
||||
Devices are housed within Racks. Each rack has a defined height measured in rack units, and a front and rear face.
|
||||
Each Rack is assigned to a Site and (optionally) a RackGroup.
|
||||
@@ -516,6 +366,11 @@ class Rack(ChangeLoggedModel, CustomFieldModel, RackElevationHelperMixin):
|
||||
name = models.CharField(
|
||||
max_length=50
|
||||
)
|
||||
_name = NaturalOrderingField(
|
||||
target_field='name',
|
||||
max_length=100,
|
||||
blank=True
|
||||
)
|
||||
facility_id = models.CharField(
|
||||
max_length=50,
|
||||
blank=True,
|
||||
@@ -612,8 +467,6 @@ class Rack(ChangeLoggedModel, CustomFieldModel, RackElevationHelperMixin):
|
||||
images = GenericRelation(
|
||||
to='extras.ImageAttachment'
|
||||
)
|
||||
|
||||
objects = NaturalOrderingManager()
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
csv_headers = [
|
||||
@@ -634,12 +487,12 @@ class Rack(ChangeLoggedModel, CustomFieldModel, RackElevationHelperMixin):
|
||||
}
|
||||
|
||||
class Meta:
|
||||
ordering = ('site', 'group', 'name', 'pk') # (site, group, name) may be non-unique
|
||||
unique_together = [
|
||||
ordering = ('site', 'group', '_name', 'pk') # (site, group, name) may be non-unique
|
||||
unique_together = (
|
||||
# Name and facility_id must be unique *only* within a RackGroup
|
||||
['group', 'name'],
|
||||
['group', 'facility_id'],
|
||||
]
|
||||
('group', 'name'),
|
||||
('group', 'facility_id'),
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.display_name or super().__str__()
|
||||
@@ -817,6 +670,28 @@ class Rack(ChangeLoggedModel, CustomFieldModel, RackElevationHelperMixin):
|
||||
reserved_units[u] = r
|
||||
return reserved_units
|
||||
|
||||
def get_elevation_svg(
|
||||
self,
|
||||
face=DeviceFaceChoices.FACE_FRONT,
|
||||
unit_width=RACK_ELEVATION_UNIT_WIDTH_DEFAULT,
|
||||
unit_height=RACK_ELEVATION_UNIT_HEIGHT_DEFAULT,
|
||||
legend_width=RACK_ELEVATION_LEGEND_WIDTH_DEFAULT,
|
||||
include_images=True
|
||||
):
|
||||
"""
|
||||
Return an SVG of the rack elevation
|
||||
|
||||
:param face: Enum of [front, rear] representing the desired side of the rack elevation to render
|
||||
:param unit_width: Width in pixels for the rendered drawing
|
||||
:param unit_height: Height of each rack unit for the rendered drawing. Note this is not the total
|
||||
height of the elevation
|
||||
:param legend_width: Width of the unit legend, in pixels
|
||||
:param include_images: Embed front/rear device images where available
|
||||
"""
|
||||
elevation = RackElevationSVG(self, include_images=include_images)
|
||||
|
||||
return elevation.render(face, unit_width, unit_height, legend_width)
|
||||
|
||||
def get_0u_devices(self):
|
||||
return self.devices.filter(position=0)
|
||||
|
||||
@@ -1007,6 +882,14 @@ class DeviceType(ChangeLoggedModel, CustomFieldModel):
|
||||
help_text='Parent devices house child devices in device bays. Leave blank '
|
||||
'if this device type is neither a parent nor a child.'
|
||||
)
|
||||
front_image = models.ImageField(
|
||||
upload_to='devicetype-images',
|
||||
blank=True
|
||||
)
|
||||
rear_image = models.ImageField(
|
||||
upload_to='devicetype-images',
|
||||
blank=True
|
||||
)
|
||||
comments = models.TextField(
|
||||
blank=True
|
||||
)
|
||||
@@ -1038,6 +921,10 @@ class DeviceType(ChangeLoggedModel, CustomFieldModel):
|
||||
# Save a copy of u_height for validation in clean()
|
||||
self._original_u_height = self.u_height
|
||||
|
||||
# Save references to the original front/rear images
|
||||
self._original_front_image = self.front_image
|
||||
self._original_rear_image = self.rear_image
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:devicetype', args=[self.pk])
|
||||
|
||||
@@ -1157,6 +1044,26 @@ class DeviceType(ChangeLoggedModel, CustomFieldModel):
|
||||
'u_height': "Child device types must be 0U."
|
||||
})
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
ret = super().save(*args, **kwargs)
|
||||
|
||||
# Delete any previously uploaded image files that are no longer in use
|
||||
if self.front_image != self._original_front_image:
|
||||
self._original_front_image.delete(save=False)
|
||||
if self.rear_image != self._original_rear_image:
|
||||
self._original_rear_image.delete(save=False)
|
||||
|
||||
return ret
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
super().delete(*args, **kwargs)
|
||||
|
||||
# Delete any uploaded image files
|
||||
if self.front_image:
|
||||
self.front_image.delete(save=False)
|
||||
if self.rear_image:
|
||||
self.rear_image.delete(save=False)
|
||||
|
||||
@property
|
||||
def display_name(self):
|
||||
return '{} {}'.format(self.manufacturer.name, self.model)
|
||||
@@ -1313,6 +1220,12 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
_name = NaturalOrderingField(
|
||||
target_field='name',
|
||||
max_length=100,
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
serial = models.CharField(
|
||||
max_length=50,
|
||||
blank=True,
|
||||
@@ -1407,8 +1320,6 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
|
||||
images = GenericRelation(
|
||||
to='extras.ImageAttachment'
|
||||
)
|
||||
|
||||
objects = NaturalOrderingManager()
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
csv_headers = [
|
||||
@@ -1430,12 +1341,12 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
|
||||
}
|
||||
|
||||
class Meta:
|
||||
ordering = ('name', 'pk') # Name may be NULL
|
||||
unique_together = [
|
||||
['site', 'tenant', 'name'], # See validate_unique below
|
||||
['rack', 'position', 'face'],
|
||||
['virtual_chassis', 'vc_position'],
|
||||
]
|
||||
ordering = ('_name', 'pk') # Name may be null
|
||||
unique_together = (
|
||||
('site', 'tenant', 'name'), # See validate_unique below
|
||||
('rack', 'position', 'face'),
|
||||
('virtual_chassis', 'vc_position'),
|
||||
)
|
||||
permissions = (
|
||||
('napalm_read', 'Read-only access to devices via NAPALM'),
|
||||
('napalm_write', 'Read/write access to devices via NAPALM'),
|
||||
@@ -2054,6 +1965,7 @@ class Cable(ChangeLoggedModel):
|
||||
STATUS_CLASS_MAP = {
|
||||
CableStatusChoices.STATUS_CONNECTED: 'success',
|
||||
CableStatusChoices.STATUS_PLANNED: 'info',
|
||||
CableStatusChoices.STATUS_DECOMMISSIONING: 'warning',
|
||||
}
|
||||
|
||||
class Meta:
|
||||
@@ -2214,14 +2126,14 @@ class Cable(ChangeLoggedModel):
|
||||
b_path = self.termination_a.trace()
|
||||
|
||||
# Determine overall path status (connected or planned)
|
||||
if self.status == CableStatusChoices.STATUS_PLANNED:
|
||||
path_status = CONNECTION_STATUS_PLANNED
|
||||
else:
|
||||
path_status = CONNECTION_STATUS_CONNECTED
|
||||
if self.status == CableStatusChoices.STATUS_CONNECTED:
|
||||
path_status = True
|
||||
for segment in a_path[1:] + b_path[1:]:
|
||||
if segment[1] is None or segment[1].status == CableStatusChoices.STATUS_PLANNED:
|
||||
path_status = CONNECTION_STATUS_PLANNED
|
||||
if segment[1] is None or segment[1].status != CableStatusChoices.STATUS_CONNECTED:
|
||||
path_status = False
|
||||
break
|
||||
else:
|
||||
path_status = False
|
||||
|
||||
a_endpoint = a_path[-1][2]
|
||||
b_endpoint = b_path[-1][2]
|
||||
|
||||
@@ -4,9 +4,9 @@ from django.db import models
|
||||
|
||||
from dcim.choices import *
|
||||
from dcim.constants import *
|
||||
from dcim.managers import InterfaceManager
|
||||
from extras.models import ObjectChange
|
||||
from utilities.managers import NaturalOrderingManager
|
||||
from utilities.fields import NaturalOrderingField
|
||||
from utilities.ordering import naturalize_interface
|
||||
from utilities.utils import serialize_object
|
||||
from .device_components import (
|
||||
ConsolePort, ConsoleServerPort, DeviceBay, FrontPort, Interface, PowerOutlet, PowerPort, RearPort,
|
||||
@@ -58,17 +58,20 @@ class ConsolePortTemplate(ComponentTemplateModel):
|
||||
name = models.CharField(
|
||||
max_length=50
|
||||
)
|
||||
_name = NaturalOrderingField(
|
||||
target_field='name',
|
||||
max_length=100,
|
||||
blank=True
|
||||
)
|
||||
type = models.CharField(
|
||||
max_length=50,
|
||||
choices=ConsolePortTypeChoices,
|
||||
blank=True
|
||||
)
|
||||
|
||||
objects = NaturalOrderingManager()
|
||||
|
||||
class Meta:
|
||||
ordering = ['device_type', 'name']
|
||||
unique_together = ['device_type', 'name']
|
||||
ordering = ('device_type', '_name')
|
||||
unique_together = ('device_type', 'name')
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
@@ -93,17 +96,20 @@ class ConsoleServerPortTemplate(ComponentTemplateModel):
|
||||
name = models.CharField(
|
||||
max_length=50
|
||||
)
|
||||
_name = NaturalOrderingField(
|
||||
target_field='name',
|
||||
max_length=100,
|
||||
blank=True
|
||||
)
|
||||
type = models.CharField(
|
||||
max_length=50,
|
||||
choices=ConsolePortTypeChoices,
|
||||
blank=True
|
||||
)
|
||||
|
||||
objects = NaturalOrderingManager()
|
||||
|
||||
class Meta:
|
||||
ordering = ['device_type', 'name']
|
||||
unique_together = ['device_type', 'name']
|
||||
ordering = ('device_type', '_name')
|
||||
unique_together = ('device_type', 'name')
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
@@ -128,6 +134,11 @@ class PowerPortTemplate(ComponentTemplateModel):
|
||||
name = models.CharField(
|
||||
max_length=50
|
||||
)
|
||||
_name = NaturalOrderingField(
|
||||
target_field='name',
|
||||
max_length=100,
|
||||
blank=True
|
||||
)
|
||||
type = models.CharField(
|
||||
max_length=50,
|
||||
choices=PowerPortTypeChoices,
|
||||
@@ -146,11 +157,9 @@ class PowerPortTemplate(ComponentTemplateModel):
|
||||
help_text="Allocated power draw (watts)"
|
||||
)
|
||||
|
||||
objects = NaturalOrderingManager()
|
||||
|
||||
class Meta:
|
||||
ordering = ['device_type', 'name']
|
||||
unique_together = ['device_type', 'name']
|
||||
ordering = ('device_type', '_name')
|
||||
unique_together = ('device_type', 'name')
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
@@ -159,6 +168,7 @@ class PowerPortTemplate(ComponentTemplateModel):
|
||||
return PowerPort(
|
||||
device=device,
|
||||
name=self.name,
|
||||
type=self.type,
|
||||
maximum_draw=self.maximum_draw,
|
||||
allocated_draw=self.allocated_draw
|
||||
)
|
||||
@@ -176,6 +186,11 @@ class PowerOutletTemplate(ComponentTemplateModel):
|
||||
name = models.CharField(
|
||||
max_length=50
|
||||
)
|
||||
_name = NaturalOrderingField(
|
||||
target_field='name',
|
||||
max_length=100,
|
||||
blank=True
|
||||
)
|
||||
type = models.CharField(
|
||||
max_length=50,
|
||||
choices=PowerOutletTypeChoices,
|
||||
@@ -195,11 +210,9 @@ class PowerOutletTemplate(ComponentTemplateModel):
|
||||
help_text="Phase (for three-phase feeds)"
|
||||
)
|
||||
|
||||
objects = NaturalOrderingManager()
|
||||
|
||||
class Meta:
|
||||
ordering = ['device_type', 'name']
|
||||
unique_together = ['device_type', 'name']
|
||||
ordering = ('device_type', '_name')
|
||||
unique_together = ('device_type', 'name')
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
@@ -220,6 +233,7 @@ class PowerOutletTemplate(ComponentTemplateModel):
|
||||
return PowerOutlet(
|
||||
device=device,
|
||||
name=self.name,
|
||||
type=self.type,
|
||||
power_port=power_port,
|
||||
feed_leg=self.feed_leg
|
||||
)
|
||||
@@ -237,6 +251,12 @@ class InterfaceTemplate(ComponentTemplateModel):
|
||||
name = models.CharField(
|
||||
max_length=64
|
||||
)
|
||||
_name = NaturalOrderingField(
|
||||
target_field='name',
|
||||
naturalize_function=naturalize_interface,
|
||||
max_length=100,
|
||||
blank=True
|
||||
)
|
||||
type = models.CharField(
|
||||
max_length=50,
|
||||
choices=InterfaceTypeChoices
|
||||
@@ -246,11 +266,9 @@ class InterfaceTemplate(ComponentTemplateModel):
|
||||
verbose_name='Management only'
|
||||
)
|
||||
|
||||
objects = InterfaceManager()
|
||||
|
||||
class Meta:
|
||||
ordering = ['device_type', 'name']
|
||||
unique_together = ['device_type', 'name']
|
||||
ordering = ('device_type', '_name')
|
||||
unique_together = ('device_type', 'name')
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
@@ -276,6 +294,11 @@ class FrontPortTemplate(ComponentTemplateModel):
|
||||
name = models.CharField(
|
||||
max_length=64
|
||||
)
|
||||
_name = NaturalOrderingField(
|
||||
target_field='name',
|
||||
max_length=100,
|
||||
blank=True
|
||||
)
|
||||
type = models.CharField(
|
||||
max_length=50,
|
||||
choices=PortTypeChoices
|
||||
@@ -290,14 +313,12 @@ class FrontPortTemplate(ComponentTemplateModel):
|
||||
validators=[MinValueValidator(1), MaxValueValidator(64)]
|
||||
)
|
||||
|
||||
objects = NaturalOrderingManager()
|
||||
|
||||
class Meta:
|
||||
ordering = ['device_type', 'name']
|
||||
unique_together = [
|
||||
['device_type', 'name'],
|
||||
['rear_port', 'rear_port_position'],
|
||||
]
|
||||
ordering = ('device_type', '_name')
|
||||
unique_together = (
|
||||
('device_type', 'name'),
|
||||
('rear_port', 'rear_port_position'),
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
@@ -344,6 +365,11 @@ class RearPortTemplate(ComponentTemplateModel):
|
||||
name = models.CharField(
|
||||
max_length=64
|
||||
)
|
||||
_name = NaturalOrderingField(
|
||||
target_field='name',
|
||||
max_length=100,
|
||||
blank=True
|
||||
)
|
||||
type = models.CharField(
|
||||
max_length=50,
|
||||
choices=PortTypeChoices
|
||||
@@ -353,11 +379,9 @@ class RearPortTemplate(ComponentTemplateModel):
|
||||
validators=[MinValueValidator(1), MaxValueValidator(64)]
|
||||
)
|
||||
|
||||
objects = NaturalOrderingManager()
|
||||
|
||||
class Meta:
|
||||
ordering = ['device_type', 'name']
|
||||
unique_together = ['device_type', 'name']
|
||||
ordering = ('device_type', '_name')
|
||||
unique_together = ('device_type', 'name')
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
@@ -383,12 +407,15 @@ class DeviceBayTemplate(ComponentTemplateModel):
|
||||
name = models.CharField(
|
||||
max_length=50
|
||||
)
|
||||
|
||||
objects = NaturalOrderingManager()
|
||||
_name = NaturalOrderingField(
|
||||
target_field='name',
|
||||
max_length=100,
|
||||
blank=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ['device_type', 'name']
|
||||
unique_together = ['device_type', 'name']
|
||||
ordering = ('device_type', '_name')
|
||||
unique_together = ('device_type', 'name')
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@@ -10,9 +10,9 @@ from dcim.choices import *
|
||||
from dcim.constants import *
|
||||
from dcim.exceptions import LoopDetected
|
||||
from dcim.fields import MACAddressField
|
||||
from dcim.managers import InterfaceManager
|
||||
from extras.models import ObjectChange, TaggedItem
|
||||
from utilities.managers import NaturalOrderingManager
|
||||
from utilities.fields import NaturalOrderingField
|
||||
from utilities.ordering import naturalize_interface
|
||||
from utilities.utils import serialize_object
|
||||
from virtualization.choices import VMInterfaceTypeChoices
|
||||
|
||||
@@ -181,6 +181,11 @@ class ConsolePort(CableTermination, ComponentModel):
|
||||
name = models.CharField(
|
||||
max_length=50
|
||||
)
|
||||
_name = NaturalOrderingField(
|
||||
target_field='name',
|
||||
max_length=100,
|
||||
blank=True
|
||||
)
|
||||
type = models.CharField(
|
||||
max_length=50,
|
||||
choices=ConsolePortTypeChoices,
|
||||
@@ -197,15 +202,13 @@ class ConsolePort(CableTermination, ComponentModel):
|
||||
choices=CONNECTION_STATUS_CHOICES,
|
||||
blank=True
|
||||
)
|
||||
|
||||
objects = NaturalOrderingManager()
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
csv_headers = ['device', 'name', 'type', 'description']
|
||||
|
||||
class Meta:
|
||||
ordering = ['device', 'name']
|
||||
unique_together = ['device', 'name']
|
||||
ordering = ('device', '_name')
|
||||
unique_together = ('device', 'name')
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
@@ -238,6 +241,11 @@ class ConsoleServerPort(CableTermination, ComponentModel):
|
||||
name = models.CharField(
|
||||
max_length=50
|
||||
)
|
||||
_name = NaturalOrderingField(
|
||||
target_field='name',
|
||||
max_length=100,
|
||||
blank=True
|
||||
)
|
||||
type = models.CharField(
|
||||
max_length=50,
|
||||
choices=ConsolePortTypeChoices,
|
||||
@@ -247,14 +255,13 @@ class ConsoleServerPort(CableTermination, ComponentModel):
|
||||
choices=CONNECTION_STATUS_CHOICES,
|
||||
blank=True
|
||||
)
|
||||
|
||||
objects = NaturalOrderingManager()
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
csv_headers = ['device', 'name', 'type', 'description']
|
||||
|
||||
class Meta:
|
||||
unique_together = ['device', 'name']
|
||||
ordering = ('device', '_name')
|
||||
unique_together = ('device', 'name')
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
@@ -287,6 +294,11 @@ class PowerPort(CableTermination, ComponentModel):
|
||||
name = models.CharField(
|
||||
max_length=50
|
||||
)
|
||||
_name = NaturalOrderingField(
|
||||
target_field='name',
|
||||
max_length=100,
|
||||
blank=True
|
||||
)
|
||||
type = models.CharField(
|
||||
max_length=50,
|
||||
choices=PowerPortTypeChoices,
|
||||
@@ -322,15 +334,13 @@ class PowerPort(CableTermination, ComponentModel):
|
||||
choices=CONNECTION_STATUS_CHOICES,
|
||||
blank=True
|
||||
)
|
||||
|
||||
objects = NaturalOrderingManager()
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
csv_headers = ['device', 'name', 'type', 'maximum_draw', 'allocated_draw', 'description']
|
||||
|
||||
class Meta:
|
||||
ordering = ['device', 'name']
|
||||
unique_together = ['device', 'name']
|
||||
ordering = ('device', '_name')
|
||||
unique_together = ('device', 'name')
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
@@ -350,9 +360,21 @@ class PowerPort(CableTermination, ComponentModel):
|
||||
|
||||
@property
|
||||
def connected_endpoint(self):
|
||||
if self._connected_poweroutlet:
|
||||
return self._connected_poweroutlet
|
||||
return self._connected_powerfeed
|
||||
"""
|
||||
Return the connected PowerOutlet, if it exists, or the connected PowerFeed, if it exists. We have to check for
|
||||
ObjectDoesNotExist in case the referenced object has been deleted from the database.
|
||||
"""
|
||||
try:
|
||||
if self._connected_poweroutlet:
|
||||
return self._connected_poweroutlet
|
||||
except ObjectDoesNotExist:
|
||||
pass
|
||||
try:
|
||||
if self._connected_powerfeed:
|
||||
return self._connected_powerfeed
|
||||
except ObjectDoesNotExist:
|
||||
pass
|
||||
return None
|
||||
|
||||
@connected_endpoint.setter
|
||||
def connected_endpoint(self, value):
|
||||
@@ -433,6 +455,11 @@ class PowerOutlet(CableTermination, ComponentModel):
|
||||
name = models.CharField(
|
||||
max_length=50
|
||||
)
|
||||
_name = NaturalOrderingField(
|
||||
target_field='name',
|
||||
max_length=100,
|
||||
blank=True
|
||||
)
|
||||
type = models.CharField(
|
||||
max_length=50,
|
||||
choices=PowerOutletTypeChoices,
|
||||
@@ -455,14 +482,13 @@ class PowerOutlet(CableTermination, ComponentModel):
|
||||
choices=CONNECTION_STATUS_CHOICES,
|
||||
blank=True
|
||||
)
|
||||
|
||||
objects = NaturalOrderingManager()
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
csv_headers = ['device', 'name', 'type', 'power_port', 'feed_leg', 'description']
|
||||
|
||||
class Meta:
|
||||
unique_together = ['device', 'name']
|
||||
ordering = ('device', '_name')
|
||||
unique_together = ('device', 'name')
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
@@ -515,6 +541,12 @@ class Interface(CableTermination, ComponentModel):
|
||||
name = models.CharField(
|
||||
max_length=64
|
||||
)
|
||||
_name = NaturalOrderingField(
|
||||
target_field='name',
|
||||
naturalize_function=naturalize_interface,
|
||||
max_length=100,
|
||||
blank=True
|
||||
)
|
||||
_connected_interface = models.OneToOneField(
|
||||
to='self',
|
||||
on_delete=models.SET_NULL,
|
||||
@@ -583,8 +615,6 @@ class Interface(CableTermination, ComponentModel):
|
||||
blank=True,
|
||||
verbose_name='Tagged VLANs'
|
||||
)
|
||||
|
||||
objects = InterfaceManager()
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
csv_headers = [
|
||||
@@ -593,8 +623,9 @@ class Interface(CableTermination, ComponentModel):
|
||||
]
|
||||
|
||||
class Meta:
|
||||
ordering = ['device', 'name']
|
||||
unique_together = ['device', 'name']
|
||||
# TODO: ordering and unique_together should include virtual_machine
|
||||
ordering = ('device', '_name')
|
||||
unique_together = ('device', 'name')
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
@@ -698,9 +729,21 @@ class Interface(CableTermination, ComponentModel):
|
||||
|
||||
@property
|
||||
def connected_endpoint(self):
|
||||
if self._connected_interface:
|
||||
return self._connected_interface
|
||||
return self._connected_circuittermination
|
||||
"""
|
||||
Return the connected Interface, if it exists, or the connected CircuitTermination, if it exists. We have to
|
||||
check for ObjectDoesNotExist in case the referenced object has been deleted from the database.
|
||||
"""
|
||||
try:
|
||||
if self._connected_interface:
|
||||
return self._connected_interface
|
||||
except ObjectDoesNotExist:
|
||||
pass
|
||||
try:
|
||||
if self._connected_circuittermination:
|
||||
return self._connected_circuittermination
|
||||
except ObjectDoesNotExist:
|
||||
pass
|
||||
return None
|
||||
|
||||
@connected_endpoint.setter
|
||||
def connected_endpoint(self, value):
|
||||
@@ -761,6 +804,11 @@ class FrontPort(CableTermination, ComponentModel):
|
||||
name = models.CharField(
|
||||
max_length=64
|
||||
)
|
||||
_name = NaturalOrderingField(
|
||||
target_field='name',
|
||||
max_length=100,
|
||||
blank=True
|
||||
)
|
||||
type = models.CharField(
|
||||
max_length=50,
|
||||
choices=PortTypeChoices
|
||||
@@ -774,20 +822,17 @@ class FrontPort(CableTermination, ComponentModel):
|
||||
default=1,
|
||||
validators=[MinValueValidator(1), MaxValueValidator(64)]
|
||||
)
|
||||
|
||||
is_path_endpoint = False
|
||||
|
||||
objects = NaturalOrderingManager()
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
csv_headers = ['device', 'name', 'type', 'rear_port', 'rear_port_position', 'description']
|
||||
is_path_endpoint = False
|
||||
|
||||
class Meta:
|
||||
ordering = ['device', 'name']
|
||||
unique_together = [
|
||||
['device', 'name'],
|
||||
['rear_port', 'rear_port_position'],
|
||||
]
|
||||
ordering = ('device', '_name')
|
||||
unique_together = (
|
||||
('device', 'name'),
|
||||
('rear_port', 'rear_port_position'),
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
@@ -831,6 +876,11 @@ class RearPort(CableTermination, ComponentModel):
|
||||
name = models.CharField(
|
||||
max_length=64
|
||||
)
|
||||
_name = NaturalOrderingField(
|
||||
target_field='name',
|
||||
max_length=100,
|
||||
blank=True
|
||||
)
|
||||
type = models.CharField(
|
||||
max_length=50,
|
||||
choices=PortTypeChoices
|
||||
@@ -839,17 +889,14 @@ class RearPort(CableTermination, ComponentModel):
|
||||
default=1,
|
||||
validators=[MinValueValidator(1), MaxValueValidator(64)]
|
||||
)
|
||||
|
||||
is_path_endpoint = False
|
||||
|
||||
objects = NaturalOrderingManager()
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
csv_headers = ['device', 'name', 'type', 'positions', 'description']
|
||||
is_path_endpoint = False
|
||||
|
||||
class Meta:
|
||||
ordering = ['device', 'name']
|
||||
unique_together = ['device', 'name']
|
||||
ordering = ('device', '_name')
|
||||
unique_together = ('device', 'name')
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
@@ -881,6 +928,11 @@ class DeviceBay(ComponentModel):
|
||||
max_length=50,
|
||||
verbose_name='Name'
|
||||
)
|
||||
_name = NaturalOrderingField(
|
||||
target_field='name',
|
||||
max_length=100,
|
||||
blank=True
|
||||
)
|
||||
installed_device = models.OneToOneField(
|
||||
to='dcim.Device',
|
||||
on_delete=models.SET_NULL,
|
||||
@@ -888,15 +940,13 @@ class DeviceBay(ComponentModel):
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
|
||||
objects = NaturalOrderingManager()
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
csv_headers = ['device', 'name', 'installed_device', 'description']
|
||||
|
||||
class Meta:
|
||||
ordering = ['device', 'name']
|
||||
unique_together = ['device', 'name']
|
||||
ordering = ('device', '_name')
|
||||
unique_together = ('device', 'name')
|
||||
|
||||
def __str__(self):
|
||||
return '{} - {}'.format(self.device.name, self.name)
|
||||
@@ -960,6 +1010,11 @@ class InventoryItem(ComponentModel):
|
||||
max_length=50,
|
||||
verbose_name='Name'
|
||||
)
|
||||
_name = NaturalOrderingField(
|
||||
target_field='name',
|
||||
max_length=100,
|
||||
blank=True
|
||||
)
|
||||
manufacturer = models.ForeignKey(
|
||||
to='dcim.Manufacturer',
|
||||
on_delete=models.PROTECT,
|
||||
@@ -997,14 +1052,14 @@ class InventoryItem(ComponentModel):
|
||||
]
|
||||
|
||||
class Meta:
|
||||
ordering = ['device__id', 'parent__id', 'name']
|
||||
unique_together = ['device', 'parent', 'name']
|
||||
ordering = ('device__id', 'parent__id', '_name')
|
||||
unique_together = ('device', 'parent', 'name')
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return self.device.get_absolute_url()
|
||||
return reverse('dcim:device_inventory', kwargs={'pk': self.device.pk})
|
||||
|
||||
def to_csv(self):
|
||||
return (
|
||||
|
||||
@@ -41,7 +41,7 @@ DEVICE_LINK = """
|
||||
"""
|
||||
|
||||
REGION_ACTIONS = """
|
||||
<a href="{% url 'dcim:region_changelog' pk=record.pk %}" class="btn btn-default btn-xs" title="Changelog">
|
||||
<a href="{% url 'dcim:region_changelog' pk=record.pk %}" class="btn btn-default btn-xs" title="Change log">
|
||||
<i class="fa fa-history"></i>
|
||||
</a>
|
||||
{% if perms.dcim.change_region %}
|
||||
@@ -50,7 +50,7 @@ REGION_ACTIONS = """
|
||||
"""
|
||||
|
||||
RACKGROUP_ACTIONS = """
|
||||
<a href="{% url 'dcim:rackgroup_changelog' pk=record.pk %}" class="btn btn-default btn-xs" title="Changelog">
|
||||
<a href="{% url 'dcim:rackgroup_changelog' pk=record.pk %}" class="btn btn-default btn-xs" title="Change log">
|
||||
<i class="fa fa-history"></i>
|
||||
</a>
|
||||
<a href="{% url 'dcim:rack_elevation_list' %}?site={{ record.site.slug }}&group_id={{ record.pk }}" class="btn btn-xs btn-primary" title="View elevations">
|
||||
@@ -64,7 +64,7 @@ RACKGROUP_ACTIONS = """
|
||||
"""
|
||||
|
||||
RACKROLE_ACTIONS = """
|
||||
<a href="{% url 'dcim:rackrole_changelog' pk=record.pk %}" class="btn btn-default btn-xs" title="Changelog">
|
||||
<a href="{% url 'dcim:rackrole_changelog' pk=record.pk %}" class="btn btn-default btn-xs" title="Change log">
|
||||
<i class="fa fa-history"></i>
|
||||
</a>
|
||||
{% if perms.dcim.change_rackrole %}
|
||||
@@ -86,7 +86,7 @@ RACK_DEVICE_COUNT = """
|
||||
"""
|
||||
|
||||
RACKRESERVATION_ACTIONS = """
|
||||
<a href="{% url 'dcim:rackreservation_changelog' pk=record.pk %}" class="btn btn-default btn-xs" title="Changelog">
|
||||
<a href="{% url 'dcim:rackreservation_changelog' pk=record.pk %}" class="btn btn-default btn-xs" title="Change log">
|
||||
<i class="fa fa-history"></i>
|
||||
</a>
|
||||
{% if perms.dcim.change_rackreservation %}
|
||||
@@ -95,7 +95,7 @@ RACKRESERVATION_ACTIONS = """
|
||||
"""
|
||||
|
||||
MANUFACTURER_ACTIONS = """
|
||||
<a href="{% url 'dcim:manufacturer_changelog' slug=record.slug %}" class="btn btn-default btn-xs" title="Changelog">
|
||||
<a href="{% url 'dcim:manufacturer_changelog' slug=record.slug %}" class="btn btn-default btn-xs" title="Change log">
|
||||
<i class="fa fa-history"></i>
|
||||
</a>
|
||||
{% if perms.dcim.change_manufacturer %}
|
||||
@@ -104,7 +104,7 @@ MANUFACTURER_ACTIONS = """
|
||||
"""
|
||||
|
||||
DEVICEROLE_ACTIONS = """
|
||||
<a href="{% url 'dcim:devicerole_changelog' slug=record.slug %}" class="btn btn-default btn-xs" title="Changelog">
|
||||
<a href="{% url 'dcim:devicerole_changelog' slug=record.slug %}" class="btn btn-default btn-xs" title="Change log">
|
||||
<i class="fa fa-history"></i>
|
||||
</a>
|
||||
{% if perms.dcim.change_devicerole %}
|
||||
@@ -129,7 +129,7 @@ PLATFORM_VM_COUNT = """
|
||||
"""
|
||||
|
||||
PLATFORM_ACTIONS = """
|
||||
<a href="{% url 'dcim:platform_changelog' slug=record.slug %}" class="btn btn-default btn-xs" title="Changelog">
|
||||
<a href="{% url 'dcim:platform_changelog' slug=record.slug %}" class="btn btn-default btn-xs" title="Change log">
|
||||
<i class="fa fa-history"></i>
|
||||
</a>
|
||||
{% if perms.dcim.change_platform %}
|
||||
@@ -166,7 +166,7 @@ UTILIZATION_GRAPH = """
|
||||
"""
|
||||
|
||||
VIRTUALCHASSIS_ACTIONS = """
|
||||
<a href="{% url 'dcim:virtualchassis_changelog' pk=record.pk %}" class="btn btn-default btn-xs" title="Changelog">
|
||||
<a href="{% url 'dcim:virtualchassis_changelog' pk=record.pk %}" class="btn btn-default btn-xs" title="Change log">
|
||||
<i class="fa fa-history"></i>
|
||||
</a>
|
||||
{% if perms.dcim.change_virtualchassis %}
|
||||
@@ -200,6 +200,11 @@ def get_component_template_actions(model_name):
|
||||
<i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
|
||||
</a>
|
||||
{{% endif %}}
|
||||
{{% if perms.dcim.delete_{model_name} %}}
|
||||
<a href="{{% url 'dcim:{model_name}_delete' pk=record.pk %}}?return_url={{{{ request.path }}}}" class="btn btn-xs btn-danger">
|
||||
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
|
||||
</a>
|
||||
{{% endif %}}
|
||||
""".format(model_name=model_name).strip()
|
||||
|
||||
|
||||
@@ -229,7 +234,7 @@ class RegionTable(BaseTable):
|
||||
|
||||
class SiteTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.LinkColumn(order_by=('_nat1', '_nat2', '_nat3'))
|
||||
name = tables.LinkColumn(order_by=('_name',))
|
||||
status = tables.TemplateColumn(template_code=STATUS_LABEL, verbose_name='Status')
|
||||
region = tables.TemplateColumn(template_code=SITE_REGION_LINK)
|
||||
tenant = tables.TemplateColumn(template_code=COL_TENANT)
|
||||
@@ -291,7 +296,7 @@ class RackRoleTable(BaseTable):
|
||||
|
||||
class RackTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.LinkColumn(order_by=('_nat1', '_nat2', '_nat3'))
|
||||
name = tables.LinkColumn(order_by=('_name',))
|
||||
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')])
|
||||
group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group')
|
||||
tenant = tables.TemplateColumn(template_code=COL_TENANT)
|
||||
@@ -409,6 +414,7 @@ class DeviceTypeTable(BaseTable):
|
||||
|
||||
class ConsolePortTemplateTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.Column(order_by=('_name',))
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=get_component_template_actions('consoleporttemplate'),
|
||||
attrs={'td': {'class': 'text-right noprint'}},
|
||||
@@ -432,6 +438,7 @@ class ConsolePortImportTable(BaseTable):
|
||||
|
||||
class ConsoleServerPortTemplateTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.Column(order_by=('_name',))
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=get_component_template_actions('consoleserverporttemplate'),
|
||||
attrs={'td': {'class': 'text-right noprint'}},
|
||||
@@ -440,7 +447,7 @@ class ConsoleServerPortTemplateTable(BaseTable):
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = ConsoleServerPortTemplate
|
||||
fields = ('pk', 'name', 'actions')
|
||||
fields = ('pk', 'name', 'type', 'actions')
|
||||
empty_text = "None"
|
||||
|
||||
|
||||
@@ -455,6 +462,7 @@ class ConsoleServerPortImportTable(BaseTable):
|
||||
|
||||
class PowerPortTemplateTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.Column(order_by=('_name',))
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=get_component_template_actions('powerporttemplate'),
|
||||
attrs={'td': {'class': 'text-right noprint'}},
|
||||
@@ -478,6 +486,7 @@ class PowerPortImportTable(BaseTable):
|
||||
|
||||
class PowerOutletTemplateTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.Column(order_by=('_name',))
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=get_component_template_actions('poweroutlettemplate'),
|
||||
attrs={'td': {'class': 'text-right noprint'}},
|
||||
@@ -526,6 +535,7 @@ class InterfaceImportTable(BaseTable):
|
||||
|
||||
class FrontPortTemplateTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.Column(order_by=('_name',))
|
||||
rear_port_position = tables.Column(
|
||||
verbose_name='Position'
|
||||
)
|
||||
@@ -552,6 +562,7 @@ class FrontPortImportTable(BaseTable):
|
||||
|
||||
class RearPortTemplateTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.Column(order_by=('_name',))
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=get_component_template_actions('rearporttemplate'),
|
||||
attrs={'td': {'class': 'text-right noprint'}},
|
||||
@@ -575,6 +586,7 @@ class RearPortImportTable(BaseTable):
|
||||
|
||||
class DeviceBayTemplateTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.Column(order_by=('_name',))
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=get_component_template_actions('devicebaytemplate'),
|
||||
attrs={'td': {'class': 'text-right noprint'}},
|
||||
@@ -654,7 +666,7 @@ class PlatformTable(BaseTable):
|
||||
class DeviceTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.TemplateColumn(
|
||||
order_by=('_nat1', '_nat2', '_nat3'),
|
||||
order_by=('_name',),
|
||||
template_code=DEVICE_LINK
|
||||
)
|
||||
status = tables.TemplateColumn(template_code=STATUS_LABEL, verbose_name='Status')
|
||||
@@ -704,6 +716,7 @@ class DeviceImportTable(BaseTable):
|
||||
|
||||
class DeviceComponentDetailTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.Column(order_by=('_name',))
|
||||
cable = tables.LinkColumn()
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
@@ -713,6 +726,7 @@ class DeviceComponentDetailTable(BaseTable):
|
||||
|
||||
|
||||
class ConsolePortTable(BaseTable):
|
||||
name = tables.Column(order_by=('_name',))
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = ConsolePort
|
||||
@@ -727,6 +741,7 @@ class ConsolePortDetailTable(DeviceComponentDetailTable):
|
||||
|
||||
|
||||
class ConsoleServerPortTable(BaseTable):
|
||||
name = tables.Column(order_by=('_name',))
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = ConsoleServerPort
|
||||
@@ -741,6 +756,7 @@ class ConsoleServerPortDetailTable(DeviceComponentDetailTable):
|
||||
|
||||
|
||||
class PowerPortTable(BaseTable):
|
||||
name = tables.Column(order_by=('_name',))
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = PowerPort
|
||||
@@ -755,6 +771,7 @@ class PowerPortDetailTable(DeviceComponentDetailTable):
|
||||
|
||||
|
||||
class PowerOutletTable(BaseTable):
|
||||
name = tables.Column(order_by=('_name',))
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = PowerOutlet
|
||||
@@ -777,14 +794,17 @@ class InterfaceTable(BaseTable):
|
||||
|
||||
class InterfaceDetailTable(DeviceComponentDetailTable):
|
||||
parent = tables.LinkColumn(order_by=('device', 'virtual_machine'))
|
||||
name = tables.LinkColumn()
|
||||
enabled = BooleanColumn()
|
||||
|
||||
class Meta(InterfaceTable.Meta):
|
||||
order_by = ('parent', 'name')
|
||||
fields = ('pk', 'parent', 'name', 'type', 'description', 'cable')
|
||||
sequence = ('pk', 'parent', 'name', 'type', 'description', 'cable')
|
||||
fields = ('pk', 'parent', 'name', 'enabled', 'type', 'description', 'cable')
|
||||
sequence = ('pk', 'parent', 'name', 'enabled', 'type', 'description', 'cable')
|
||||
|
||||
|
||||
class FrontPortTable(BaseTable):
|
||||
name = tables.Column(order_by=('_name',))
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = FrontPort
|
||||
@@ -800,6 +820,7 @@ class FrontPortDetailTable(DeviceComponentDetailTable):
|
||||
|
||||
|
||||
class RearPortTable(BaseTable):
|
||||
name = tables.Column(order_by=('_name',))
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = RearPort
|
||||
@@ -815,6 +836,7 @@ class RearPortDetailTable(DeviceComponentDetailTable):
|
||||
|
||||
|
||||
class DeviceBayTable(BaseTable):
|
||||
name = tables.Column(order_by=('_name',))
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = DeviceBay
|
||||
|
||||
@@ -596,6 +596,28 @@ class RackTest(APITestCase):
|
||||
|
||||
self.assertEqual(response.data['count'], 42)
|
||||
|
||||
def test_get_elevation_rack_units(self):
|
||||
|
||||
url = '{}?q=3'.format(reverse('dcim-api:rack-elevation', kwargs={'pk': self.rack1.pk}))
|
||||
response = self.client.get(url, **self.header)
|
||||
|
||||
self.assertEqual(response.data['count'], 13)
|
||||
|
||||
url = '{}?q=U3'.format(reverse('dcim-api:rack-elevation', kwargs={'pk': self.rack1.pk}))
|
||||
response = self.client.get(url, **self.header)
|
||||
|
||||
self.assertEqual(response.data['count'], 11)
|
||||
|
||||
url = '{}?q=10'.format(reverse('dcim-api:rack-elevation', kwargs={'pk': self.rack1.pk}))
|
||||
response = self.client.get(url, **self.header)
|
||||
|
||||
self.assertEqual(response.data['count'], 1)
|
||||
|
||||
url = '{}?q=U20'.format(reverse('dcim-api:rack-elevation', kwargs={'pk': self.rack1.pk}))
|
||||
response = self.client.get(url, **self.header)
|
||||
|
||||
self.assertEqual(response.data['count'], 1)
|
||||
|
||||
def test_get_rack_elevation(self):
|
||||
|
||||
url = reverse('dcim-api:rack-elevation', kwargs={'pk': self.rack1.pk})
|
||||
@@ -1448,13 +1470,13 @@ class InterfaceTemplateTest(APITestCase):
|
||||
manufacturer=self.manufacturer, model='Test Device Type 1', slug='test-device-type-1'
|
||||
)
|
||||
self.interfacetemplate1 = InterfaceTemplate.objects.create(
|
||||
device_type=self.devicetype, name='Test Interface Template 1'
|
||||
device_type=self.devicetype, name='Test Interface Template 1', type='1000base-t'
|
||||
)
|
||||
self.interfacetemplate2 = InterfaceTemplate.objects.create(
|
||||
device_type=self.devicetype, name='Test Interface Template 2'
|
||||
device_type=self.devicetype, name='Test Interface Template 2', type='1000base-t'
|
||||
)
|
||||
self.interfacetemplate3 = InterfaceTemplate.objects.create(
|
||||
device_type=self.devicetype, name='Test Interface Template 3'
|
||||
device_type=self.devicetype, name='Test Interface Template 3', type='1000base-t'
|
||||
)
|
||||
|
||||
def test_get_interfacetemplate(self):
|
||||
@@ -1476,6 +1498,7 @@ class InterfaceTemplateTest(APITestCase):
|
||||
data = {
|
||||
'device_type': self.devicetype.pk,
|
||||
'name': 'Test Interface Template 4',
|
||||
'type': '1000base-t',
|
||||
}
|
||||
|
||||
url = reverse('dcim-api:interfacetemplate-list')
|
||||
@@ -1493,14 +1516,17 @@ class InterfaceTemplateTest(APITestCase):
|
||||
{
|
||||
'device_type': self.devicetype.pk,
|
||||
'name': 'Test Interface Template 4',
|
||||
'type': '1000base-t',
|
||||
},
|
||||
{
|
||||
'device_type': self.devicetype.pk,
|
||||
'name': 'Test Interface Template 5',
|
||||
'type': '1000base-t',
|
||||
},
|
||||
{
|
||||
'device_type': self.devicetype.pk,
|
||||
'name': 'Test Interface Template 6',
|
||||
'type': '1000base-t',
|
||||
},
|
||||
]
|
||||
|
||||
@@ -1518,6 +1544,7 @@ class InterfaceTemplateTest(APITestCase):
|
||||
data = {
|
||||
'device_type': self.devicetype.pk,
|
||||
'name': 'Test Interface Template X',
|
||||
'type': '1000base-x-gbic',
|
||||
}
|
||||
|
||||
url = reverse('dcim-api:interfacetemplate-detail', kwargs={'pk': self.interfacetemplate1.pk})
|
||||
@@ -2628,9 +2655,9 @@ class InterfaceTest(APITestCase):
|
||||
self.device = Device.objects.create(
|
||||
device_type=devicetype, device_role=devicerole, name='Test Device 1', site=site
|
||||
)
|
||||
self.interface1 = Interface.objects.create(device=self.device, name='Test Interface 1')
|
||||
self.interface2 = Interface.objects.create(device=self.device, name='Test Interface 2')
|
||||
self.interface3 = Interface.objects.create(device=self.device, name='Test Interface 3')
|
||||
self.interface1 = Interface.objects.create(device=self.device, name='Test Interface 1', type='1000base-t')
|
||||
self.interface2 = Interface.objects.create(device=self.device, name='Test Interface 2', type='1000base-t')
|
||||
self.interface3 = Interface.objects.create(device=self.device, name='Test Interface 3', type='1000base-t')
|
||||
|
||||
self.vlan1 = VLAN.objects.create(name="Test VLAN 1", vid=1)
|
||||
self.vlan2 = VLAN.objects.create(name="Test VLAN 2", vid=2)
|
||||
@@ -2691,6 +2718,7 @@ class InterfaceTest(APITestCase):
|
||||
data = {
|
||||
'device': self.device.pk,
|
||||
'name': 'Test Interface 4',
|
||||
'type': '1000base-t',
|
||||
}
|
||||
|
||||
url = reverse('dcim-api:interface-list')
|
||||
@@ -2707,6 +2735,7 @@ class InterfaceTest(APITestCase):
|
||||
data = {
|
||||
'device': self.device.pk,
|
||||
'name': 'Test Interface 4',
|
||||
'type': '1000base-t',
|
||||
'mode': InterfaceModeChoices.MODE_TAGGED,
|
||||
'untagged_vlan': self.vlan3.id,
|
||||
'tagged_vlans': [self.vlan1.id, self.vlan2.id],
|
||||
@@ -2728,14 +2757,17 @@ class InterfaceTest(APITestCase):
|
||||
{
|
||||
'device': self.device.pk,
|
||||
'name': 'Test Interface 4',
|
||||
'type': '1000base-t',
|
||||
},
|
||||
{
|
||||
'device': self.device.pk,
|
||||
'name': 'Test Interface 5',
|
||||
'type': '1000base-t',
|
||||
},
|
||||
{
|
||||
'device': self.device.pk,
|
||||
'name': 'Test Interface 6',
|
||||
'type': '1000base-t',
|
||||
},
|
||||
]
|
||||
|
||||
@@ -2754,6 +2786,7 @@ class InterfaceTest(APITestCase):
|
||||
{
|
||||
'device': self.device.pk,
|
||||
'name': 'Test Interface 4',
|
||||
'type': '1000base-t',
|
||||
'mode': InterfaceModeChoices.MODE_TAGGED,
|
||||
'untagged_vlan': self.vlan2.id,
|
||||
'tagged_vlans': [self.vlan1.id],
|
||||
@@ -2761,6 +2794,7 @@ class InterfaceTest(APITestCase):
|
||||
{
|
||||
'device': self.device.pk,
|
||||
'name': 'Test Interface 5',
|
||||
'type': '1000base-t',
|
||||
'mode': InterfaceModeChoices.MODE_TAGGED,
|
||||
'untagged_vlan': self.vlan2.id,
|
||||
'tagged_vlans': [self.vlan1.id],
|
||||
@@ -2768,6 +2802,7 @@ class InterfaceTest(APITestCase):
|
||||
{
|
||||
'device': self.device.pk,
|
||||
'name': 'Test Interface 6',
|
||||
'type': '1000base-t',
|
||||
'mode': InterfaceModeChoices.MODE_TAGGED,
|
||||
'untagged_vlan': self.vlan2.id,
|
||||
'tagged_vlans': [self.vlan1.id],
|
||||
@@ -2793,6 +2828,7 @@ class InterfaceTest(APITestCase):
|
||||
data = {
|
||||
'device': self.device.pk,
|
||||
'name': 'Test Interface X',
|
||||
'type': '1000base-x-gbic',
|
||||
'lag': lag_interface.pk,
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ from django.core.exceptions import ValidationError
|
||||
from django.test import TestCase
|
||||
|
||||
from dcim.choices import *
|
||||
from dcim.constants import CONNECTION_STATUS_CONNECTED, CONNECTION_STATUS_PLANNED
|
||||
from dcim.models import *
|
||||
from tenancy.models import Tenant
|
||||
|
||||
@@ -522,14 +521,14 @@ class CablePathTestCase(TestCase):
|
||||
cable3.save()
|
||||
interface1 = Interface.objects.get(pk=self.interface1.pk)
|
||||
self.assertEqual(interface1.connected_endpoint, self.interface2)
|
||||
self.assertEqual(interface1.connection_status, CONNECTION_STATUS_PLANNED)
|
||||
self.assertFalse(interface1.connection_status)
|
||||
|
||||
# Switch third segment from planned to connected
|
||||
cable3.status = CableStatusChoices.STATUS_CONNECTED
|
||||
cable3.save()
|
||||
interface1 = Interface.objects.get(pk=self.interface1.pk)
|
||||
self.assertEqual(interface1.connected_endpoint, self.interface2)
|
||||
self.assertEqual(interface1.connection_status, CONNECTION_STATUS_CONNECTED)
|
||||
self.assertTrue(interface1.connection_status)
|
||||
|
||||
def test_path_teardown(self):
|
||||
|
||||
@@ -542,7 +541,7 @@ class CablePathTestCase(TestCase):
|
||||
cable3.save()
|
||||
interface1 = Interface.objects.get(pk=self.interface1.pk)
|
||||
self.assertEqual(interface1.connected_endpoint, self.interface2)
|
||||
self.assertEqual(interface1.connection_status, CONNECTION_STATUS_CONNECTED)
|
||||
self.assertTrue(interface1.connection_status)
|
||||
|
||||
# Remove a cable
|
||||
cable2.delete()
|
||||
|
||||
@@ -11,7 +11,7 @@ from dcim.choices import *
|
||||
from dcim.constants import *
|
||||
from dcim.models import *
|
||||
from ipam.models import VLAN
|
||||
from utilities.testing import StandardTestCases
|
||||
from utilities.testing import ViewTestCases
|
||||
|
||||
|
||||
def create_test_device(name):
|
||||
@@ -27,14 +27,9 @@ def create_test_device(name):
|
||||
return device
|
||||
|
||||
|
||||
class RegionTestCase(StandardTestCases.Views):
|
||||
class RegionTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
||||
model = Region
|
||||
|
||||
# Disable inapplicable tests
|
||||
test_get_object = None
|
||||
test_delete_object = None
|
||||
test_bulk_edit_objects = None
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
@@ -61,7 +56,7 @@ class RegionTestCase(StandardTestCases.Views):
|
||||
)
|
||||
|
||||
|
||||
class SiteTestCase(StandardTestCases.Views):
|
||||
class SiteTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
model = Site
|
||||
|
||||
@classmethod
|
||||
@@ -118,14 +113,9 @@ class SiteTestCase(StandardTestCases.Views):
|
||||
}
|
||||
|
||||
|
||||
class RackGroupTestCase(StandardTestCases.Views):
|
||||
class RackGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
||||
model = RackGroup
|
||||
|
||||
# Disable inapplicable tests
|
||||
test_get_object = None
|
||||
test_delete_object = None
|
||||
test_bulk_edit_objects = None
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
@@ -152,14 +142,9 @@ class RackGroupTestCase(StandardTestCases.Views):
|
||||
)
|
||||
|
||||
|
||||
class RackRoleTestCase(StandardTestCases.Views):
|
||||
class RackRoleTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
||||
model = RackRole
|
||||
|
||||
# Disable inapplicable tests
|
||||
test_get_object = None
|
||||
test_delete_object = None
|
||||
test_bulk_edit_objects = None
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
@@ -184,7 +169,7 @@ class RackRoleTestCase(StandardTestCases.Views):
|
||||
)
|
||||
|
||||
|
||||
class RackReservationTestCase(StandardTestCases.Views):
|
||||
class RackReservationTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
model = RackReservation
|
||||
|
||||
# Disable inapplicable tests
|
||||
@@ -226,7 +211,7 @@ class RackReservationTestCase(StandardTestCases.Views):
|
||||
}
|
||||
|
||||
|
||||
class RackTestCase(StandardTestCases.Views):
|
||||
class RackTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
model = Rack
|
||||
|
||||
@classmethod
|
||||
@@ -302,14 +287,9 @@ class RackTestCase(StandardTestCases.Views):
|
||||
}
|
||||
|
||||
|
||||
class ManufacturerTestCase(StandardTestCases.Views):
|
||||
class ManufacturerTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
||||
model = Manufacturer
|
||||
|
||||
# Disable inapplicable tests
|
||||
test_get_object = None
|
||||
test_delete_object = None
|
||||
test_bulk_edit_objects = None
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
@@ -332,7 +312,7 @@ class ManufacturerTestCase(StandardTestCases.Views):
|
||||
)
|
||||
|
||||
|
||||
class DeviceTypeTestCase(StandardTestCases.Views):
|
||||
class DeviceTypeTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
model = DeviceType
|
||||
|
||||
@classmethod
|
||||
@@ -524,14 +504,318 @@ device-bays:
|
||||
self.assertEqual(data[0]['model'], 'Device Type 1')
|
||||
|
||||
|
||||
class DeviceRoleTestCase(StandardTestCases.Views):
|
||||
model = DeviceRole
|
||||
#
|
||||
# DeviceType components
|
||||
#
|
||||
|
||||
# Disable inapplicable tests
|
||||
test_get_object = None
|
||||
test_delete_object = None
|
||||
class ConsolePortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
|
||||
model = ConsolePortTemplate
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
||||
devicetypes = (
|
||||
DeviceType(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'),
|
||||
DeviceType(manufacturer=manufacturer, model='Device Type 2', slug='device-type-2'),
|
||||
)
|
||||
DeviceType.objects.bulk_create(devicetypes)
|
||||
|
||||
ConsolePortTemplate.objects.bulk_create((
|
||||
ConsolePortTemplate(device_type=devicetypes[0], name='Console Port Template 1'),
|
||||
ConsolePortTemplate(device_type=devicetypes[0], name='Console Port Template 2'),
|
||||
ConsolePortTemplate(device_type=devicetypes[0], name='Console Port Template 3'),
|
||||
))
|
||||
|
||||
cls.form_data = {
|
||||
'device_type': devicetypes[1].pk,
|
||||
'name': 'Console Port Template X',
|
||||
'type': ConsolePortTypeChoices.TYPE_RJ45,
|
||||
}
|
||||
|
||||
cls.bulk_create_data = {
|
||||
'device_type': devicetypes[1].pk,
|
||||
'name_pattern': 'Console Port Template [4-6]',
|
||||
'type': ConsolePortTypeChoices.TYPE_RJ45,
|
||||
}
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
'type': ConsolePortTypeChoices.TYPE_RJ45,
|
||||
}
|
||||
|
||||
|
||||
class ConsoleServerPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
|
||||
model = ConsoleServerPortTemplate
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
||||
devicetypes = (
|
||||
DeviceType(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'),
|
||||
DeviceType(manufacturer=manufacturer, model='Device Type 2', slug='device-type-2'),
|
||||
)
|
||||
DeviceType.objects.bulk_create(devicetypes)
|
||||
|
||||
ConsoleServerPortTemplate.objects.bulk_create((
|
||||
ConsoleServerPortTemplate(device_type=devicetypes[0], name='Console Server Port Template 1'),
|
||||
ConsoleServerPortTemplate(device_type=devicetypes[0], name='Console Server Port Template 2'),
|
||||
ConsoleServerPortTemplate(device_type=devicetypes[0], name='Console Server Port Template 3'),
|
||||
))
|
||||
|
||||
cls.form_data = {
|
||||
'device_type': devicetypes[1].pk,
|
||||
'name': 'Console Server Port Template X',
|
||||
'type': ConsolePortTypeChoices.TYPE_RJ45,
|
||||
}
|
||||
|
||||
cls.bulk_create_data = {
|
||||
'device_type': devicetypes[1].pk,
|
||||
'name_pattern': 'Console Server Port Template [4-6]',
|
||||
'type': ConsolePortTypeChoices.TYPE_RJ45,
|
||||
}
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
'type': ConsolePortTypeChoices.TYPE_RJ45,
|
||||
}
|
||||
|
||||
|
||||
class PowerPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
|
||||
model = PowerPortTemplate
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
||||
devicetypes = (
|
||||
DeviceType(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'),
|
||||
DeviceType(manufacturer=manufacturer, model='Device Type 2', slug='device-type-2'),
|
||||
)
|
||||
DeviceType.objects.bulk_create(devicetypes)
|
||||
|
||||
PowerPortTemplate.objects.bulk_create((
|
||||
PowerPortTemplate(device_type=devicetypes[0], name='Power Port Template 1'),
|
||||
PowerPortTemplate(device_type=devicetypes[0], name='Power Port Template 2'),
|
||||
PowerPortTemplate(device_type=devicetypes[0], name='Power Port Template 3'),
|
||||
))
|
||||
|
||||
cls.form_data = {
|
||||
'device_type': devicetypes[1].pk,
|
||||
'name': 'Power Port Template X',
|
||||
'type': PowerPortTypeChoices.TYPE_IEC_C14,
|
||||
'maximum_draw': 100,
|
||||
'allocated_draw': 50,
|
||||
}
|
||||
|
||||
cls.bulk_create_data = {
|
||||
'device_type': devicetypes[1].pk,
|
||||
'name_pattern': 'Power Port Template [4-6]',
|
||||
'type': PowerPortTypeChoices.TYPE_IEC_C14,
|
||||
'maximum_draw': 100,
|
||||
'allocated_draw': 50,
|
||||
}
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
'type': PowerPortTypeChoices.TYPE_IEC_C14,
|
||||
'maximum_draw': 100,
|
||||
'allocated_draw': 50,
|
||||
}
|
||||
|
||||
|
||||
class PowerOutletTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
|
||||
model = PowerOutletTemplate
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
||||
devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1')
|
||||
|
||||
PowerOutletTemplate.objects.bulk_create((
|
||||
PowerOutletTemplate(device_type=devicetype, name='Power Outlet Template 1'),
|
||||
PowerOutletTemplate(device_type=devicetype, name='Power Outlet Template 2'),
|
||||
PowerOutletTemplate(device_type=devicetype, name='Power Outlet Template 3'),
|
||||
))
|
||||
|
||||
powerports = (
|
||||
PowerPortTemplate(device_type=devicetype, name='Power Port Template 1'),
|
||||
)
|
||||
PowerPortTemplate.objects.bulk_create(powerports)
|
||||
|
||||
cls.form_data = {
|
||||
'device_type': devicetype.pk,
|
||||
'name': 'Power Outlet Template X',
|
||||
'type': PowerOutletTypeChoices.TYPE_IEC_C13,
|
||||
'power_port': powerports[0].pk,
|
||||
'feed_leg': PowerOutletFeedLegChoices.FEED_LEG_B,
|
||||
}
|
||||
|
||||
cls.bulk_create_data = {
|
||||
'device_type': devicetype.pk,
|
||||
'name_pattern': 'Power Outlet Template [4-6]',
|
||||
'type': PowerOutletTypeChoices.TYPE_IEC_C13,
|
||||
'power_port': powerports[0].pk,
|
||||
'feed_leg': PowerOutletFeedLegChoices.FEED_LEG_B,
|
||||
}
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
'type': PowerOutletTypeChoices.TYPE_IEC_C13,
|
||||
'feed_leg': PowerOutletFeedLegChoices.FEED_LEG_B,
|
||||
}
|
||||
|
||||
|
||||
class InterfaceTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
|
||||
model = InterfaceTemplate
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
||||
devicetypes = (
|
||||
DeviceType(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'),
|
||||
DeviceType(manufacturer=manufacturer, model='Device Type 2', slug='device-type-2'),
|
||||
)
|
||||
DeviceType.objects.bulk_create(devicetypes)
|
||||
|
||||
InterfaceTemplate.objects.bulk_create((
|
||||
InterfaceTemplate(device_type=devicetypes[0], name='Interface Template 1'),
|
||||
InterfaceTemplate(device_type=devicetypes[0], name='Interface Template 2'),
|
||||
InterfaceTemplate(device_type=devicetypes[0], name='Interface Template 3'),
|
||||
))
|
||||
|
||||
cls.form_data = {
|
||||
'device_type': devicetypes[1].pk,
|
||||
'name': 'Interface Template X',
|
||||
'type': InterfaceTypeChoices.TYPE_1GE_GBIC,
|
||||
'mgmt_only': True,
|
||||
}
|
||||
|
||||
cls.bulk_create_data = {
|
||||
'device_type': devicetypes[1].pk,
|
||||
'name_pattern': 'Interface Template [4-6]',
|
||||
'type': InterfaceTypeChoices.TYPE_1GE_GBIC,
|
||||
'mgmt_only': True,
|
||||
}
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
'type': InterfaceTypeChoices.TYPE_1GE_GBIC,
|
||||
'mgmt_only': True,
|
||||
}
|
||||
|
||||
|
||||
class FrontPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
|
||||
model = FrontPortTemplate
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
||||
devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1')
|
||||
|
||||
rearports = (
|
||||
RearPortTemplate(device_type=devicetype, name='Rear Port Template 1'),
|
||||
RearPortTemplate(device_type=devicetype, name='Rear Port Template 2'),
|
||||
RearPortTemplate(device_type=devicetype, name='Rear Port Template 3'),
|
||||
RearPortTemplate(device_type=devicetype, name='Rear Port Template 4'),
|
||||
RearPortTemplate(device_type=devicetype, name='Rear Port Template 5'),
|
||||
RearPortTemplate(device_type=devicetype, name='Rear Port Template 6'),
|
||||
)
|
||||
RearPortTemplate.objects.bulk_create(rearports)
|
||||
|
||||
FrontPortTemplate.objects.bulk_create((
|
||||
FrontPortTemplate(device_type=devicetype, name='Front Port Template 1', rear_port=rearports[0], rear_port_position=1),
|
||||
FrontPortTemplate(device_type=devicetype, name='Front Port Template 2', rear_port=rearports[1], rear_port_position=1),
|
||||
FrontPortTemplate(device_type=devicetype, name='Front Port Template 3', rear_port=rearports[2], rear_port_position=1),
|
||||
))
|
||||
|
||||
cls.form_data = {
|
||||
'device_type': devicetype.pk,
|
||||
'name': 'Front Port X',
|
||||
'type': PortTypeChoices.TYPE_8P8C,
|
||||
'rear_port': rearports[3].pk,
|
||||
'rear_port_position': 1,
|
||||
}
|
||||
|
||||
cls.bulk_create_data = {
|
||||
'device_type': devicetype.pk,
|
||||
'name_pattern': 'Front Port [4-6]',
|
||||
'type': PortTypeChoices.TYPE_8P8C,
|
||||
'rear_port_set': [
|
||||
'{}:1'.format(rp.pk) for rp in rearports[3:6]
|
||||
],
|
||||
}
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
'type': PortTypeChoices.TYPE_8P8C,
|
||||
}
|
||||
|
||||
|
||||
class RearPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
|
||||
model = RearPortTemplate
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
||||
devicetypes = (
|
||||
DeviceType(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'),
|
||||
DeviceType(manufacturer=manufacturer, model='Device Type 2', slug='device-type-2'),
|
||||
)
|
||||
DeviceType.objects.bulk_create(devicetypes)
|
||||
|
||||
RearPortTemplate.objects.bulk_create((
|
||||
RearPortTemplate(device_type=devicetypes[0], name='Rear Port Template 1'),
|
||||
RearPortTemplate(device_type=devicetypes[0], name='Rear Port Template 2'),
|
||||
RearPortTemplate(device_type=devicetypes[0], name='Rear Port Template 3'),
|
||||
))
|
||||
|
||||
cls.form_data = {
|
||||
'device_type': devicetypes[1].pk,
|
||||
'name': 'Rear Port Template X',
|
||||
'type': PortTypeChoices.TYPE_8P8C,
|
||||
'positions': 2,
|
||||
}
|
||||
|
||||
cls.bulk_create_data = {
|
||||
'device_type': devicetypes[1].pk,
|
||||
'name_pattern': 'Rear Port Template [4-6]',
|
||||
'type': PortTypeChoices.TYPE_8P8C,
|
||||
'positions': 2,
|
||||
}
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
'type': PortTypeChoices.TYPE_8P8C,
|
||||
}
|
||||
|
||||
|
||||
class DeviceBayTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
|
||||
model = DeviceBayTemplate
|
||||
|
||||
# Disable inapplicable views
|
||||
test_bulk_edit_objects = None
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
||||
devicetypes = (
|
||||
DeviceType(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'),
|
||||
DeviceType(manufacturer=manufacturer, model='Device Type 2', slug='device-type-2'),
|
||||
)
|
||||
DeviceType.objects.bulk_create(devicetypes)
|
||||
|
||||
DeviceBayTemplate.objects.bulk_create((
|
||||
DeviceBayTemplate(device_type=devicetypes[0], name='Device Bay Template 1'),
|
||||
DeviceBayTemplate(device_type=devicetypes[0], name='Device Bay Template 2'),
|
||||
DeviceBayTemplate(device_type=devicetypes[0], name='Device Bay Template 3'),
|
||||
))
|
||||
|
||||
cls.form_data = {
|
||||
'device_type': devicetypes[1].pk,
|
||||
'name': 'Device Bay Template X',
|
||||
}
|
||||
|
||||
cls.bulk_create_data = {
|
||||
'device_type': devicetypes[1].pk,
|
||||
'name_pattern': 'Device Bay Template [4-6]',
|
||||
}
|
||||
|
||||
|
||||
class DeviceRoleTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
||||
model = DeviceRole
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
@@ -557,14 +841,9 @@ class DeviceRoleTestCase(StandardTestCases.Views):
|
||||
)
|
||||
|
||||
|
||||
class PlatformTestCase(StandardTestCases.Views):
|
||||
class PlatformTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
||||
model = Platform
|
||||
|
||||
# Disable inapplicable tests
|
||||
test_get_object = None
|
||||
test_delete_object = None
|
||||
test_bulk_edit_objects = None
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
@@ -592,7 +871,7 @@ class PlatformTestCase(StandardTestCases.Views):
|
||||
)
|
||||
|
||||
|
||||
class DeviceTestCase(StandardTestCases.Views):
|
||||
class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
model = Device
|
||||
|
||||
@classmethod
|
||||
@@ -677,17 +956,9 @@ class DeviceTestCase(StandardTestCases.Views):
|
||||
}
|
||||
|
||||
|
||||
class ConsolePortTestCase(StandardTestCases.Views):
|
||||
class ConsolePortTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
model = ConsolePort
|
||||
|
||||
# Disable inapplicable views
|
||||
test_get_object = None
|
||||
test_bulk_edit_objects = None
|
||||
|
||||
# TODO
|
||||
test_create_object = None
|
||||
test_bulk_delete_objects = None
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
device = create_test_device('Device 1')
|
||||
@@ -704,11 +975,19 @@ class ConsolePortTestCase(StandardTestCases.Views):
|
||||
'type': ConsolePortTypeChoices.TYPE_RJ45,
|
||||
'description': 'A console port',
|
||||
'tags': 'Alpha,Bravo,Charlie',
|
||||
}
|
||||
|
||||
# Extraneous model fields
|
||||
'cable': None,
|
||||
'connected_endpoint': None,
|
||||
'connection_status': None,
|
||||
cls.bulk_create_data = {
|
||||
'device': device.pk,
|
||||
'name_pattern': 'Console Port [4-6]',
|
||||
'type': ConsolePortTypeChoices.TYPE_RJ45,
|
||||
'description': 'A console port',
|
||||
'tags': 'Alpha,Bravo,Charlie',
|
||||
}
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
'type': ConsolePortTypeChoices.TYPE_RJ45,
|
||||
'description': 'New description',
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
@@ -719,17 +998,9 @@ class ConsolePortTestCase(StandardTestCases.Views):
|
||||
)
|
||||
|
||||
|
||||
class ConsoleServerPortTestCase(StandardTestCases.Views):
|
||||
class ConsoleServerPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
model = ConsoleServerPort
|
||||
|
||||
# Disable inapplicable views
|
||||
test_get_object = None
|
||||
|
||||
# TODO
|
||||
test_create_object = None
|
||||
test_bulk_edit_objects = None
|
||||
test_bulk_delete_objects = None
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
device = create_test_device('Device 1')
|
||||
@@ -746,10 +1017,20 @@ class ConsoleServerPortTestCase(StandardTestCases.Views):
|
||||
'type': ConsolePortTypeChoices.TYPE_RJ45,
|
||||
'description': 'A console server port',
|
||||
'tags': 'Alpha,Bravo,Charlie',
|
||||
}
|
||||
|
||||
# Extraneous model fields
|
||||
'cable': None,
|
||||
'connection_status': None,
|
||||
cls.bulk_create_data = {
|
||||
'device': device.pk,
|
||||
'name_pattern': 'Console Server Port [4-6]',
|
||||
'type': ConsolePortTypeChoices.TYPE_RJ45,
|
||||
'description': 'A console server port',
|
||||
'tags': 'Alpha,Bravo,Charlie',
|
||||
}
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
'device': device.pk,
|
||||
'type': ConsolePortTypeChoices.TYPE_RJ45,
|
||||
'description': 'New description',
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
@@ -760,17 +1041,9 @@ class ConsoleServerPortTestCase(StandardTestCases.Views):
|
||||
)
|
||||
|
||||
|
||||
class PowerPortTestCase(StandardTestCases.Views):
|
||||
class PowerPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
model = PowerPort
|
||||
|
||||
# Disable inapplicable views
|
||||
test_get_object = None
|
||||
test_bulk_edit_objects = None
|
||||
|
||||
# TODO
|
||||
test_create_object = None
|
||||
test_bulk_delete_objects = None
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
device = create_test_device('Device 1')
|
||||
@@ -789,10 +1062,23 @@ class PowerPortTestCase(StandardTestCases.Views):
|
||||
'allocated_draw': 50,
|
||||
'description': 'A power port',
|
||||
'tags': 'Alpha,Bravo,Charlie',
|
||||
}
|
||||
|
||||
# Extraneous model fields
|
||||
'cable': None,
|
||||
'connection_status': None,
|
||||
cls.bulk_create_data = {
|
||||
'device': device.pk,
|
||||
'name_pattern': 'Power Port [4-6]]',
|
||||
'type': PowerPortTypeChoices.TYPE_IEC_C14,
|
||||
'maximum_draw': 100,
|
||||
'allocated_draw': 50,
|
||||
'description': 'A power port',
|
||||
'tags': 'Alpha,Bravo,Charlie',
|
||||
}
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
'type': PowerPortTypeChoices.TYPE_IEC_C14,
|
||||
'maximum_draw': 100,
|
||||
'allocated_draw': 50,
|
||||
'description': 'New description',
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
@@ -803,17 +1089,9 @@ class PowerPortTestCase(StandardTestCases.Views):
|
||||
)
|
||||
|
||||
|
||||
class PowerOutletTestCase(StandardTestCases.Views):
|
||||
class PowerOutletTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
model = PowerOutlet
|
||||
|
||||
# Disable inapplicable views
|
||||
test_get_object = None
|
||||
|
||||
# TODO
|
||||
test_create_object = None
|
||||
test_bulk_edit_objects = None
|
||||
test_bulk_delete_objects = None
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
device = create_test_device('Device 1')
|
||||
@@ -838,10 +1116,24 @@ class PowerOutletTestCase(StandardTestCases.Views):
|
||||
'feed_leg': PowerOutletFeedLegChoices.FEED_LEG_B,
|
||||
'description': 'A power outlet',
|
||||
'tags': 'Alpha,Bravo,Charlie',
|
||||
}
|
||||
|
||||
# Extraneous model fields
|
||||
'cable': None,
|
||||
'connection_status': None,
|
||||
cls.bulk_create_data = {
|
||||
'device': device.pk,
|
||||
'name_pattern': 'Power Outlet [4-6]',
|
||||
'type': PowerOutletTypeChoices.TYPE_IEC_C13,
|
||||
'power_port': powerports[1].pk,
|
||||
'feed_leg': PowerOutletFeedLegChoices.FEED_LEG_B,
|
||||
'description': 'A power outlet',
|
||||
'tags': 'Alpha,Bravo,Charlie',
|
||||
}
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
'device': device.pk,
|
||||
'type': PowerOutletTypeChoices.TYPE_IEC_C13,
|
||||
'power_port': powerports[1].pk,
|
||||
'feed_leg': PowerOutletFeedLegChoices.FEED_LEG_B,
|
||||
'description': 'New description',
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
@@ -852,23 +1144,23 @@ class PowerOutletTestCase(StandardTestCases.Views):
|
||||
)
|
||||
|
||||
|
||||
class InterfaceTestCase(StandardTestCases.Views):
|
||||
class InterfaceTestCase(
|
||||
ViewTestCases.GetObjectViewTestCase,
|
||||
ViewTestCases.DeviceComponentViewTestCase,
|
||||
):
|
||||
model = Interface
|
||||
|
||||
# TODO
|
||||
test_create_object = None
|
||||
test_bulk_edit_objects = None
|
||||
test_bulk_delete_objects = None
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
device = create_test_device('Device 1')
|
||||
|
||||
Interface.objects.bulk_create([
|
||||
interfaces = (
|
||||
Interface(device=device, name='Interface 1'),
|
||||
Interface(device=device, name='Interface 2'),
|
||||
Interface(device=device, name='Interface 3'),
|
||||
])
|
||||
Interface(device=device, name='LAG', type=InterfaceTypeChoices.TYPE_LAG),
|
||||
)
|
||||
Interface.objects.bulk_create(interfaces)
|
||||
|
||||
vlans = (
|
||||
VLAN(vid=1, name='VLAN1', site=device.site),
|
||||
@@ -884,7 +1176,38 @@ class InterfaceTestCase(StandardTestCases.Views):
|
||||
'name': 'Interface X',
|
||||
'type': InterfaceTypeChoices.TYPE_1GE_GBIC,
|
||||
'enabled': False,
|
||||
'lag': None,
|
||||
'lag': interfaces[3].pk,
|
||||
'mac_address': EUI('01:02:03:04:05:06'),
|
||||
'mtu': 2000,
|
||||
'mgmt_only': True,
|
||||
'description': 'A front port',
|
||||
'mode': InterfaceModeChoices.MODE_TAGGED,
|
||||
'untagged_vlan': vlans[0].pk,
|
||||
'tagged_vlans': [v.pk for v in vlans[1:4]],
|
||||
'tags': 'Alpha,Bravo,Charlie',
|
||||
}
|
||||
|
||||
cls.bulk_create_data = {
|
||||
'device': device.pk,
|
||||
'name_pattern': 'Interface [4-6]',
|
||||
'type': InterfaceTypeChoices.TYPE_1GE_GBIC,
|
||||
'enabled': False,
|
||||
'lag': interfaces[3].pk,
|
||||
'mac_address': EUI('01:02:03:04:05:06'),
|
||||
'mtu': 2000,
|
||||
'mgmt_only': True,
|
||||
'description': 'A front port',
|
||||
'mode': InterfaceModeChoices.MODE_TAGGED,
|
||||
'untagged_vlan': vlans[0].pk,
|
||||
'tagged_vlans': [v.pk for v in vlans[1:4]],
|
||||
'tags': 'Alpha,Bravo,Charlie',
|
||||
}
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
'device': device.pk,
|
||||
'type': InterfaceTypeChoices.TYPE_1GE_GBIC,
|
||||
'enabled': False,
|
||||
'lag': interfaces[3].pk,
|
||||
'mac_address': EUI('01:02:03:04:05:06'),
|
||||
'mtu': 2000,
|
||||
'mgmt_only': True,
|
||||
@@ -892,11 +1215,6 @@ class InterfaceTestCase(StandardTestCases.Views):
|
||||
'mode': InterfaceModeChoices.MODE_TAGGED,
|
||||
'untagged_vlan': vlans[0].pk,
|
||||
'tagged_vlans': [v.pk for v in vlans[1:4]],
|
||||
'tags': 'Alpha,Bravo,Charlie',
|
||||
|
||||
# Extraneous model fields
|
||||
'cable': None,
|
||||
'connection_status': None,
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
@@ -907,17 +1225,9 @@ class InterfaceTestCase(StandardTestCases.Views):
|
||||
)
|
||||
|
||||
|
||||
class FrontPortTestCase(StandardTestCases.Views):
|
||||
class FrontPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
model = FrontPort
|
||||
|
||||
# Disable inapplicable views
|
||||
test_get_object = None
|
||||
|
||||
# TODO
|
||||
test_create_object = None
|
||||
test_bulk_edit_objects = None
|
||||
test_bulk_delete_objects = None
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
device = create_test_device('Device 1')
|
||||
@@ -946,9 +1256,22 @@ class FrontPortTestCase(StandardTestCases.Views):
|
||||
'rear_port_position': 1,
|
||||
'description': 'New description',
|
||||
'tags': 'Alpha,Bravo,Charlie',
|
||||
}
|
||||
|
||||
# Extraneous model fields
|
||||
'cable': None,
|
||||
cls.bulk_create_data = {
|
||||
'device': device.pk,
|
||||
'name_pattern': 'Front Port [4-6]',
|
||||
'type': PortTypeChoices.TYPE_8P8C,
|
||||
'rear_port_set': [
|
||||
'{}:1'.format(rp.pk) for rp in rearports[3:6]
|
||||
],
|
||||
'description': 'New description',
|
||||
'tags': 'Alpha,Bravo,Charlie',
|
||||
}
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
'type': PortTypeChoices.TYPE_8P8C,
|
||||
'description': 'New description',
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
@@ -959,17 +1282,9 @@ class FrontPortTestCase(StandardTestCases.Views):
|
||||
)
|
||||
|
||||
|
||||
class RearPortTestCase(StandardTestCases.Views):
|
||||
class RearPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
model = RearPort
|
||||
|
||||
# Disable inapplicable views
|
||||
test_get_object = None
|
||||
|
||||
# TODO
|
||||
test_create_object = None
|
||||
test_bulk_edit_objects = None
|
||||
test_bulk_delete_objects = None
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
device = create_test_device('Device 1')
|
||||
@@ -985,11 +1300,22 @@ class RearPortTestCase(StandardTestCases.Views):
|
||||
'name': 'Rear Port X',
|
||||
'type': PortTypeChoices.TYPE_8P8C,
|
||||
'positions': 3,
|
||||
'description': 'New description',
|
||||
'description': 'A rear port',
|
||||
'tags': 'Alpha,Bravo,Charlie',
|
||||
}
|
||||
|
||||
# Extraneous model fields
|
||||
'cable': None,
|
||||
cls.bulk_create_data = {
|
||||
'device': device.pk,
|
||||
'name_pattern': 'Rear Port [4-6]',
|
||||
'type': PortTypeChoices.TYPE_8P8C,
|
||||
'positions': 3,
|
||||
'description': 'A rear port',
|
||||
'tags': 'Alpha,Bravo,Charlie',
|
||||
}
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
'type': PortTypeChoices.TYPE_8P8C,
|
||||
'description': 'New description',
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
@@ -1000,16 +1326,11 @@ class RearPortTestCase(StandardTestCases.Views):
|
||||
)
|
||||
|
||||
|
||||
class DeviceBayTestCase(StandardTestCases.Views):
|
||||
class DeviceBayTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
model = DeviceBay
|
||||
|
||||
# Disable inapplicable views
|
||||
test_get_object = None
|
||||
|
||||
# TODO
|
||||
test_create_object = None
|
||||
test_bulk_edit_objects = None
|
||||
test_bulk_delete_objects = None
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@@ -1030,9 +1351,13 @@ class DeviceBayTestCase(StandardTestCases.Views):
|
||||
'name': 'Device Bay X',
|
||||
'description': 'A device bay',
|
||||
'tags': 'Alpha,Bravo,Charlie',
|
||||
}
|
||||
|
||||
# Extraneous model fields
|
||||
'installed_device': None,
|
||||
cls.bulk_create_data = {
|
||||
'device': device2.pk,
|
||||
'name_pattern': 'Device Bay [4-6]',
|
||||
'description': 'A device bay',
|
||||
'tags': 'Alpha,Bravo,Charlie',
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
@@ -1043,15 +1368,9 @@ class DeviceBayTestCase(StandardTestCases.Views):
|
||||
)
|
||||
|
||||
|
||||
class InventoryItemTestCase(StandardTestCases.Views):
|
||||
class InventoryItemTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
||||
model = InventoryItem
|
||||
|
||||
# Disable inapplicable views
|
||||
test_get_object = None
|
||||
|
||||
# TODO
|
||||
test_create_object = None
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
device = create_test_device('Device 1')
|
||||
@@ -1076,12 +1395,17 @@ class InventoryItemTestCase(StandardTestCases.Views):
|
||||
'tags': 'Alpha,Bravo,Charlie',
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
"device,name",
|
||||
"Device 1,Inventory Item 4",
|
||||
"Device 1,Inventory Item 5",
|
||||
"Device 1,Inventory Item 6",
|
||||
)
|
||||
cls.bulk_create_data = {
|
||||
'device': device.pk,
|
||||
'name_pattern': 'Inventory Item [4-6]',
|
||||
'manufacturer': manufacturer.pk,
|
||||
'parent': None,
|
||||
'discovered': False,
|
||||
'part_id': '123456',
|
||||
'serial': '123ABC',
|
||||
'description': 'An inventory item',
|
||||
'tags': 'Alpha,Bravo,Charlie',
|
||||
}
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
'device': device.pk,
|
||||
@@ -1090,8 +1414,15 @@ class InventoryItemTestCase(StandardTestCases.Views):
|
||||
'description': 'New description',
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
"device,name",
|
||||
"Device 1,Inventory Item 4",
|
||||
"Device 1,Inventory Item 5",
|
||||
"Device 1,Inventory Item 6",
|
||||
)
|
||||
|
||||
class CableTestCase(StandardTestCases.Views):
|
||||
|
||||
class CableTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
model = Cable
|
||||
|
||||
# TODO: Creation URL needs termination context
|
||||
@@ -1165,7 +1496,7 @@ class CableTestCase(StandardTestCases.Views):
|
||||
}
|
||||
|
||||
|
||||
class VirtualChassisTestCase(StandardTestCases.Views):
|
||||
class VirtualChassisTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
model = VirtualChassis
|
||||
|
||||
# Disable inapplicable tests
|
||||
@@ -1219,7 +1550,7 @@ class VirtualChassisTestCase(StandardTestCases.Views):
|
||||
Device.objects.filter(pk=device6.pk).update(virtual_chassis=vc3, vc_position=2)
|
||||
|
||||
|
||||
class PowerPanelTestCase(StandardTestCases.Views):
|
||||
class PowerPanelTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
model = PowerPanel
|
||||
|
||||
# Disable inapplicable tests
|
||||
@@ -1260,7 +1591,7 @@ class PowerPanelTestCase(StandardTestCases.Views):
|
||||
)
|
||||
|
||||
|
||||
class PowerFeedTestCase(StandardTestCases.Views):
|
||||
class PowerFeedTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
model = PowerFeed
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -14,317 +14,338 @@ app_name = 'dcim'
|
||||
urlpatterns = [
|
||||
|
||||
# Regions
|
||||
path(r'regions/', views.RegionListView.as_view(), name='region_list'),
|
||||
path(r'regions/add/', views.RegionCreateView.as_view(), name='region_add'),
|
||||
path(r'regions/import/', views.RegionBulkImportView.as_view(), name='region_import'),
|
||||
path(r'regions/delete/', views.RegionBulkDeleteView.as_view(), name='region_bulk_delete'),
|
||||
path(r'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/', views.RegionListView.as_view(), name='region_list'),
|
||||
path('regions/add/', views.RegionCreateView.as_view(), name='region_add'),
|
||||
path('regions/import/', views.RegionBulkImportView.as_view(), name='region_import'),
|
||||
path('regions/delete/', views.RegionBulkDeleteView.as_view(), name='region_bulk_delete'),
|
||||
path('regions/<int:pk>/edit/', views.RegionEditView.as_view(), name='region_edit'),
|
||||
path('regions/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='region_changelog', kwargs={'model': Region}),
|
||||
|
||||
# Sites
|
||||
path(r'sites/', views.SiteListView.as_view(), name='site_list'),
|
||||
path(r'sites/add/', views.SiteCreateView.as_view(), name='site_add'),
|
||||
path(r'sites/import/', views.SiteBulkImportView.as_view(), name='site_import'),
|
||||
path(r'sites/edit/', views.SiteBulkEditView.as_view(), name='site_bulk_edit'),
|
||||
path(r'sites/delete/', views.SiteBulkDeleteView.as_view(), name='site_bulk_delete'),
|
||||
path(r'sites/<slug:slug>/', views.SiteView.as_view(), name='site'),
|
||||
path(r'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(r'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/', views.SiteListView.as_view(), name='site_list'),
|
||||
path('sites/add/', views.SiteCreateView.as_view(), name='site_add'),
|
||||
path('sites/import/', views.SiteBulkImportView.as_view(), name='site_import'),
|
||||
path('sites/edit/', views.SiteBulkEditView.as_view(), name='site_bulk_edit'),
|
||||
path('sites/delete/', views.SiteBulkDeleteView.as_view(), name='site_bulk_delete'),
|
||||
path('sites/<slug:slug>/', views.SiteView.as_view(), name='site'),
|
||||
path('sites/<slug:slug>/edit/', views.SiteEditView.as_view(), name='site_edit'),
|
||||
path('sites/<slug:slug>/delete/', views.SiteDeleteView.as_view(), name='site_delete'),
|
||||
path('sites/<slug:slug>/changelog/', ObjectChangeLogView.as_view(), name='site_changelog', kwargs={'model': Site}),
|
||||
path('sites/<int:object_id>/images/add/', ImageAttachmentEditView.as_view(), name='site_add_image', kwargs={'model': Site}),
|
||||
|
||||
# Rack groups
|
||||
path(r'rack-groups/', views.RackGroupListView.as_view(), name='rackgroup_list'),
|
||||
path(r'rack-groups/add/', views.RackGroupCreateView.as_view(), name='rackgroup_add'),
|
||||
path(r'rack-groups/import/', views.RackGroupBulkImportView.as_view(), name='rackgroup_import'),
|
||||
path(r'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(r'rack-groups/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='rackgroup_changelog', kwargs={'model': RackGroup}),
|
||||
path('rack-groups/', views.RackGroupListView.as_view(), name='rackgroup_list'),
|
||||
path('rack-groups/add/', views.RackGroupCreateView.as_view(), name='rackgroup_add'),
|
||||
path('rack-groups/import/', views.RackGroupBulkImportView.as_view(), name='rackgroup_import'),
|
||||
path('rack-groups/delete/', views.RackGroupBulkDeleteView.as_view(), name='rackgroup_bulk_delete'),
|
||||
path('rack-groups/<int:pk>/edit/', views.RackGroupEditView.as_view(), name='rackgroup_edit'),
|
||||
path('rack-groups/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='rackgroup_changelog', kwargs={'model': RackGroup}),
|
||||
|
||||
# Rack roles
|
||||
path(r'rack-roles/', views.RackRoleListView.as_view(), name='rackrole_list'),
|
||||
path(r'rack-roles/add/', views.RackRoleCreateView.as_view(), name='rackrole_add'),
|
||||
path(r'rack-roles/import/', views.RackRoleBulkImportView.as_view(), name='rackrole_import'),
|
||||
path(r'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(r'rack-roles/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='rackrole_changelog', kwargs={'model': RackRole}),
|
||||
path('rack-roles/', views.RackRoleListView.as_view(), name='rackrole_list'),
|
||||
path('rack-roles/add/', views.RackRoleCreateView.as_view(), name='rackrole_add'),
|
||||
path('rack-roles/import/', views.RackRoleBulkImportView.as_view(), name='rackrole_import'),
|
||||
path('rack-roles/delete/', views.RackRoleBulkDeleteView.as_view(), name='rackrole_bulk_delete'),
|
||||
path('rack-roles/<int:pk>/edit/', views.RackRoleEditView.as_view(), name='rackrole_edit'),
|
||||
path('rack-roles/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='rackrole_changelog', kwargs={'model': RackRole}),
|
||||
|
||||
# Rack reservations
|
||||
path(r'rack-reservations/', views.RackReservationListView.as_view(), name='rackreservation_list'),
|
||||
path(r'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(r'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(r'rack-reservations/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='rackreservation_changelog', kwargs={'model': RackReservation}),
|
||||
path('rack-reservations/', views.RackReservationListView.as_view(), name='rackreservation_list'),
|
||||
path('rack-reservations/edit/', views.RackReservationBulkEditView.as_view(), name='rackreservation_bulk_edit'),
|
||||
path('rack-reservations/delete/', views.RackReservationBulkDeleteView.as_view(), name='rackreservation_bulk_delete'),
|
||||
path('rack-reservations/<int:pk>/edit/', views.RackReservationEditView.as_view(), name='rackreservation_edit'),
|
||||
path('rack-reservations/<int:pk>/delete/', views.RackReservationDeleteView.as_view(), name='rackreservation_delete'),
|
||||
path('rack-reservations/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='rackreservation_changelog', kwargs={'model': RackReservation}),
|
||||
|
||||
# Racks
|
||||
path(r'racks/', views.RackListView.as_view(), name='rack_list'),
|
||||
path(r'rack-elevations/', views.RackElevationListView.as_view(), name='rack_elevation_list'),
|
||||
path(r'racks/add/', views.RackCreateView.as_view(), name='rack_add'),
|
||||
path(r'racks/import/', views.RackBulkImportView.as_view(), name='rack_import'),
|
||||
path(r'racks/edit/', views.RackBulkEditView.as_view(), name='rack_bulk_edit'),
|
||||
path(r'racks/delete/', views.RackBulkDeleteView.as_view(), name='rack_bulk_delete'),
|
||||
path(r'racks/<int:pk>/', views.RackView.as_view(), name='rack'),
|
||||
path(r'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(r'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(r'racks/<int:object_id>/images/add/', ImageAttachmentEditView.as_view(), name='rack_add_image', kwargs={'model': Rack}),
|
||||
path('racks/', views.RackListView.as_view(), name='rack_list'),
|
||||
path('rack-elevations/', views.RackElevationListView.as_view(), name='rack_elevation_list'),
|
||||
path('racks/add/', views.RackCreateView.as_view(), name='rack_add'),
|
||||
path('racks/import/', views.RackBulkImportView.as_view(), name='rack_import'),
|
||||
path('racks/edit/', views.RackBulkEditView.as_view(), name='rack_bulk_edit'),
|
||||
path('racks/delete/', views.RackBulkDeleteView.as_view(), name='rack_bulk_delete'),
|
||||
path('racks/<int:pk>/', views.RackView.as_view(), name='rack'),
|
||||
path('racks/<int:pk>/edit/', views.RackEditView.as_view(), name='rack_edit'),
|
||||
path('racks/<int:pk>/delete/', views.RackDeleteView.as_view(), name='rack_delete'),
|
||||
path('racks/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='rack_changelog', kwargs={'model': Rack}),
|
||||
path('racks/<int:rack>/reservations/add/', views.RackReservationCreateView.as_view(), name='rack_add_reservation'),
|
||||
path('racks/<int:object_id>/images/add/', ImageAttachmentEditView.as_view(), name='rack_add_image', kwargs={'model': Rack}),
|
||||
|
||||
# Manufacturers
|
||||
path(r'manufacturers/', views.ManufacturerListView.as_view(), name='manufacturer_list'),
|
||||
path(r'manufacturers/add/', views.ManufacturerCreateView.as_view(), name='manufacturer_add'),
|
||||
path(r'manufacturers/import/', views.ManufacturerBulkImportView.as_view(), name='manufacturer_import'),
|
||||
path(r'manufacturers/delete/', views.ManufacturerBulkDeleteView.as_view(), name='manufacturer_bulk_delete'),
|
||||
path(r'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/', views.ManufacturerListView.as_view(), name='manufacturer_list'),
|
||||
path('manufacturers/add/', views.ManufacturerCreateView.as_view(), name='manufacturer_add'),
|
||||
path('manufacturers/import/', views.ManufacturerBulkImportView.as_view(), name='manufacturer_import'),
|
||||
path('manufacturers/delete/', views.ManufacturerBulkDeleteView.as_view(), name='manufacturer_bulk_delete'),
|
||||
path('manufacturers/<slug:slug>/edit/', views.ManufacturerEditView.as_view(), name='manufacturer_edit'),
|
||||
path('manufacturers/<slug:slug>/changelog/', ObjectChangeLogView.as_view(), name='manufacturer_changelog', kwargs={'model': Manufacturer}),
|
||||
|
||||
# Device types
|
||||
path(r'device-types/', views.DeviceTypeListView.as_view(), name='devicetype_list'),
|
||||
path(r'device-types/add/', views.DeviceTypeCreateView.as_view(), name='devicetype_add'),
|
||||
path(r'device-types/import/', views.DeviceTypeImportView.as_view(), name='devicetype_import'),
|
||||
path(r'device-types/edit/', views.DeviceTypeBulkEditView.as_view(), name='devicetype_bulk_edit'),
|
||||
path(r'device-types/delete/', views.DeviceTypeBulkDeleteView.as_view(), name='devicetype_bulk_delete'),
|
||||
path(r'device-types/<int:pk>/', views.DeviceTypeView.as_view(), name='devicetype'),
|
||||
path(r'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(r'device-types/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='devicetype_changelog', kwargs={'model': DeviceType}),
|
||||
path('device-types/', views.DeviceTypeListView.as_view(), name='devicetype_list'),
|
||||
path('device-types/add/', views.DeviceTypeCreateView.as_view(), name='devicetype_add'),
|
||||
path('device-types/import/', views.DeviceTypeImportView.as_view(), name='devicetype_import'),
|
||||
path('device-types/edit/', views.DeviceTypeBulkEditView.as_view(), name='devicetype_bulk_edit'),
|
||||
path('device-types/delete/', views.DeviceTypeBulkDeleteView.as_view(), name='devicetype_bulk_delete'),
|
||||
path('device-types/<int:pk>/', views.DeviceTypeView.as_view(), name='devicetype'),
|
||||
path('device-types/<int:pk>/edit/', views.DeviceTypeEditView.as_view(), name='devicetype_edit'),
|
||||
path('device-types/<int:pk>/delete/', views.DeviceTypeDeleteView.as_view(), name='devicetype_delete'),
|
||||
path('device-types/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='devicetype_changelog', kwargs={'model': DeviceType}),
|
||||
|
||||
# Console port templates
|
||||
path(r'device-types/<int:pk>/console-ports/add/', views.ConsolePortTemplateCreateView.as_view(), name='devicetype_add_consoleport'),
|
||||
path(r'device-types/<int:pk>/console-ports/delete/', views.ConsolePortTemplateBulkDeleteView.as_view(), name='devicetype_delete_consoleport'),
|
||||
path(r'console-port-templates/<int:pk>/edit/', views.ConsolePortTemplateEditView.as_view(), name='consoleporttemplate_edit'),
|
||||
path('console-port-templates/add/', views.ConsolePortTemplateCreateView.as_view(), name='consoleporttemplate_add'),
|
||||
path('console-port-templates/edit/', views.ConsolePortTemplateBulkEditView.as_view(), name='consoleporttemplate_bulk_edit'),
|
||||
path('console-port-templates/delete/', views.ConsolePortTemplateBulkDeleteView.as_view(), name='consoleporttemplate_bulk_delete'),
|
||||
path('console-port-templates/<int:pk>/edit/', views.ConsolePortTemplateEditView.as_view(), name='consoleporttemplate_edit'),
|
||||
path('console-port-templates/<int:pk>/delete/', views.ConsolePortTemplateDeleteView.as_view(), name='consoleporttemplate_delete'),
|
||||
|
||||
# Console server port templates
|
||||
path(r'device-types/<int:pk>/console-server-ports/add/', views.ConsoleServerPortTemplateCreateView.as_view(), name='devicetype_add_consoleserverport'),
|
||||
path(r'device-types/<int:pk>/console-server-ports/delete/', views.ConsoleServerPortTemplateBulkDeleteView.as_view(), name='devicetype_delete_consoleserverport'),
|
||||
path(r'console-server-port-templates/<int:pk>/edit/', views.ConsoleServerPortTemplateEditView.as_view(), name='consoleserverporttemplate_edit'),
|
||||
path('console-server-port-templates/add/', views.ConsoleServerPortTemplateCreateView.as_view(), name='consoleserverporttemplate_add'),
|
||||
path('console-server-port-templates/edit/', views.ConsoleServerPortTemplateBulkEditView.as_view(), name='consoleserverporttemplate_bulk_edit'),
|
||||
path('console-server-port-templates/delete/', views.ConsoleServerPortTemplateBulkDeleteView.as_view(), name='consoleserverporttemplate_bulk_delete'),
|
||||
path('console-server-port-templates/<int:pk>/edit/', views.ConsoleServerPortTemplateEditView.as_view(), name='consoleserverporttemplate_edit'),
|
||||
path('console-server-port-templates/<int:pk>/delete/', views.ConsoleServerPortTemplateDeleteView.as_view(), name='consoleserverporttemplate_delete'),
|
||||
|
||||
# Power port templates
|
||||
path(r'device-types/<int:pk>/power-ports/add/', views.PowerPortTemplateCreateView.as_view(), name='devicetype_add_powerport'),
|
||||
path(r'device-types/<int:pk>/power-ports/delete/', views.PowerPortTemplateBulkDeleteView.as_view(), name='devicetype_delete_powerport'),
|
||||
path(r'power-port-templates/<int:pk>/edit/', views.PowerPortTemplateEditView.as_view(), name='powerporttemplate_edit'),
|
||||
path('power-port-templates/add/', views.PowerPortTemplateCreateView.as_view(), name='powerporttemplate_add'),
|
||||
path('power-port-templates/edit/', views.PowerPortTemplateBulkEditView.as_view(), name='powerporttemplate_bulk_edit'),
|
||||
path('power-port-templates/delete/', views.PowerPortTemplateBulkDeleteView.as_view(), name='powerporttemplate_bulk_delete'),
|
||||
path('power-port-templates/<int:pk>/edit/', views.PowerPortTemplateEditView.as_view(), name='powerporttemplate_edit'),
|
||||
path('power-port-templates/<int:pk>/delete/', views.PowerPortTemplateDeleteView.as_view(), name='powerporttemplate_delete'),
|
||||
|
||||
# Power outlet templates
|
||||
path(r'device-types/<int:pk>/power-outlets/add/', views.PowerOutletTemplateCreateView.as_view(), name='devicetype_add_poweroutlet'),
|
||||
path(r'device-types/<int:pk>/power-outlets/delete/', views.PowerOutletTemplateBulkDeleteView.as_view(), name='devicetype_delete_poweroutlet'),
|
||||
path(r'power-outlet-templates/<int:pk>/edit/', views.PowerOutletTemplateEditView.as_view(), name='poweroutlettemplate_edit'),
|
||||
path('power-outlet-templates/add/', views.PowerOutletTemplateCreateView.as_view(), name='poweroutlettemplate_add'),
|
||||
path('power-outlet-templates/edit/', views.PowerOutletTemplateBulkEditView.as_view(), name='poweroutlettemplate_bulk_edit'),
|
||||
path('power-outlet-templates/delete/', views.PowerOutletTemplateBulkDeleteView.as_view(), name='poweroutlettemplate_bulk_delete'),
|
||||
path('power-outlet-templates/<int:pk>/edit/', views.PowerOutletTemplateEditView.as_view(), name='poweroutlettemplate_edit'),
|
||||
path('power-outlet-templates/<int:pk>/delete/', views.PowerOutletTemplateDeleteView.as_view(), name='poweroutlettemplate_delete'),
|
||||
|
||||
# Interface templates
|
||||
path(r'device-types/<int:pk>/interfaces/add/', views.InterfaceTemplateCreateView.as_view(), name='devicetype_add_interface'),
|
||||
path(r'device-types/<int:pk>/interfaces/edit/', views.InterfaceTemplateBulkEditView.as_view(), name='devicetype_bulkedit_interface'),
|
||||
path(r'device-types/<int:pk>/interfaces/delete/', views.InterfaceTemplateBulkDeleteView.as_view(), name='devicetype_delete_interface'),
|
||||
path(r'interface-templates/<int:pk>/edit/', views.InterfaceTemplateEditView.as_view(), name='interfacetemplate_edit'),
|
||||
path('interface-templates/add/', views.InterfaceTemplateCreateView.as_view(), name='interfacetemplate_add'),
|
||||
path('interface-templates/edit/', views.InterfaceTemplateBulkEditView.as_view(), name='interfacetemplate_bulk_edit'),
|
||||
path('interface-templates/delete/', views.InterfaceTemplateBulkDeleteView.as_view(), name='interfacetemplate_bulk_delete'),
|
||||
path('interface-templates/<int:pk>/edit/', views.InterfaceTemplateEditView.as_view(), name='interfacetemplate_edit'),
|
||||
path('interface-templates/<int:pk>/delete/', views.InterfaceTemplateDeleteView.as_view(), name='interfacetemplate_delete'),
|
||||
|
||||
# Front port templates
|
||||
path(r'device-types/<int:pk>/front-ports/add/', views.FrontPortTemplateCreateView.as_view(), name='devicetype_add_frontport'),
|
||||
path(r'device-types/<int:pk>/front-ports/delete/', views.FrontPortTemplateBulkDeleteView.as_view(), name='devicetype_delete_frontport'),
|
||||
path(r'front-port-templates/<int:pk>/edit/', views.FrontPortTemplateEditView.as_view(), name='frontporttemplate_edit'),
|
||||
path('front-port-templates/add/', views.FrontPortTemplateCreateView.as_view(), name='frontporttemplate_add'),
|
||||
path('front-port-templates/edit/', views.FrontPortTemplateBulkEditView.as_view(), name='frontporttemplate_bulk_edit'),
|
||||
path('front-port-templates/delete/', views.FrontPortTemplateBulkDeleteView.as_view(), name='frontporttemplate_bulk_delete'),
|
||||
path('front-port-templates/<int:pk>/edit/', views.FrontPortTemplateEditView.as_view(), name='frontporttemplate_edit'),
|
||||
path('front-port-templates/<int:pk>/delete/', views.FrontPortTemplateDeleteView.as_view(), name='frontporttemplate_delete'),
|
||||
|
||||
# Rear port templates
|
||||
path(r'device-types/<int:pk>/rear-ports/add/', views.RearPortTemplateCreateView.as_view(), name='devicetype_add_rearport'),
|
||||
path(r'device-types/<int:pk>/rear-ports/delete/', views.RearPortTemplateBulkDeleteView.as_view(), name='devicetype_delete_rearport'),
|
||||
path(r'rear-port-templates/<int:pk>/edit/', views.RearPortTemplateEditView.as_view(), name='rearporttemplate_edit'),
|
||||
path('rear-port-templates/add/', views.RearPortTemplateCreateView.as_view(), name='rearporttemplate_add'),
|
||||
path('rear-port-templates/edit/', views.RearPortTemplateBulkEditView.as_view(), name='rearporttemplate_bulk_edit'),
|
||||
path('rear-port-templates/delete/', views.RearPortTemplateBulkDeleteView.as_view(), name='rearporttemplate_bulk_delete'),
|
||||
path('rear-port-templates/<int:pk>/edit/', views.RearPortTemplateEditView.as_view(), name='rearporttemplate_edit'),
|
||||
path('rear-port-templates/<int:pk>/delete/', views.RearPortTemplateDeleteView.as_view(), name='rearporttemplate_delete'),
|
||||
|
||||
# Device bay templates
|
||||
path(r'device-types/<int:pk>/device-bays/add/', views.DeviceBayTemplateCreateView.as_view(), name='devicetype_add_devicebay'),
|
||||
path(r'device-types/<int:pk>/device-bays/delete/', views.DeviceBayTemplateBulkDeleteView.as_view(), name='devicetype_delete_devicebay'),
|
||||
path(r'device-bay-templates/<int:pk>/edit/', views.DeviceBayTemplateEditView.as_view(), name='devicebaytemplate_edit'),
|
||||
path('device-bay-templates/add/', views.DeviceBayTemplateCreateView.as_view(), name='devicebaytemplate_add'),
|
||||
# path('device-bay-templates/edit/', views.DeviceBayTemplateBulkEditView.as_view(), name='devicebaytemplate_bulk_edit'),
|
||||
path('device-bay-templates/delete/', views.DeviceBayTemplateBulkDeleteView.as_view(), name='devicebaytemplate_bulk_delete'),
|
||||
path('device-bay-templates/<int:pk>/edit/', views.DeviceBayTemplateEditView.as_view(), name='devicebaytemplate_edit'),
|
||||
path('device-bay-templates/<int:pk>/delete/', views.DeviceBayTemplateDeleteView.as_view(), name='devicebaytemplate_delete'),
|
||||
|
||||
# Device roles
|
||||
path(r'device-roles/', views.DeviceRoleListView.as_view(), name='devicerole_list'),
|
||||
path(r'device-roles/add/', views.DeviceRoleCreateView.as_view(), name='devicerole_add'),
|
||||
path(r'device-roles/import/', views.DeviceRoleBulkImportView.as_view(), name='devicerole_import'),
|
||||
path(r'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(r'device-roles/<slug:slug>/changelog/', ObjectChangeLogView.as_view(), name='devicerole_changelog', kwargs={'model': DeviceRole}),
|
||||
path('device-roles/', views.DeviceRoleListView.as_view(), name='devicerole_list'),
|
||||
path('device-roles/add/', views.DeviceRoleCreateView.as_view(), name='devicerole_add'),
|
||||
path('device-roles/import/', views.DeviceRoleBulkImportView.as_view(), name='devicerole_import'),
|
||||
path('device-roles/delete/', views.DeviceRoleBulkDeleteView.as_view(), name='devicerole_bulk_delete'),
|
||||
path('device-roles/<slug:slug>/edit/', views.DeviceRoleEditView.as_view(), name='devicerole_edit'),
|
||||
path('device-roles/<slug:slug>/changelog/', ObjectChangeLogView.as_view(), name='devicerole_changelog', kwargs={'model': DeviceRole}),
|
||||
|
||||
# Platforms
|
||||
path(r'platforms/', views.PlatformListView.as_view(), name='platform_list'),
|
||||
path(r'platforms/add/', views.PlatformCreateView.as_view(), name='platform_add'),
|
||||
path(r'platforms/import/', views.PlatformBulkImportView.as_view(), name='platform_import'),
|
||||
path(r'platforms/delete/', views.PlatformBulkDeleteView.as_view(), name='platform_bulk_delete'),
|
||||
path(r'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/', views.PlatformListView.as_view(), name='platform_list'),
|
||||
path('platforms/add/', views.PlatformCreateView.as_view(), name='platform_add'),
|
||||
path('platforms/import/', views.PlatformBulkImportView.as_view(), name='platform_import'),
|
||||
path('platforms/delete/', views.PlatformBulkDeleteView.as_view(), name='platform_bulk_delete'),
|
||||
path('platforms/<slug:slug>/edit/', views.PlatformEditView.as_view(), name='platform_edit'),
|
||||
path('platforms/<slug:slug>/changelog/', ObjectChangeLogView.as_view(), name='platform_changelog', kwargs={'model': Platform}),
|
||||
|
||||
# Devices
|
||||
path(r'devices/', views.DeviceListView.as_view(), name='device_list'),
|
||||
path(r'devices/add/', views.DeviceCreateView.as_view(), name='device_add'),
|
||||
path(r'devices/import/', views.DeviceBulkImportView.as_view(), name='device_import'),
|
||||
path(r'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(r'devices/delete/', views.DeviceBulkDeleteView.as_view(), name='device_bulk_delete'),
|
||||
path(r'devices/<int:pk>/', views.DeviceView.as_view(), name='device'),
|
||||
path(r'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(r'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(r'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(r'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(r'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(r'devices/<int:object_id>/images/add/', ImageAttachmentEditView.as_view(), name='device_add_image', kwargs={'model': Device}),
|
||||
path('devices/', views.DeviceListView.as_view(), name='device_list'),
|
||||
path('devices/add/', views.DeviceCreateView.as_view(), name='device_add'),
|
||||
path('devices/import/', views.DeviceBulkImportView.as_view(), name='device_import'),
|
||||
path('devices/import/child-devices/', views.ChildDeviceBulkImportView.as_view(), name='device_import_child'),
|
||||
path('devices/edit/', views.DeviceBulkEditView.as_view(), name='device_bulk_edit'),
|
||||
path('devices/delete/', views.DeviceBulkDeleteView.as_view(), name='device_bulk_delete'),
|
||||
path('devices/<int:pk>/', views.DeviceView.as_view(), name='device'),
|
||||
path('devices/<int:pk>/edit/', views.DeviceEditView.as_view(), name='device_edit'),
|
||||
path('devices/<int:pk>/delete/', views.DeviceDeleteView.as_view(), name='device_delete'),
|
||||
path('devices/<int:pk>/config-context/', views.DeviceConfigContextView.as_view(), name='device_configcontext'),
|
||||
path('devices/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='device_changelog', kwargs={'model': Device}),
|
||||
path('devices/<int:pk>/inventory/', views.DeviceInventoryView.as_view(), name='device_inventory'),
|
||||
path('devices/<int:pk>/status/', views.DeviceStatusView.as_view(), name='device_status'),
|
||||
path('devices/<int:pk>/lldp-neighbors/', views.DeviceLLDPNeighborsView.as_view(), name='device_lldp_neighbors'),
|
||||
path('devices/<int:pk>/config/', views.DeviceConfigView.as_view(), name='device_config'),
|
||||
path('devices/<int:pk>/add-secret/', secret_add, name='device_addsecret'),
|
||||
path('devices/<int:device>/services/assign/', ServiceCreateView.as_view(), name='device_service_assign'),
|
||||
path('devices/<int:object_id>/images/add/', ImageAttachmentEditView.as_view(), name='device_add_image', kwargs={'model': Device}),
|
||||
|
||||
# Console ports
|
||||
path(r'devices/console-ports/add/', views.DeviceBulkAddConsolePortView.as_view(), name='device_bulk_add_consoleport'),
|
||||
path(r'devices/<int:pk>/console-ports/add/', views.ConsolePortCreateView.as_view(), name='consoleport_add'),
|
||||
path(r'devices/<int:pk>/console-ports/delete/', views.ConsolePortBulkDeleteView.as_view(), name='consoleport_bulk_delete'),
|
||||
path(r'console-ports/', views.ConsolePortListView.as_view(), name='consoleport_list'),
|
||||
path(r'console-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='consoleport_connect', kwargs={'termination_a_type': ConsolePort}),
|
||||
path(r'console-ports/<int:pk>/edit/', views.ConsolePortEditView.as_view(), name='consoleport_edit'),
|
||||
path(r'console-ports/<int:pk>/delete/', views.ConsolePortDeleteView.as_view(), name='consoleport_delete'),
|
||||
path(r'console-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='consoleport_trace', kwargs={'model': ConsolePort}),
|
||||
path(r'console-ports/import/', views.ConsolePortBulkImportView.as_view(), name='consoleport_import'),
|
||||
path('console-ports/', views.ConsolePortListView.as_view(), name='consoleport_list'),
|
||||
path('console-ports/add/', views.ConsolePortCreateView.as_view(), name='consoleport_add'),
|
||||
path('console-ports/import/', views.ConsolePortBulkImportView.as_view(), name='consoleport_import'),
|
||||
path('console-ports/edit/', views.ConsolePortBulkEditView.as_view(), name='consoleport_bulk_edit'),
|
||||
# TODO: Bulk rename, disconnect views for ConsolePorts
|
||||
path('console-ports/delete/', views.ConsolePortBulkDeleteView.as_view(), name='consoleport_bulk_delete'),
|
||||
path('console-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='consoleport_connect', kwargs={'termination_a_type': ConsolePort}),
|
||||
path('console-ports/<int:pk>/edit/', views.ConsolePortEditView.as_view(), name='consoleport_edit'),
|
||||
path('console-ports/<int:pk>/delete/', views.ConsolePortDeleteView.as_view(), name='consoleport_delete'),
|
||||
path('console-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='consoleport_trace', kwargs={'model': ConsolePort}),
|
||||
path('devices/console-ports/add/', views.DeviceBulkAddConsolePortView.as_view(), name='device_bulk_add_consoleport'),
|
||||
|
||||
# Console server ports
|
||||
path(r'devices/console-server-ports/add/', views.DeviceBulkAddConsoleServerPortView.as_view(), name='device_bulk_add_consoleserverport'),
|
||||
path(r'devices/<int:pk>/console-server-ports/add/', views.ConsoleServerPortCreateView.as_view(), name='consoleserverport_add'),
|
||||
path(r'devices/<int:pk>/console-server-ports/edit/', views.ConsoleServerPortBulkEditView.as_view(), name='consoleserverport_bulk_edit'),
|
||||
path(r'devices/<int:pk>/console-server-ports/delete/', views.ConsoleServerPortBulkDeleteView.as_view(), name='consoleserverport_bulk_delete'),
|
||||
path(r'console-server-ports/', views.ConsoleServerPortListView.as_view(), name='consoleserverport_list'),
|
||||
path(r'console-server-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='consoleserverport_connect', kwargs={'termination_a_type': ConsoleServerPort}),
|
||||
path(r'console-server-ports/<int:pk>/edit/', views.ConsoleServerPortEditView.as_view(), name='consoleserverport_edit'),
|
||||
path(r'console-server-ports/<int:pk>/delete/', views.ConsoleServerPortDeleteView.as_view(), name='consoleserverport_delete'),
|
||||
path(r'console-server-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='consoleserverport_trace', kwargs={'model': ConsoleServerPort}),
|
||||
path(r'console-server-ports/rename/', views.ConsoleServerPortBulkRenameView.as_view(), name='consoleserverport_bulk_rename'),
|
||||
path(r'console-server-ports/disconnect/', views.ConsoleServerPortBulkDisconnectView.as_view(), name='consoleserverport_bulk_disconnect'),
|
||||
path(r'console-server-ports/import/', views.ConsoleServerPortBulkImportView.as_view(), name='consoleserverport_import'),
|
||||
path('console-server-ports/', views.ConsoleServerPortListView.as_view(), name='consoleserverport_list'),
|
||||
path('console-server-ports/add/', views.ConsoleServerPortCreateView.as_view(), name='consoleserverport_add'),
|
||||
path('console-server-ports/import/', views.ConsoleServerPortBulkImportView.as_view(), name='consoleserverport_import'),
|
||||
path('console-server-ports/edit/', views.ConsoleServerPortBulkEditView.as_view(), name='consoleserverport_bulk_edit'),
|
||||
path('console-server-ports/rename/', views.ConsoleServerPortBulkRenameView.as_view(), name='consoleserverport_bulk_rename'),
|
||||
path('console-server-ports/disconnect/', views.ConsoleServerPortBulkDisconnectView.as_view(), name='consoleserverport_bulk_disconnect'),
|
||||
path('console-server-ports/delete/', views.ConsoleServerPortBulkDeleteView.as_view(), name='consoleserverport_bulk_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('console-server-ports/<int:pk>/edit/', views.ConsoleServerPortEditView.as_view(), name='consoleserverport_edit'),
|
||||
path('console-server-ports/<int:pk>/delete/', views.ConsoleServerPortDeleteView.as_view(), name='consoleserverport_delete'),
|
||||
path('console-server-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='consoleserverport_trace', kwargs={'model': ConsoleServerPort}),
|
||||
path('devices/console-server-ports/add/', views.DeviceBulkAddConsoleServerPortView.as_view(), name='device_bulk_add_consoleserverport'),
|
||||
|
||||
# Power ports
|
||||
path(r'devices/power-ports/add/', views.DeviceBulkAddPowerPortView.as_view(), name='device_bulk_add_powerport'),
|
||||
path(r'devices/<int:pk>/power-ports/add/', views.PowerPortCreateView.as_view(), name='powerport_add'),
|
||||
path(r'devices/<int:pk>/power-ports/delete/', views.PowerPortBulkDeleteView.as_view(), name='powerport_bulk_delete'),
|
||||
path(r'power-ports/', views.PowerPortListView.as_view(), name='powerport_list'),
|
||||
path(r'power-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='powerport_connect', kwargs={'termination_a_type': PowerPort}),
|
||||
path(r'power-ports/<int:pk>/edit/', views.PowerPortEditView.as_view(), name='powerport_edit'),
|
||||
path(r'power-ports/<int:pk>/delete/', views.PowerPortDeleteView.as_view(), name='powerport_delete'),
|
||||
path(r'power-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='powerport_trace', kwargs={'model': PowerPort}),
|
||||
path(r'power-ports/import/', views.PowerPortBulkImportView.as_view(), name='powerport_import'),
|
||||
path('power-ports/', views.PowerPortListView.as_view(), name='powerport_list'),
|
||||
path('power-ports/add/', views.PowerPortCreateView.as_view(), name='powerport_add'),
|
||||
path('power-ports/import/', views.PowerPortBulkImportView.as_view(), name='powerport_import'),
|
||||
path('power-ports/edit/', views.PowerPortBulkEditView.as_view(), name='powerport_bulk_edit'),
|
||||
# TODO: Bulk rename, disconnect views for PowerPorts
|
||||
path('power-ports/delete/', views.PowerPortBulkDeleteView.as_view(), name='powerport_bulk_delete'),
|
||||
path('power-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='powerport_connect', kwargs={'termination_a_type': PowerPort}),
|
||||
path('power-ports/<int:pk>/edit/', views.PowerPortEditView.as_view(), name='powerport_edit'),
|
||||
path('power-ports/<int:pk>/delete/', views.PowerPortDeleteView.as_view(), name='powerport_delete'),
|
||||
path('power-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='powerport_trace', kwargs={'model': PowerPort}),
|
||||
path('devices/power-ports/add/', views.DeviceBulkAddPowerPortView.as_view(), name='device_bulk_add_powerport'),
|
||||
|
||||
# Power outlets
|
||||
path(r'devices/power-outlets/add/', views.DeviceBulkAddPowerOutletView.as_view(), name='device_bulk_add_poweroutlet'),
|
||||
path(r'devices/<int:pk>/power-outlets/add/', views.PowerOutletCreateView.as_view(), name='poweroutlet_add'),
|
||||
path(r'devices/<int:pk>/power-outlets/edit/', views.PowerOutletBulkEditView.as_view(), name='poweroutlet_bulk_edit'),
|
||||
path(r'devices/<int:pk>/power-outlets/delete/', views.PowerOutletBulkDeleteView.as_view(), name='poweroutlet_bulk_delete'),
|
||||
path(r'power-outlets/', views.PowerOutletListView.as_view(), name='poweroutlet_list'),
|
||||
path(r'power-outlets/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='poweroutlet_connect', kwargs={'termination_a_type': PowerOutlet}),
|
||||
path(r'power-outlets/<int:pk>/edit/', views.PowerOutletEditView.as_view(), name='poweroutlet_edit'),
|
||||
path(r'power-outlets/<int:pk>/delete/', views.PowerOutletDeleteView.as_view(), name='poweroutlet_delete'),
|
||||
path(r'power-outlets/<int:pk>/trace/', views.CableTraceView.as_view(), name='poweroutlet_trace', kwargs={'model': PowerOutlet}),
|
||||
path(r'power-outlets/rename/', views.PowerOutletBulkRenameView.as_view(), name='poweroutlet_bulk_rename'),
|
||||
path(r'power-outlets/disconnect/', views.PowerOutletBulkDisconnectView.as_view(), name='poweroutlet_bulk_disconnect'),
|
||||
path(r'power-outlets/import/', views.PowerOutletBulkImportView.as_view(), name='poweroutlet_import'),
|
||||
path('power-outlets/', views.PowerOutletListView.as_view(), name='poweroutlet_list'),
|
||||
path('power-outlets/add/', views.PowerOutletCreateView.as_view(), name='poweroutlet_add'),
|
||||
path('power-outlets/import/', views.PowerOutletBulkImportView.as_view(), name='poweroutlet_import'),
|
||||
path('power-outlets/edit/', views.PowerOutletBulkEditView.as_view(), name='poweroutlet_bulk_edit'),
|
||||
path('power-outlets/rename/', views.PowerOutletBulkRenameView.as_view(), name='poweroutlet_bulk_rename'),
|
||||
path('power-outlets/disconnect/', views.PowerOutletBulkDisconnectView.as_view(), name='poweroutlet_bulk_disconnect'),
|
||||
path('power-outlets/delete/', views.PowerOutletBulkDeleteView.as_view(), name='poweroutlet_bulk_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('power-outlets/<int:pk>/edit/', views.PowerOutletEditView.as_view(), name='poweroutlet_edit'),
|
||||
path('power-outlets/<int:pk>/delete/', views.PowerOutletDeleteView.as_view(), name='poweroutlet_delete'),
|
||||
path('power-outlets/<int:pk>/trace/', views.CableTraceView.as_view(), name='poweroutlet_trace', kwargs={'model': PowerOutlet}),
|
||||
path('devices/power-outlets/add/', views.DeviceBulkAddPowerOutletView.as_view(), name='device_bulk_add_poweroutlet'),
|
||||
|
||||
# Interfaces
|
||||
path(r'devices/interfaces/add/', views.DeviceBulkAddInterfaceView.as_view(), name='device_bulk_add_interface'),
|
||||
path(r'devices/<int:pk>/interfaces/add/', views.InterfaceCreateView.as_view(), name='interface_add'),
|
||||
path(r'devices/<int:pk>/interfaces/edit/', views.InterfaceBulkEditView.as_view(), name='interface_bulk_edit'),
|
||||
path(r'devices/<int:pk>/interfaces/delete/', views.InterfaceBulkDeleteView.as_view(), name='interface_bulk_delete'),
|
||||
path(r'interfaces/', views.InterfaceListView.as_view(), name='interface_list'),
|
||||
path(r'interfaces/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='interface_connect', kwargs={'termination_a_type': Interface}),
|
||||
path(r'interfaces/<int:pk>/', views.InterfaceView.as_view(), name='interface'),
|
||||
path(r'interfaces/<int:pk>/edit/', views.InterfaceEditView.as_view(), name='interface_edit'),
|
||||
path(r'interfaces/<int:pk>/delete/', views.InterfaceDeleteView.as_view(), name='interface_delete'),
|
||||
path(r'interfaces/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='interface_changelog', kwargs={'model': Interface}),
|
||||
path(r'interfaces/<int:pk>/trace/', views.CableTraceView.as_view(), name='interface_trace', kwargs={'model': Interface}),
|
||||
path(r'interfaces/rename/', views.InterfaceBulkRenameView.as_view(), name='interface_bulk_rename'),
|
||||
path(r'interfaces/disconnect/', views.InterfaceBulkDisconnectView.as_view(), name='interface_bulk_disconnect'),
|
||||
path(r'interfaces/import/', views.InterfaceBulkImportView.as_view(), name='interface_import'),
|
||||
path('interfaces/', views.InterfaceListView.as_view(), name='interface_list'),
|
||||
path('interfaces/add/', views.InterfaceCreateView.as_view(), name='interface_add'),
|
||||
path('interfaces/import/', views.InterfaceBulkImportView.as_view(), name='interface_import'),
|
||||
path('interfaces/edit/', views.InterfaceBulkEditView.as_view(), name='interface_bulk_edit'),
|
||||
path('interfaces/rename/', views.InterfaceBulkRenameView.as_view(), name='interface_bulk_rename'),
|
||||
path('interfaces/disconnect/', views.InterfaceBulkDisconnectView.as_view(), name='interface_bulk_disconnect'),
|
||||
path('interfaces/delete/', views.InterfaceBulkDeleteView.as_view(), name='interface_bulk_delete'),
|
||||
path('interfaces/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='interface_connect', kwargs={'termination_a_type': Interface}),
|
||||
path('interfaces/<int:pk>/', views.InterfaceView.as_view(), name='interface'),
|
||||
path('interfaces/<int:pk>/edit/', views.InterfaceEditView.as_view(), name='interface_edit'),
|
||||
path('interfaces/<int:pk>/delete/', views.InterfaceDeleteView.as_view(), name='interface_delete'),
|
||||
path('interfaces/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='interface_changelog', kwargs={'model': Interface}),
|
||||
path('interfaces/<int:pk>/trace/', views.CableTraceView.as_view(), name='interface_trace', kwargs={'model': Interface}),
|
||||
path('devices/interfaces/add/', views.DeviceBulkAddInterfaceView.as_view(), name='device_bulk_add_interface'),
|
||||
|
||||
# Front ports
|
||||
# path(r'devices/front-ports/add/', views.DeviceBulkAddFrontPortView.as_view(), name='device_bulk_add_frontport'),
|
||||
path(r'devices/<int:pk>/front-ports/add/', views.FrontPortCreateView.as_view(), name='frontport_add'),
|
||||
path(r'devices/<int:pk>/front-ports/edit/', views.FrontPortBulkEditView.as_view(), name='frontport_bulk_edit'),
|
||||
path(r'devices/<int:pk>/front-ports/delete/', views.FrontPortBulkDeleteView.as_view(), name='frontport_bulk_delete'),
|
||||
path(r'front-ports/', views.FrontPortListView.as_view(), name='frontport_list'),
|
||||
path(r'front-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='frontport_connect', kwargs={'termination_a_type': FrontPort}),
|
||||
path(r'front-ports/<int:pk>/edit/', views.FrontPortEditView.as_view(), name='frontport_edit'),
|
||||
path(r'front-ports/<int:pk>/delete/', views.FrontPortDeleteView.as_view(), name='frontport_delete'),
|
||||
path(r'front-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='frontport_trace', kwargs={'model': FrontPort}),
|
||||
path(r'front-ports/rename/', views.FrontPortBulkRenameView.as_view(), name='frontport_bulk_rename'),
|
||||
path(r'front-ports/disconnect/', views.FrontPortBulkDisconnectView.as_view(), name='frontport_bulk_disconnect'),
|
||||
path(r'front-ports/import/', views.FrontPortBulkImportView.as_view(), name='frontport_import'),
|
||||
path('front-ports/', views.FrontPortListView.as_view(), name='frontport_list'),
|
||||
path('front-ports/add/', views.FrontPortCreateView.as_view(), name='frontport_add'),
|
||||
path('front-ports/import/', views.FrontPortBulkImportView.as_view(), name='frontport_import'),
|
||||
path('front-ports/edit/', views.FrontPortBulkEditView.as_view(), name='frontport_bulk_edit'),
|
||||
path('front-ports/rename/', views.FrontPortBulkRenameView.as_view(), name='frontport_bulk_rename'),
|
||||
path('front-ports/disconnect/', views.FrontPortBulkDisconnectView.as_view(), name='frontport_bulk_disconnect'),
|
||||
path('front-ports/delete/', views.FrontPortBulkDeleteView.as_view(), name='frontport_bulk_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('front-ports/<int:pk>/edit/', views.FrontPortEditView.as_view(), name='frontport_edit'),
|
||||
path('front-ports/<int:pk>/delete/', views.FrontPortDeleteView.as_view(), name='frontport_delete'),
|
||||
path('front-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='frontport_trace', kwargs={'model': FrontPort}),
|
||||
# path('devices/front-ports/add/', views.DeviceBulkAddFrontPortView.as_view(), name='device_bulk_add_frontport'),
|
||||
|
||||
# Rear ports
|
||||
# path(r'devices/rear-ports/add/', views.DeviceBulkAddRearPortView.as_view(), name='device_bulk_add_rearport'),
|
||||
path(r'devices/<int:pk>/rear-ports/add/', views.RearPortCreateView.as_view(), name='rearport_add'),
|
||||
path(r'devices/<int:pk>/rear-ports/edit/', views.RearPortBulkEditView.as_view(), name='rearport_bulk_edit'),
|
||||
path(r'devices/<int:pk>/rear-ports/delete/', views.RearPortBulkDeleteView.as_view(), name='rearport_bulk_delete'),
|
||||
path(r'rear-ports/', views.RearPortListView.as_view(), name='rearport_list'),
|
||||
path(r'rear-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='rearport_connect', kwargs={'termination_a_type': RearPort}),
|
||||
path(r'rear-ports/<int:pk>/edit/', views.RearPortEditView.as_view(), name='rearport_edit'),
|
||||
path(r'rear-ports/<int:pk>/delete/', views.RearPortDeleteView.as_view(), name='rearport_delete'),
|
||||
path(r'rear-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='rearport_trace', kwargs={'model': RearPort}),
|
||||
path(r'rear-ports/rename/', views.RearPortBulkRenameView.as_view(), name='rearport_bulk_rename'),
|
||||
path(r'rear-ports/disconnect/', views.RearPortBulkDisconnectView.as_view(), name='rearport_bulk_disconnect'),
|
||||
path(r'rear-ports/import/', views.RearPortBulkImportView.as_view(), name='rearport_import'),
|
||||
path('rear-ports/', views.RearPortListView.as_view(), name='rearport_list'),
|
||||
path('rear-ports/add/', views.RearPortCreateView.as_view(), name='rearport_add'),
|
||||
path('rear-ports/import/', views.RearPortBulkImportView.as_view(), name='rearport_import'),
|
||||
path('rear-ports/edit/', views.RearPortBulkEditView.as_view(), name='rearport_bulk_edit'),
|
||||
path('rear-ports/rename/', views.RearPortBulkRenameView.as_view(), name='rearport_bulk_rename'),
|
||||
path('rear-ports/disconnect/', views.RearPortBulkDisconnectView.as_view(), name='rearport_bulk_disconnect'),
|
||||
path('rear-ports/delete/', views.RearPortBulkDeleteView.as_view(), name='rearport_bulk_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('rear-ports/<int:pk>/edit/', views.RearPortEditView.as_view(), name='rearport_edit'),
|
||||
path('rear-ports/<int:pk>/delete/', views.RearPortDeleteView.as_view(), name='rearport_delete'),
|
||||
path('rear-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='rearport_trace', kwargs={'model': RearPort}),
|
||||
# path('devices/rear-ports/add/', views.DeviceBulkAddRearPortView.as_view(), name='device_bulk_add_rearport'),
|
||||
|
||||
# Device bays
|
||||
path(r'devices/device-bays/add/', views.DeviceBulkAddDeviceBayView.as_view(), name='device_bulk_add_devicebay'),
|
||||
path(r'devices/<int:pk>/bays/add/', views.DeviceBayCreateView.as_view(), name='devicebay_add'),
|
||||
path(r'devices/<int:pk>/bays/delete/', views.DeviceBayBulkDeleteView.as_view(), name='devicebay_bulk_delete'),
|
||||
path(r'device-bays/', views.DeviceBayListView.as_view(), name='devicebay_list'),
|
||||
path(r'device-bays/<int:pk>/edit/', views.DeviceBayEditView.as_view(), name='devicebay_edit'),
|
||||
path(r'device-bays/<int:pk>/delete/', views.DeviceBayDeleteView.as_view(), name='devicebay_delete'),
|
||||
path(r'device-bays/<int:pk>/populate/', views.DeviceBayPopulateView.as_view(), name='devicebay_populate'),
|
||||
path(r'device-bays/<int:pk>/depopulate/', views.DeviceBayDepopulateView.as_view(), name='devicebay_depopulate'),
|
||||
path(r'device-bays/rename/', views.DeviceBayBulkRenameView.as_view(), name='devicebay_bulk_rename'),
|
||||
path(r'device-bays/import/', views.DeviceBayBulkImportView.as_view(), name='devicebay_import'),
|
||||
path('device-bays/', views.DeviceBayListView.as_view(), name='devicebay_list'),
|
||||
path('device-bays/add/', views.DeviceBayCreateView.as_view(), name='devicebay_add'),
|
||||
path('device-bays/import/', views.DeviceBayBulkImportView.as_view(), name='devicebay_import'),
|
||||
# TODO: Bulk edit view for DeviceBays
|
||||
path('device-bays/rename/', views.DeviceBayBulkRenameView.as_view(), name='devicebay_bulk_rename'),
|
||||
path('device-bays/delete/', views.DeviceBayBulkDeleteView.as_view(), name='devicebay_bulk_delete'),
|
||||
path('device-bays/<int:pk>/edit/', views.DeviceBayEditView.as_view(), name='devicebay_edit'),
|
||||
path('device-bays/<int:pk>/delete/', views.DeviceBayDeleteView.as_view(), name='devicebay_delete'),
|
||||
path('device-bays/<int:pk>/populate/', views.DeviceBayPopulateView.as_view(), name='devicebay_populate'),
|
||||
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
|
||||
path(r'inventory-items/', views.InventoryItemListView.as_view(), name='inventoryitem_list'),
|
||||
path(r'inventory-items/import/', views.InventoryItemBulkImportView.as_view(), name='inventoryitem_import'),
|
||||
path(r'inventory-items/edit/', views.InventoryItemBulkEditView.as_view(), name='inventoryitem_bulk_edit'),
|
||||
path(r'inventory-items/delete/', views.InventoryItemBulkDeleteView.as_view(), name='inventoryitem_bulk_delete'),
|
||||
path(r'inventory-items/<int:pk>/edit/', views.InventoryItemEditView.as_view(), name='inventoryitem_edit'),
|
||||
path(r'inventory-items/<int:pk>/delete/', views.InventoryItemDeleteView.as_view(), name='inventoryitem_delete'),
|
||||
path(r'devices/<int:device>/inventory-items/add/', views.InventoryItemEditView.as_view(), name='inventoryitem_add'),
|
||||
path('inventory-items/', views.InventoryItemListView.as_view(), name='inventoryitem_list'),
|
||||
path('inventory-items/add/', views.InventoryItemCreateView.as_view(), name='inventoryitem_add'),
|
||||
path('inventory-items/import/', views.InventoryItemBulkImportView.as_view(), name='inventoryitem_import'),
|
||||
path('inventory-items/edit/', views.InventoryItemBulkEditView.as_view(), name='inventoryitem_bulk_edit'),
|
||||
# TODO: Bulk rename view for InventoryItems
|
||||
path('inventory-items/delete/', views.InventoryItemBulkDeleteView.as_view(), name='inventoryitem_bulk_delete'),
|
||||
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
|
||||
path(r'cables/', views.CableListView.as_view(), name='cable_list'),
|
||||
path(r'cables/import/', views.CableBulkImportView.as_view(), name='cable_import'),
|
||||
path(r'cables/edit/', views.CableBulkEditView.as_view(), name='cable_bulk_edit'),
|
||||
path(r'cables/delete/', views.CableBulkDeleteView.as_view(), name='cable_bulk_delete'),
|
||||
path(r'cables/<int:pk>/', views.CableView.as_view(), name='cable'),
|
||||
path(r'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(r'cables/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='cable_changelog', kwargs={'model': Cable}),
|
||||
path('cables/', views.CableListView.as_view(), name='cable_list'),
|
||||
path('cables/import/', views.CableBulkImportView.as_view(), name='cable_import'),
|
||||
path('cables/edit/', views.CableBulkEditView.as_view(), name='cable_bulk_edit'),
|
||||
path('cables/delete/', views.CableBulkDeleteView.as_view(), name='cable_bulk_delete'),
|
||||
path('cables/<int:pk>/', views.CableView.as_view(), name='cable'),
|
||||
path('cables/<int:pk>/edit/', views.CableEditView.as_view(), name='cable_edit'),
|
||||
path('cables/<int:pk>/delete/', views.CableDeleteView.as_view(), name='cable_delete'),
|
||||
path('cables/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='cable_changelog', kwargs={'model': Cable}),
|
||||
|
||||
# Console/power/interface connections (read-only)
|
||||
path(r'console-connections/', views.ConsoleConnectionsListView.as_view(), name='console_connections_list'),
|
||||
path(r'power-connections/', views.PowerConnectionsListView.as_view(), name='power_connections_list'),
|
||||
path(r'interface-connections/', views.InterfaceConnectionsListView.as_view(), name='interface_connections_list'),
|
||||
path('console-connections/', views.ConsoleConnectionsListView.as_view(), name='console_connections_list'),
|
||||
path('power-connections/', views.PowerConnectionsListView.as_view(), name='power_connections_list'),
|
||||
path('interface-connections/', views.InterfaceConnectionsListView.as_view(), name='interface_connections_list'),
|
||||
|
||||
# Virtual chassis
|
||||
path(r'virtual-chassis/', views.VirtualChassisListView.as_view(), name='virtualchassis_list'),
|
||||
path(r'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(r'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(r'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/', views.VirtualChassisListView.as_view(), name='virtualchassis_list'),
|
||||
path('virtual-chassis/add/', views.VirtualChassisCreateView.as_view(), name='virtualchassis_add'),
|
||||
path('virtual-chassis/<int:pk>/edit/', views.VirtualChassisEditView.as_view(), name='virtualchassis_edit'),
|
||||
path('virtual-chassis/<int:pk>/delete/', views.VirtualChassisDeleteView.as_view(), name='virtualchassis_delete'),
|
||||
path('virtual-chassis/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='virtualchassis_changelog', kwargs={'model': VirtualChassis}),
|
||||
path('virtual-chassis/<int:pk>/add-member/', views.VirtualChassisAddMemberView.as_view(), name='virtualchassis_add_member'),
|
||||
path('virtual-chassis-members/<int:pk>/delete/', views.VirtualChassisRemoveMemberView.as_view(), name='virtualchassis_remove_member'),
|
||||
|
||||
# Power panels
|
||||
path(r'power-panels/', views.PowerPanelListView.as_view(), name='powerpanel_list'),
|
||||
path(r'power-panels/add/', views.PowerPanelCreateView.as_view(), name='powerpanel_add'),
|
||||
path(r'power-panels/import/', views.PowerPanelBulkImportView.as_view(), name='powerpanel_import'),
|
||||
path(r'power-panels/delete/', views.PowerPanelBulkDeleteView.as_view(), name='powerpanel_bulk_delete'),
|
||||
path(r'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(r'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/', views.PowerPanelListView.as_view(), name='powerpanel_list'),
|
||||
path('power-panels/add/', views.PowerPanelCreateView.as_view(), name='powerpanel_add'),
|
||||
path('power-panels/import/', views.PowerPanelBulkImportView.as_view(), name='powerpanel_import'),
|
||||
path('power-panels/delete/', views.PowerPanelBulkDeleteView.as_view(), name='powerpanel_bulk_delete'),
|
||||
path('power-panels/<int:pk>/', views.PowerPanelView.as_view(), name='powerpanel'),
|
||||
path('power-panels/<int:pk>/edit/', views.PowerPanelEditView.as_view(), name='powerpanel_edit'),
|
||||
path('power-panels/<int:pk>/delete/', views.PowerPanelDeleteView.as_view(), name='powerpanel_delete'),
|
||||
path('power-panels/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='powerpanel_changelog', kwargs={'model': PowerPanel}),
|
||||
|
||||
# Power feeds
|
||||
path(r'power-feeds/', views.PowerFeedListView.as_view(), name='powerfeed_list'),
|
||||
path(r'power-feeds/add/', views.PowerFeedCreateView.as_view(), name='powerfeed_add'),
|
||||
path(r'power-feeds/import/', views.PowerFeedBulkImportView.as_view(), name='powerfeed_import'),
|
||||
path(r'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(r'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(r'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/', views.PowerFeedListView.as_view(), name='powerfeed_list'),
|
||||
path('power-feeds/add/', views.PowerFeedCreateView.as_view(), name='powerfeed_add'),
|
||||
path('power-feeds/import/', views.PowerFeedBulkImportView.as_view(), name='powerfeed_import'),
|
||||
path('power-feeds/edit/', views.PowerFeedBulkEditView.as_view(), name='powerfeed_bulk_edit'),
|
||||
path('power-feeds/delete/', views.PowerFeedBulkDeleteView.as_view(), name='powerfeed_bulk_delete'),
|
||||
path('power-feeds/<int:pk>/', views.PowerFeedView.as_view(), name='powerfeed'),
|
||||
path('power-feeds/<int:pk>/edit/', views.PowerFeedEditView.as_view(), name='powerfeed_edit'),
|
||||
path('power-feeds/<int:pk>/delete/', views.PowerFeedDeleteView.as_view(), name='powerfeed_delete'),
|
||||
path('power-feeds/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='powerfeed_changelog', kwargs={'model': PowerFeed}),
|
||||
|
||||
]
|
||||
|
||||
@@ -31,6 +31,7 @@ from utilities.views import (
|
||||
from virtualization.models import VirtualMachine
|
||||
from . import filters, forms, tables
|
||||
from .choices import DeviceFaceChoices
|
||||
from .constants import NONCONNECTABLE_IFACE_TYPES
|
||||
from .models import (
|
||||
Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
|
||||
DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate,
|
||||
@@ -152,7 +153,6 @@ class RegionListView(PermissionRequiredMixin, ObjectListView):
|
||||
filterset = filters.RegionFilterSet
|
||||
filterset_form = forms.RegionFilterForm
|
||||
table = tables.RegionTable
|
||||
template_name = 'dcim/region_list.html'
|
||||
|
||||
|
||||
class RegionCreateView(PermissionRequiredMixin, ObjectEditView):
|
||||
@@ -191,7 +191,6 @@ class SiteListView(PermissionRequiredMixin, ObjectListView):
|
||||
filterset = filters.SiteFilterSet
|
||||
filterset_form = forms.SiteFilterForm
|
||||
table = tables.SiteTable
|
||||
template_name = 'dcim/site_list.html'
|
||||
|
||||
|
||||
class SiteView(PermissionRequiredMixin, View):
|
||||
@@ -271,7 +270,6 @@ class RackGroupListView(PermissionRequiredMixin, ObjectListView):
|
||||
filterset = filters.RackGroupFilterSet
|
||||
filterset_form = forms.RackGroupFilterForm
|
||||
table = tables.RackGroupTable
|
||||
template_name = 'dcim/rackgroup_list.html'
|
||||
|
||||
|
||||
class RackGroupCreateView(PermissionRequiredMixin, ObjectEditView):
|
||||
@@ -308,7 +306,6 @@ class RackRoleListView(PermissionRequiredMixin, ObjectListView):
|
||||
permission_required = 'dcim.view_rackrole'
|
||||
queryset = RackRole.objects.annotate(rack_count=Count('racks'))
|
||||
table = tables.RackRoleTable
|
||||
template_name = 'dcim/rackrole_list.html'
|
||||
|
||||
|
||||
class RackRoleCreateView(PermissionRequiredMixin, ObjectEditView):
|
||||
@@ -350,7 +347,6 @@ class RackListView(PermissionRequiredMixin, ObjectListView):
|
||||
filterset = filters.RackFilterSet
|
||||
filterset_form = forms.RackFilterForm
|
||||
table = tables.RackDetailTable
|
||||
template_name = 'dcim/rack_list.html'
|
||||
|
||||
|
||||
class RackElevationListView(PermissionRequiredMixin, View):
|
||||
@@ -361,7 +357,7 @@ class RackElevationListView(PermissionRequiredMixin, View):
|
||||
|
||||
def get(self, request):
|
||||
|
||||
racks = Rack.objects.prefetch_related('site', 'group', 'tenant', 'role', 'devices__device_type')
|
||||
racks = Rack.objects.prefetch_related('role')
|
||||
racks = filters.RackFilterSet(request.GET, racks).qs
|
||||
total_count = racks.count()
|
||||
|
||||
@@ -474,7 +470,7 @@ class RackReservationListView(PermissionRequiredMixin, ObjectListView):
|
||||
filterset = filters.RackReservationFilterSet
|
||||
filterset_form = forms.RackReservationFilterForm
|
||||
table = tables.RackReservationTable
|
||||
template_name = 'dcim/rackreservation_list.html'
|
||||
action_buttons = ()
|
||||
|
||||
|
||||
class RackReservationCreateView(PermissionRequiredMixin, ObjectEditView):
|
||||
@@ -533,7 +529,6 @@ class ManufacturerListView(PermissionRequiredMixin, ObjectListView):
|
||||
platform_count=Count('platforms', distinct=True),
|
||||
)
|
||||
table = tables.ManufacturerTable
|
||||
template_name = 'dcim/manufacturer_list.html'
|
||||
|
||||
|
||||
class ManufacturerCreateView(PermissionRequiredMixin, ObjectEditView):
|
||||
@@ -571,7 +566,6 @@ class DeviceTypeListView(PermissionRequiredMixin, ObjectListView):
|
||||
filterset = filters.DeviceTypeFilterSet
|
||||
filterset_form = forms.DeviceTypeFilterForm
|
||||
table = tables.DeviceTypeTable
|
||||
template_name = 'dcim/devicetype_list.html'
|
||||
|
||||
|
||||
class DeviceTypeView(PermissionRequiredMixin, View):
|
||||
@@ -700,13 +694,11 @@ class DeviceTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
|
||||
|
||||
#
|
||||
# Device type components
|
||||
# Console port templates
|
||||
#
|
||||
|
||||
class ConsolePortTemplateCreateView(PermissionRequiredMixin, ComponentCreateView):
|
||||
permission_required = 'dcim.add_consoleporttemplate'
|
||||
parent_model = DeviceType
|
||||
parent_field = 'device_type'
|
||||
model = ConsolePortTemplate
|
||||
form = forms.ConsolePortTemplateCreateForm
|
||||
model_form = forms.ConsolePortTemplateForm
|
||||
@@ -719,17 +711,30 @@ class ConsolePortTemplateEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
model_form = forms.ConsolePortTemplateForm
|
||||
|
||||
|
||||
class ConsolePortTemplateDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
permission_required = 'dcim.delete_consoleporttemplate'
|
||||
model = ConsolePortTemplate
|
||||
|
||||
|
||||
class ConsolePortTemplateBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'dcim.change_consoleporttemplate'
|
||||
queryset = ConsolePortTemplate.objects.all()
|
||||
table = tables.ConsolePortTemplateTable
|
||||
form = forms.ConsolePortTemplateBulkEditForm
|
||||
|
||||
|
||||
class ConsolePortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_consoleporttemplate'
|
||||
queryset = ConsolePortTemplate.objects.all()
|
||||
parent_model = DeviceType
|
||||
table = tables.ConsolePortTemplateTable
|
||||
|
||||
|
||||
#
|
||||
# Console server port templates
|
||||
#
|
||||
|
||||
class ConsoleServerPortTemplateCreateView(PermissionRequiredMixin, ComponentCreateView):
|
||||
permission_required = 'dcim.add_consoleserverporttemplate'
|
||||
parent_model = DeviceType
|
||||
parent_field = 'device_type'
|
||||
model = ConsoleServerPortTemplate
|
||||
form = forms.ConsoleServerPortTemplateCreateForm
|
||||
model_form = forms.ConsoleServerPortTemplateForm
|
||||
@@ -742,17 +747,30 @@ class ConsoleServerPortTemplateEditView(PermissionRequiredMixin, ObjectEditView)
|
||||
model_form = forms.ConsoleServerPortTemplateForm
|
||||
|
||||
|
||||
class ConsoleServerPortTemplateDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
permission_required = 'dcim.delete_consoleserverporttemplate'
|
||||
model = ConsoleServerPortTemplate
|
||||
|
||||
|
||||
class ConsoleServerPortTemplateBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'dcim.change_consoleserverporttemplate'
|
||||
queryset = ConsoleServerPortTemplate.objects.all()
|
||||
table = tables.ConsoleServerPortTemplateTable
|
||||
form = forms.ConsoleServerPortTemplateBulkEditForm
|
||||
|
||||
|
||||
class ConsoleServerPortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_consoleserverporttemplate'
|
||||
queryset = ConsoleServerPortTemplate.objects.all()
|
||||
parent_model = DeviceType
|
||||
table = tables.ConsoleServerPortTemplateTable
|
||||
|
||||
|
||||
#
|
||||
# Power port templates
|
||||
#
|
||||
|
||||
class PowerPortTemplateCreateView(PermissionRequiredMixin, ComponentCreateView):
|
||||
permission_required = 'dcim.add_powerporttemplate'
|
||||
parent_model = DeviceType
|
||||
parent_field = 'device_type'
|
||||
model = PowerPortTemplate
|
||||
form = forms.PowerPortTemplateCreateForm
|
||||
model_form = forms.PowerPortTemplateForm
|
||||
@@ -765,17 +783,30 @@ class PowerPortTemplateEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
model_form = forms.PowerPortTemplateForm
|
||||
|
||||
|
||||
class PowerPortTemplateDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
permission_required = 'dcim.delete_powerporttemplate'
|
||||
model = PowerPortTemplate
|
||||
|
||||
|
||||
class PowerPortTemplateBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'dcim.change_powerporttemplate'
|
||||
queryset = PowerPortTemplate.objects.all()
|
||||
table = tables.PowerPortTemplateTable
|
||||
form = forms.PowerPortTemplateBulkEditForm
|
||||
|
||||
|
||||
class PowerPortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_powerporttemplate'
|
||||
queryset = PowerPortTemplate.objects.all()
|
||||
parent_model = DeviceType
|
||||
table = tables.PowerPortTemplateTable
|
||||
|
||||
|
||||
#
|
||||
# Power outlet templates
|
||||
#
|
||||
|
||||
class PowerOutletTemplateCreateView(PermissionRequiredMixin, ComponentCreateView):
|
||||
permission_required = 'dcim.add_poweroutlettemplate'
|
||||
parent_model = DeviceType
|
||||
parent_field = 'device_type'
|
||||
model = PowerOutletTemplate
|
||||
form = forms.PowerOutletTemplateCreateForm
|
||||
model_form = forms.PowerOutletTemplateForm
|
||||
@@ -788,17 +819,30 @@ class PowerOutletTemplateEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
model_form = forms.PowerOutletTemplateForm
|
||||
|
||||
|
||||
class PowerOutletTemplateDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
permission_required = 'dcim.delete_poweroutlettemplate'
|
||||
model = PowerOutletTemplate
|
||||
|
||||
|
||||
class PowerOutletTemplateBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'dcim.change_poweroutlettemplate'
|
||||
queryset = PowerOutletTemplate.objects.all()
|
||||
table = tables.PowerOutletTemplateTable
|
||||
form = forms.PowerOutletTemplateBulkEditForm
|
||||
|
||||
|
||||
class PowerOutletTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_poweroutlettemplate'
|
||||
queryset = PowerOutletTemplate.objects.all()
|
||||
parent_model = DeviceType
|
||||
table = tables.PowerOutletTemplateTable
|
||||
|
||||
|
||||
#
|
||||
# Interface templates
|
||||
#
|
||||
|
||||
class InterfaceTemplateCreateView(PermissionRequiredMixin, ComponentCreateView):
|
||||
permission_required = 'dcim.add_interfacetemplate'
|
||||
parent_model = DeviceType
|
||||
parent_field = 'device_type'
|
||||
model = InterfaceTemplate
|
||||
form = forms.InterfaceTemplateCreateForm
|
||||
model_form = forms.InterfaceTemplateForm
|
||||
@@ -811,10 +855,14 @@ class InterfaceTemplateEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
model_form = forms.InterfaceTemplateForm
|
||||
|
||||
|
||||
class InterfaceTemplateDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
permission_required = 'dcim.delete_interfacetemplate'
|
||||
model = InterfaceTemplate
|
||||
|
||||
|
||||
class InterfaceTemplateBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'dcim.change_interfacetemplate'
|
||||
queryset = InterfaceTemplate.objects.all()
|
||||
parent_model = DeviceType
|
||||
table = tables.InterfaceTemplateTable
|
||||
form = forms.InterfaceTemplateBulkEditForm
|
||||
|
||||
@@ -822,14 +870,15 @@ class InterfaceTemplateBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
class InterfaceTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_interfacetemplate'
|
||||
queryset = InterfaceTemplate.objects.all()
|
||||
parent_model = DeviceType
|
||||
table = tables.InterfaceTemplateTable
|
||||
|
||||
|
||||
#
|
||||
# Front port templates
|
||||
#
|
||||
|
||||
class FrontPortTemplateCreateView(PermissionRequiredMixin, ComponentCreateView):
|
||||
permission_required = 'dcim.add_frontporttemplate'
|
||||
parent_model = DeviceType
|
||||
parent_field = 'device_type'
|
||||
model = FrontPortTemplate
|
||||
form = forms.FrontPortTemplateCreateForm
|
||||
model_form = forms.FrontPortTemplateForm
|
||||
@@ -842,17 +891,30 @@ class FrontPortTemplateEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
model_form = forms.FrontPortTemplateForm
|
||||
|
||||
|
||||
class FrontPortTemplateDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
permission_required = 'dcim.delete_frontporttemplate'
|
||||
model = FrontPortTemplate
|
||||
|
||||
|
||||
class FrontPortTemplateBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'dcim.change_frontporttemplate'
|
||||
queryset = FrontPortTemplate.objects.all()
|
||||
table = tables.FrontPortTemplateTable
|
||||
form = forms.FrontPortTemplateBulkEditForm
|
||||
|
||||
|
||||
class FrontPortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_frontporttemplate'
|
||||
queryset = FrontPortTemplate.objects.all()
|
||||
parent_model = DeviceType
|
||||
table = tables.FrontPortTemplateTable
|
||||
|
||||
|
||||
#
|
||||
# Rear port templates
|
||||
#
|
||||
|
||||
class RearPortTemplateCreateView(PermissionRequiredMixin, ComponentCreateView):
|
||||
permission_required = 'dcim.add_rearporttemplate'
|
||||
parent_model = DeviceType
|
||||
parent_field = 'device_type'
|
||||
model = RearPortTemplate
|
||||
form = forms.RearPortTemplateCreateForm
|
||||
model_form = forms.RearPortTemplateForm
|
||||
@@ -865,17 +927,30 @@ class RearPortTemplateEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
model_form = forms.RearPortTemplateForm
|
||||
|
||||
|
||||
class RearPortTemplateDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
permission_required = 'dcim.delete_rearporttemplate'
|
||||
model = RearPortTemplate
|
||||
|
||||
|
||||
class RearPortTemplateBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'dcim.change_rearporttemplate'
|
||||
queryset = RearPortTemplate.objects.all()
|
||||
table = tables.RearPortTemplateTable
|
||||
form = forms.RearPortTemplateBulkEditForm
|
||||
|
||||
|
||||
class RearPortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_rearporttemplate'
|
||||
queryset = RearPortTemplate.objects.all()
|
||||
parent_model = DeviceType
|
||||
table = tables.RearPortTemplateTable
|
||||
|
||||
|
||||
#
|
||||
# Device bay templates
|
||||
#
|
||||
|
||||
class DeviceBayTemplateCreateView(PermissionRequiredMixin, ComponentCreateView):
|
||||
permission_required = 'dcim.add_devicebaytemplate'
|
||||
parent_model = DeviceType
|
||||
parent_field = 'device_type'
|
||||
model = DeviceBayTemplate
|
||||
form = forms.DeviceBayTemplateCreateForm
|
||||
model_form = forms.DeviceBayTemplateForm
|
||||
@@ -888,10 +963,21 @@ class DeviceBayTemplateEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
model_form = forms.DeviceBayTemplateForm
|
||||
|
||||
|
||||
class DeviceBayTemplateDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
permission_required = 'dcim.delete_devicebaytemplate'
|
||||
model = DeviceBayTemplate
|
||||
|
||||
|
||||
# class DeviceBayTemplateBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
# permission_required = 'dcim.change_devicebaytemplate'
|
||||
# queryset = DeviceBayTemplate.objects.all()
|
||||
# table = tables.DeviceBayTemplateTable
|
||||
# form = forms.DeviceBayTemplateBulkEditForm
|
||||
|
||||
|
||||
class DeviceBayTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_devicebaytemplate'
|
||||
queryset = DeviceBayTemplate.objects.all()
|
||||
parent_model = DeviceType
|
||||
table = tables.DeviceBayTemplateTable
|
||||
|
||||
|
||||
@@ -903,7 +989,6 @@ class DeviceRoleListView(PermissionRequiredMixin, ObjectListView):
|
||||
permission_required = 'dcim.view_devicerole'
|
||||
queryset = DeviceRole.objects.all()
|
||||
table = tables.DeviceRoleTable
|
||||
template_name = 'dcim/devicerole_list.html'
|
||||
|
||||
|
||||
class DeviceRoleCreateView(PermissionRequiredMixin, ObjectEditView):
|
||||
@@ -939,7 +1024,6 @@ class PlatformListView(PermissionRequiredMixin, ObjectListView):
|
||||
permission_required = 'dcim.view_platform'
|
||||
queryset = Platform.objects.all()
|
||||
table = tables.PlatformTable
|
||||
template_name = 'dcim/platform_list.html'
|
||||
|
||||
|
||||
class PlatformCreateView(PermissionRequiredMixin, ObjectEditView):
|
||||
@@ -1098,7 +1182,7 @@ class DeviceLLDPNeighborsView(PermissionRequiredMixin, View):
|
||||
def get(self, request, pk):
|
||||
|
||||
device = get_object_or_404(Device, pk=pk)
|
||||
interfaces = device.vc_interfaces.connectable().prefetch_related(
|
||||
interfaces = device.vc_interfaces.exclude(type__in=NONCONNECTABLE_IFACE_TYPES).prefetch_related(
|
||||
'_connected_interface__device'
|
||||
)
|
||||
|
||||
@@ -1200,13 +1284,11 @@ class ConsolePortListView(PermissionRequiredMixin, ObjectListView):
|
||||
filterset = filters.ConsolePortFilterSet
|
||||
filterset_form = forms.ConsolePortFilterForm
|
||||
table = tables.ConsolePortDetailTable
|
||||
template_name = 'dcim/device_component_list.html'
|
||||
action_buttons = ('import', 'export')
|
||||
|
||||
|
||||
class ConsolePortCreateView(PermissionRequiredMixin, ComponentCreateView):
|
||||
permission_required = 'dcim.add_consoleport'
|
||||
parent_model = Device
|
||||
parent_field = 'device'
|
||||
model = ConsolePort
|
||||
form = forms.ConsolePortCreateForm
|
||||
model_form = forms.ConsolePortForm
|
||||
@@ -1231,11 +1313,18 @@ class ConsolePortBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
default_return_url = 'dcim:consoleport_list'
|
||||
|
||||
|
||||
class ConsolePortBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'dcim.change_consoleport'
|
||||
queryset = ConsolePort.objects.all()
|
||||
table = tables.ConsolePortTable
|
||||
form = forms.ConsolePortBulkEditForm
|
||||
|
||||
|
||||
class ConsolePortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_consoleport'
|
||||
queryset = ConsolePort.objects.all()
|
||||
parent_model = Device
|
||||
table = tables.ConsolePortTable
|
||||
default_return_url = 'dcim:consoleport_list'
|
||||
|
||||
|
||||
#
|
||||
@@ -1248,13 +1337,11 @@ class ConsoleServerPortListView(PermissionRequiredMixin, ObjectListView):
|
||||
filterset = filters.ConsoleServerPortFilterSet
|
||||
filterset_form = forms.ConsoleServerPortFilterForm
|
||||
table = tables.ConsoleServerPortDetailTable
|
||||
template_name = 'dcim/device_component_list.html'
|
||||
action_buttons = ('import', 'export')
|
||||
|
||||
|
||||
class ConsoleServerPortCreateView(PermissionRequiredMixin, ComponentCreateView):
|
||||
permission_required = 'dcim.add_consoleserverport'
|
||||
parent_model = Device
|
||||
parent_field = 'device'
|
||||
model = ConsoleServerPort
|
||||
form = forms.ConsoleServerPortCreateForm
|
||||
model_form = forms.ConsoleServerPortForm
|
||||
@@ -1282,7 +1369,6 @@ class ConsoleServerPortBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
class ConsoleServerPortBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'dcim.change_consoleserverport'
|
||||
queryset = ConsoleServerPort.objects.all()
|
||||
parent_model = Device
|
||||
table = tables.ConsoleServerPortTable
|
||||
form = forms.ConsoleServerPortBulkEditForm
|
||||
|
||||
@@ -1302,8 +1388,8 @@ class ConsoleServerPortBulkDisconnectView(PermissionRequiredMixin, BulkDisconnec
|
||||
class ConsoleServerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_consoleserverport'
|
||||
queryset = ConsoleServerPort.objects.all()
|
||||
parent_model = Device
|
||||
table = tables.ConsoleServerPortTable
|
||||
default_return_url = 'dcim:consoleserverport_list'
|
||||
|
||||
|
||||
#
|
||||
@@ -1316,13 +1402,11 @@ class PowerPortListView(PermissionRequiredMixin, ObjectListView):
|
||||
filterset = filters.PowerPortFilterSet
|
||||
filterset_form = forms.PowerPortFilterForm
|
||||
table = tables.PowerPortDetailTable
|
||||
template_name = 'dcim/device_component_list.html'
|
||||
action_buttons = ('import', 'export')
|
||||
|
||||
|
||||
class PowerPortCreateView(PermissionRequiredMixin, ComponentCreateView):
|
||||
permission_required = 'dcim.add_powerport'
|
||||
parent_model = Device
|
||||
parent_field = 'device'
|
||||
model = PowerPort
|
||||
form = forms.PowerPortCreateForm
|
||||
model_form = forms.PowerPortForm
|
||||
@@ -1347,11 +1431,18 @@ class PowerPortBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
default_return_url = 'dcim:powerport_list'
|
||||
|
||||
|
||||
class PowerPortBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'dcim.change_powerport'
|
||||
queryset = PowerPort.objects.all()
|
||||
table = tables.PowerPortTable
|
||||
form = forms.PowerPortBulkEditForm
|
||||
|
||||
|
||||
class PowerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_powerport'
|
||||
queryset = PowerPort.objects.all()
|
||||
parent_model = Device
|
||||
table = tables.PowerPortTable
|
||||
default_return_url = 'dcim:powerport_list'
|
||||
|
||||
|
||||
#
|
||||
@@ -1364,13 +1455,11 @@ class PowerOutletListView(PermissionRequiredMixin, ObjectListView):
|
||||
filterset = filters.PowerOutletFilterSet
|
||||
filterset_form = forms.PowerOutletFilterForm
|
||||
table = tables.PowerOutletDetailTable
|
||||
template_name = 'dcim/device_component_list.html'
|
||||
action_buttons = ('import', 'export')
|
||||
|
||||
|
||||
class PowerOutletCreateView(PermissionRequiredMixin, ComponentCreateView):
|
||||
permission_required = 'dcim.add_poweroutlet'
|
||||
parent_model = Device
|
||||
parent_field = 'device'
|
||||
model = PowerOutlet
|
||||
form = forms.PowerOutletCreateForm
|
||||
model_form = forms.PowerOutletForm
|
||||
@@ -1398,7 +1487,6 @@ class PowerOutletBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
class PowerOutletBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'dcim.change_poweroutlet'
|
||||
queryset = PowerOutlet.objects.all()
|
||||
parent_model = Device
|
||||
table = tables.PowerOutletTable
|
||||
form = forms.PowerOutletBulkEditForm
|
||||
|
||||
@@ -1418,8 +1506,8 @@ class PowerOutletBulkDisconnectView(PermissionRequiredMixin, BulkDisconnectView)
|
||||
class PowerOutletBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_poweroutlet'
|
||||
queryset = PowerOutlet.objects.all()
|
||||
parent_model = Device
|
||||
table = tables.PowerOutletTable
|
||||
default_return_url = 'dcim:poweroutlet_list'
|
||||
|
||||
|
||||
#
|
||||
@@ -1432,7 +1520,7 @@ class InterfaceListView(PermissionRequiredMixin, ObjectListView):
|
||||
filterset = filters.InterfaceFilterSet
|
||||
filterset_form = forms.InterfaceFilterForm
|
||||
table = tables.InterfaceDetailTable
|
||||
template_name = 'dcim/device_component_list.html'
|
||||
action_buttons = ('import', 'export')
|
||||
|
||||
|
||||
class InterfaceView(PermissionRequiredMixin, View):
|
||||
@@ -1473,8 +1561,6 @@ class InterfaceView(PermissionRequiredMixin, View):
|
||||
|
||||
class InterfaceCreateView(PermissionRequiredMixin, ComponentCreateView):
|
||||
permission_required = 'dcim.add_interface'
|
||||
parent_model = Device
|
||||
parent_field = 'device'
|
||||
model = Interface
|
||||
form = forms.InterfaceCreateForm
|
||||
model_form = forms.InterfaceForm
|
||||
@@ -1503,7 +1589,6 @@ class InterfaceBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
class InterfaceBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'dcim.change_interface'
|
||||
queryset = Interface.objects.all()
|
||||
parent_model = Device
|
||||
table = tables.InterfaceTable
|
||||
form = forms.InterfaceBulkEditForm
|
||||
|
||||
@@ -1523,8 +1608,8 @@ class InterfaceBulkDisconnectView(PermissionRequiredMixin, BulkDisconnectView):
|
||||
class InterfaceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_interface'
|
||||
queryset = Interface.objects.all()
|
||||
parent_model = Device
|
||||
table = tables.InterfaceTable
|
||||
default_return_url = 'dcim:interface_list'
|
||||
|
||||
|
||||
#
|
||||
@@ -1537,13 +1622,11 @@ class FrontPortListView(PermissionRequiredMixin, ObjectListView):
|
||||
filterset = filters.FrontPortFilterSet
|
||||
filterset_form = forms.FrontPortFilterForm
|
||||
table = tables.FrontPortDetailTable
|
||||
template_name = 'dcim/device_component_list.html'
|
||||
action_buttons = ('import', 'export')
|
||||
|
||||
|
||||
class FrontPortCreateView(PermissionRequiredMixin, ComponentCreateView):
|
||||
permission_required = 'dcim.add_frontport'
|
||||
parent_model = Device
|
||||
parent_field = 'device'
|
||||
model = FrontPort
|
||||
form = forms.FrontPortCreateForm
|
||||
model_form = forms.FrontPortForm
|
||||
@@ -1571,7 +1654,6 @@ class FrontPortBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
class FrontPortBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'dcim.change_frontport'
|
||||
queryset = FrontPort.objects.all()
|
||||
parent_model = Device
|
||||
table = tables.FrontPortTable
|
||||
form = forms.FrontPortBulkEditForm
|
||||
|
||||
@@ -1591,8 +1673,8 @@ class FrontPortBulkDisconnectView(PermissionRequiredMixin, BulkDisconnectView):
|
||||
class FrontPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_frontport'
|
||||
queryset = FrontPort.objects.all()
|
||||
parent_model = Device
|
||||
table = tables.FrontPortTable
|
||||
default_return_url = 'dcim:frontport_list'
|
||||
|
||||
|
||||
#
|
||||
@@ -1605,13 +1687,11 @@ class RearPortListView(PermissionRequiredMixin, ObjectListView):
|
||||
filterset = filters.RearPortFilterSet
|
||||
filterset_form = forms.RearPortFilterForm
|
||||
table = tables.RearPortDetailTable
|
||||
template_name = 'dcim/device_component_list.html'
|
||||
action_buttons = ('import', 'export')
|
||||
|
||||
|
||||
class RearPortCreateView(PermissionRequiredMixin, ComponentCreateView):
|
||||
permission_required = 'dcim.add_rearport'
|
||||
parent_model = Device
|
||||
parent_field = 'device'
|
||||
model = RearPort
|
||||
form = forms.RearPortCreateForm
|
||||
model_form = forms.RearPortForm
|
||||
@@ -1639,7 +1719,6 @@ class RearPortBulkImportView(PermissionRequiredMixin, BulkImportView):
|
||||
class RearPortBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
permission_required = 'dcim.change_rearport'
|
||||
queryset = RearPort.objects.all()
|
||||
parent_model = Device
|
||||
table = tables.RearPortTable
|
||||
form = forms.RearPortBulkEditForm
|
||||
|
||||
@@ -1659,8 +1738,8 @@ class RearPortBulkDisconnectView(PermissionRequiredMixin, BulkDisconnectView):
|
||||
class RearPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_rearport'
|
||||
queryset = RearPort.objects.all()
|
||||
parent_model = Device
|
||||
table = tables.RearPortTable
|
||||
default_return_url = 'dcim:rearport_list'
|
||||
|
||||
|
||||
#
|
||||
@@ -1675,13 +1754,11 @@ class DeviceBayListView(PermissionRequiredMixin, ObjectListView):
|
||||
filterset = filters.DeviceBayFilterSet
|
||||
filterset_form = forms.DeviceBayFilterForm
|
||||
table = tables.DeviceBayDetailTable
|
||||
template_name = 'dcim/device_component_list.html'
|
||||
action_buttons = ('import', 'export')
|
||||
|
||||
|
||||
class DeviceBayCreateView(PermissionRequiredMixin, ComponentCreateView):
|
||||
permission_required = 'dcim.add_devicebay'
|
||||
parent_model = Device
|
||||
parent_field = 'device'
|
||||
model = DeviceBay
|
||||
form = forms.DeviceBayCreateForm
|
||||
model_form = forms.DeviceBayForm
|
||||
@@ -1784,8 +1861,8 @@ class DeviceBayBulkRenameView(PermissionRequiredMixin, BulkRenameView):
|
||||
class DeviceBayBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_devicebay'
|
||||
queryset = DeviceBay.objects.all()
|
||||
parent_model = Device
|
||||
table = tables.DeviceBayTable
|
||||
default_return_url = 'dcim:devicebay_list'
|
||||
|
||||
|
||||
#
|
||||
@@ -1876,7 +1953,7 @@ class CableListView(PermissionRequiredMixin, ObjectListView):
|
||||
filterset = filters.CableFilterSet
|
||||
filterset_form = forms.CableFilterForm
|
||||
table = tables.CableTable
|
||||
template_name = 'dcim/cable_list.html'
|
||||
action_buttons = ('import', 'export')
|
||||
|
||||
|
||||
class CableView(PermissionRequiredMixin, View):
|
||||
@@ -2148,7 +2225,7 @@ class InventoryItemListView(PermissionRequiredMixin, ObjectListView):
|
||||
filterset = filters.InventoryItemFilterSet
|
||||
filterset_form = forms.InventoryItemFilterForm
|
||||
table = tables.InventoryItemTable
|
||||
template_name = 'dcim/inventoryitem_list.html'
|
||||
action_buttons = ('import', 'export')
|
||||
|
||||
|
||||
class InventoryItemEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
@@ -2156,13 +2233,13 @@ class InventoryItemEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
model = InventoryItem
|
||||
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):
|
||||
return reverse('dcim:device_inventory', kwargs={'pk': obj.device.pk})
|
||||
class InventoryItemCreateView(PermissionRequiredMixin, ComponentCreateView):
|
||||
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):
|
||||
@@ -2204,7 +2281,7 @@ class VirtualChassisListView(PermissionRequiredMixin, ObjectListView):
|
||||
table = tables.VirtualChassisTable
|
||||
filterset = filters.VirtualChassisFilterSet
|
||||
filterset_form = forms.VirtualChassisFilterForm
|
||||
template_name = 'dcim/virtualchassis_list.html'
|
||||
action_buttons = ('export',)
|
||||
|
||||
|
||||
class VirtualChassisCreateView(PermissionRequiredMixin, View):
|
||||
@@ -2448,7 +2525,6 @@ class PowerPanelListView(PermissionRequiredMixin, ObjectListView):
|
||||
filterset = filters.PowerPanelFilterSet
|
||||
filterset_form = forms.PowerPanelFilterForm
|
||||
table = tables.PowerPanelTable
|
||||
template_name = 'dcim/powerpanel_list.html'
|
||||
|
||||
|
||||
class PowerPanelView(PermissionRequiredMixin, View):
|
||||
@@ -2517,7 +2593,6 @@ class PowerFeedListView(PermissionRequiredMixin, ObjectListView):
|
||||
filterset = filters.PowerFeedFilterSet
|
||||
filterset_form = forms.PowerFeedFilterForm
|
||||
table = tables.PowerFeedTable
|
||||
template_name = 'dcim/powerfeed_list.html'
|
||||
|
||||
|
||||
class PowerFeedView(PermissionRequiredMixin, View):
|
||||
|
||||
@@ -26,7 +26,7 @@ class WebhookForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = Webhook
|
||||
exclude = []
|
||||
exclude = ()
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -38,13 +38,35 @@ class WebhookForm(forms.ModelForm):
|
||||
@admin.register(Webhook, site=admin_site)
|
||||
class WebhookAdmin(admin.ModelAdmin):
|
||||
list_display = [
|
||||
'name', 'models', 'payload_url', 'http_content_type', 'enabled', 'type_create', 'type_update',
|
||||
'type_delete', 'ssl_verification',
|
||||
'name', 'models', 'payload_url', 'http_content_type', 'enabled', 'type_create', 'type_update', 'type_delete',
|
||||
'ssl_verification',
|
||||
]
|
||||
list_filter = [
|
||||
'enabled', 'type_create', 'type_update', 'type_delete', 'obj_type',
|
||||
]
|
||||
form = WebhookForm
|
||||
fieldsets = (
|
||||
(None, {
|
||||
'fields': (
|
||||
'name', 'obj_type', 'enabled',
|
||||
)
|
||||
}),
|
||||
('Events', {
|
||||
'fields': (
|
||||
'type_create', 'type_update', 'type_delete',
|
||||
)
|
||||
}),
|
||||
('HTTP Request', {
|
||||
'fields': (
|
||||
'payload_url', 'http_method', 'http_content_type', 'additional_headers', 'body_template', 'secret',
|
||||
)
|
||||
}),
|
||||
('SSL', {
|
||||
'fields': (
|
||||
'ssl_verification', 'ca_file_path',
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
def models(self, obj):
|
||||
return ', '.join([ct.name for ct in obj.obj_type.all()])
|
||||
|
||||
@@ -40,10 +40,14 @@ class GraphSerializer(ValidatedModelSerializer):
|
||||
|
||||
|
||||
class RenderedGraphSerializer(serializers.ModelSerializer):
|
||||
embed_url = serializers.SerializerMethodField()
|
||||
embed_link = serializers.SerializerMethodField()
|
||||
embed_url = serializers.SerializerMethodField(
|
||||
read_only=True
|
||||
)
|
||||
embed_link = serializers.SerializerMethodField(
|
||||
read_only=True
|
||||
)
|
||||
type = ContentTypeField(
|
||||
queryset=ContentType.objects.all()
|
||||
read_only=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
@@ -62,6 +66,9 @@ class RenderedGraphSerializer(serializers.ModelSerializer):
|
||||
#
|
||||
|
||||
class ExportTemplateSerializer(ValidatedModelSerializer):
|
||||
content_type = ContentTypeField(
|
||||
queryset=ContentType.objects.filter(EXPORTTEMPLATE_MODELS),
|
||||
)
|
||||
template_language = ChoiceField(
|
||||
choices=TemplateLanguageChoices,
|
||||
default=TemplateLanguageChoices.LANGUAGE_JINJA2
|
||||
|
||||
@@ -15,34 +15,34 @@ router = routers.DefaultRouter()
|
||||
router.APIRootView = ExtrasRootView
|
||||
|
||||
# Field choices
|
||||
router.register(r'_choices', views.ExtrasFieldChoicesViewSet, basename='field-choice')
|
||||
router.register('_choices', views.ExtrasFieldChoicesViewSet, basename='field-choice')
|
||||
|
||||
# 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
|
||||
router.register(r'graphs', views.GraphViewSet)
|
||||
router.register('graphs', views.GraphViewSet)
|
||||
|
||||
# Export templates
|
||||
router.register(r'export-templates', views.ExportTemplateViewSet)
|
||||
router.register('export-templates', views.ExportTemplateViewSet)
|
||||
|
||||
# Tags
|
||||
router.register(r'tags', views.TagViewSet)
|
||||
router.register('tags', views.TagViewSet)
|
||||
|
||||
# Image attachments
|
||||
router.register(r'image-attachments', views.ImageAttachmentViewSet)
|
||||
router.register('image-attachments', views.ImageAttachmentViewSet)
|
||||
|
||||
# Config contexts
|
||||
router.register(r'config-contexts', views.ConfigContextViewSet)
|
||||
router.register('config-contexts', views.ConfigContextViewSet)
|
||||
|
||||
# Reports
|
||||
router.register(r'reports', views.ReportViewSet, basename='report')
|
||||
router.register('reports', views.ReportViewSet, basename='report')
|
||||
|
||||
# Scripts
|
||||
router.register(r'scripts', views.ScriptViewSet, basename='script')
|
||||
router.register('scripts', views.ScriptViewSet, basename='script')
|
||||
|
||||
# Change logging
|
||||
router.register(r'object-changes', views.ObjectChangeViewSet)
|
||||
router.register('object-changes', views.ObjectChangeViewSet)
|
||||
|
||||
app_name = 'extras-api'
|
||||
urlpatterns = router.urls
|
||||
|
||||
@@ -1,28 +1,8 @@
|
||||
from django.apps import AppConfig
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
import redis
|
||||
|
||||
|
||||
class ExtrasConfig(AppConfig):
|
||||
name = "extras"
|
||||
|
||||
def ready(self):
|
||||
|
||||
import extras.signals
|
||||
|
||||
# Check that we can connect to the configured Redis database.
|
||||
try:
|
||||
rs = redis.Redis(
|
||||
host=settings.WEBHOOKS_REDIS_HOST,
|
||||
port=settings.WEBHOOKS_REDIS_PORT,
|
||||
db=settings.WEBHOOKS_REDIS_DATABASE,
|
||||
password=settings.WEBHOOKS_REDIS_PASSWORD or None,
|
||||
ssl=settings.WEBHOOKS_REDIS_SSL,
|
||||
)
|
||||
rs.ping()
|
||||
except redis.exceptions.ConnectionError:
|
||||
raise ImproperlyConfigured(
|
||||
"Unable to connect to the Redis database. Check that the Redis configuration has been defined in "
|
||||
"configuration.py."
|
||||
)
|
||||
|
||||
@@ -124,17 +124,18 @@ class TemplateLanguageChoices(ChoiceSet):
|
||||
# Webhooks
|
||||
#
|
||||
|
||||
class WebhookContentTypeChoices(ChoiceSet):
|
||||
class WebhookHttpMethodChoices(ChoiceSet):
|
||||
|
||||
CONTENTTYPE_JSON = 'application/json'
|
||||
CONTENTTYPE_FORMDATA = 'application/x-www-form-urlencoded'
|
||||
METHOD_GET = 'GET'
|
||||
METHOD_POST = 'POST'
|
||||
METHOD_PUT = 'PUT'
|
||||
METHOD_PATCH = 'PATCH'
|
||||
METHOD_DELETE = 'DELETE'
|
||||
|
||||
CHOICES = (
|
||||
(CONTENTTYPE_JSON, 'JSON'),
|
||||
(CONTENTTYPE_FORMDATA, 'Form data'),
|
||||
(METHOD_GET, 'GET'),
|
||||
(METHOD_POST, 'POST'),
|
||||
(METHOD_PUT, 'PUT'),
|
||||
(METHOD_PATCH, 'PATCH'),
|
||||
(METHOD_DELETE, 'DELETE'),
|
||||
)
|
||||
|
||||
LEGACY_MAP = {
|
||||
CONTENTTYPE_JSON: 1,
|
||||
CONTENTTYPE_FORMDATA: 2,
|
||||
}
|
||||
|
||||
@@ -138,6 +138,8 @@ LOG_LEVEL_CODES = {
|
||||
LOG_FAILURE: 'failure',
|
||||
}
|
||||
|
||||
HTTP_CONTENT_TYPE_JSON = 'application/json'
|
||||
|
||||
# Models which support registered webhooks
|
||||
WEBHOOK_MODELS = Q(
|
||||
Q(app_label='circuits', model__in=[
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
from django import forms
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from mptt.forms import TreeNodeMultipleChoiceField
|
||||
from taggit.forms import TagField
|
||||
|
||||
from dcim.models import DeviceRole, Platform, Region, Site
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from utilities.forms import (
|
||||
add_blank_choice, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ColorSelect,
|
||||
CommentField, ContentTypeSelect, DateTimePicker, FilterChoiceField, JSONField, SlugField, StaticSelect2,
|
||||
BOOLEAN_WITH_BLANK_CHOICES,
|
||||
CommentField, ContentTypeSelect, DateTimePicker, DynamicModelMultipleChoiceField, JSONField, SlugField,
|
||||
StaticSelect2, StaticSelect2Multiple, BOOLEAN_WITH_BLANK_CHOICES,
|
||||
)
|
||||
from virtualization.models import Cluster, ClusterGroup
|
||||
from .choices import *
|
||||
@@ -133,7 +134,8 @@ class CustomFieldFilterForm(forms.Form):
|
||||
filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED
|
||||
)
|
||||
for cf in custom_fields:
|
||||
self.fields[cf.name] = cf.to_form_field(set_initial=True, enforce_required=False)
|
||||
field_name = 'cf_{}'.format(cf.name)
|
||||
self.fields[field_name] = cf.to_form_field(set_initial=True, enforce_required=False)
|
||||
|
||||
|
||||
#
|
||||
@@ -189,7 +191,61 @@ class TagBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
#
|
||||
|
||||
class ConfigContextForm(BootstrapMixin, forms.ModelForm):
|
||||
tags = forms.ModelMultipleChoiceField(
|
||||
regions = TreeNodeMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
widget=StaticSelect2Multiple()
|
||||
)
|
||||
sites = DynamicModelMultipleChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
required=False,
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/dcim/sites/"
|
||||
)
|
||||
)
|
||||
roles = DynamicModelMultipleChoiceField(
|
||||
queryset=DeviceRole.objects.all(),
|
||||
required=False,
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/dcim/device-roles/"
|
||||
)
|
||||
)
|
||||
platforms = DynamicModelMultipleChoiceField(
|
||||
queryset=Platform.objects.all(),
|
||||
required=False,
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/dcim/platforms/"
|
||||
)
|
||||
)
|
||||
cluster_groups = DynamicModelMultipleChoiceField(
|
||||
queryset=ClusterGroup.objects.all(),
|
||||
required=False,
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/virtualization/cluster-groups/"
|
||||
)
|
||||
)
|
||||
clusters = DynamicModelMultipleChoiceField(
|
||||
queryset=Cluster.objects.all(),
|
||||
required=False,
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/virtualization/clusters/"
|
||||
)
|
||||
)
|
||||
tenant_groups = DynamicModelMultipleChoiceField(
|
||||
queryset=TenantGroup.objects.all(),
|
||||
required=False,
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/tenancy/tenant-groups/"
|
||||
)
|
||||
)
|
||||
tenants = DynamicModelMultipleChoiceField(
|
||||
queryset=Tenant.objects.all(),
|
||||
required=False,
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/tenancy/tenants/"
|
||||
)
|
||||
)
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
to_field_name='slug',
|
||||
required=False,
|
||||
@@ -203,36 +259,10 @@ class ConfigContextForm(BootstrapMixin, forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = ConfigContext
|
||||
fields = [
|
||||
fields = (
|
||||
'name', 'weight', 'description', 'is_active', 'regions', 'sites', 'roles', 'platforms', 'cluster_groups',
|
||||
'clusters', 'tenant_groups', 'tenants', 'tags', 'data',
|
||||
]
|
||||
widgets = {
|
||||
'regions': APISelectMultiple(
|
||||
api_url="/api/dcim/regions/"
|
||||
),
|
||||
'sites': APISelectMultiple(
|
||||
api_url="/api/dcim/sites/"
|
||||
),
|
||||
'roles': APISelectMultiple(
|
||||
api_url="/api/dcim/device-roles/"
|
||||
),
|
||||
'platforms': APISelectMultiple(
|
||||
api_url="/api/dcim/platforms/"
|
||||
),
|
||||
'cluster_groups': APISelectMultiple(
|
||||
api_url="/api/virtualization/cluster-groups/"
|
||||
),
|
||||
'clusters': APISelectMultiple(
|
||||
api_url="/api/virtualization/clusters/"
|
||||
),
|
||||
'tenant_groups': APISelectMultiple(
|
||||
api_url="/api/tenancy/tenant-groups/"
|
||||
),
|
||||
'tenants': APISelectMultiple(
|
||||
api_url="/api/tenancy/tenants/"
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class ConfigContextBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
@@ -264,72 +294,81 @@ class ConfigContextFilterForm(BootstrapMixin, forms.Form):
|
||||
required=False,
|
||||
label='Search'
|
||||
)
|
||||
region = FilterChoiceField(
|
||||
region = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
to_field_name='slug',
|
||||
required=False,
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/dcim/regions/",
|
||||
value_field="slug",
|
||||
)
|
||||
)
|
||||
site = FilterChoiceField(
|
||||
site = DynamicModelMultipleChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
required=False,
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/dcim/sites/",
|
||||
value_field="slug",
|
||||
)
|
||||
)
|
||||
role = FilterChoiceField(
|
||||
role = DynamicModelMultipleChoiceField(
|
||||
queryset=DeviceRole.objects.all(),
|
||||
to_field_name='slug',
|
||||
required=False,
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/dcim/device-roles/",
|
||||
value_field="slug",
|
||||
)
|
||||
)
|
||||
platform = FilterChoiceField(
|
||||
platform = DynamicModelMultipleChoiceField(
|
||||
queryset=Platform.objects.all(),
|
||||
to_field_name='slug',
|
||||
required=False,
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/dcim/platforms/",
|
||||
value_field="slug",
|
||||
)
|
||||
)
|
||||
cluster_group = FilterChoiceField(
|
||||
cluster_group = DynamicModelMultipleChoiceField(
|
||||
queryset=ClusterGroup.objects.all(),
|
||||
to_field_name='slug',
|
||||
required=False,
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/virtualization/cluster-groups/",
|
||||
value_field="slug",
|
||||
)
|
||||
)
|
||||
cluster_id = FilterChoiceField(
|
||||
cluster_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Cluster.objects.all(),
|
||||
required=False,
|
||||
label='Cluster',
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/virtualization/clusters/",
|
||||
)
|
||||
)
|
||||
tenant_group = FilterChoiceField(
|
||||
tenant_group = DynamicModelMultipleChoiceField(
|
||||
queryset=TenantGroup.objects.all(),
|
||||
to_field_name='slug',
|
||||
required=False,
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/tenancy/tenant-groups/",
|
||||
value_field="slug",
|
||||
)
|
||||
)
|
||||
tenant = FilterChoiceField(
|
||||
tenant = DynamicModelMultipleChoiceField(
|
||||
queryset=Tenant.objects.all(),
|
||||
to_field_name='slug',
|
||||
required=False,
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/tenancy/tenants/",
|
||||
value_field="slug",
|
||||
)
|
||||
)
|
||||
tag = FilterChoiceField(
|
||||
tag = DynamicModelMultipleChoiceField(
|
||||
queryset=Tag.objects.all(),
|
||||
to_field_name='slug',
|
||||
required=False,
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/extras/tags/",
|
||||
value_field="slug",
|
||||
@@ -386,11 +425,14 @@ class ObjectChangeFilterForm(BootstrapMixin, forms.Form):
|
||||
)
|
||||
action = forms.ChoiceField(
|
||||
choices=add_blank_choice(ObjectChangeActionChoices),
|
||||
required=False
|
||||
required=False,
|
||||
widget=StaticSelect2()
|
||||
)
|
||||
# TODO: Convert to DynamicModelMultipleChoiceField once we have an API endpoint for users
|
||||
user = forms.ModelChoiceField(
|
||||
queryset=User.objects.order_by('username'),
|
||||
required=False
|
||||
required=False,
|
||||
widget=StaticSelect2()
|
||||
)
|
||||
changed_object_type = forms.ModelChoiceField(
|
||||
queryset=ContentType.objects.order_by('model'),
|
||||
|
||||
111
netbox/extras/management/commands/renaturalize.py
Normal file
111
netbox/extras/management/commands/renaturalize.py
Normal file
@@ -0,0 +1,111 @@
|
||||
from django.apps import apps
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
|
||||
from utilities.fields import NaturalOrderingField
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Recalculate natural ordering values for the specified models"
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
'args', metavar='app_label.ModelName', nargs='*',
|
||||
help='One or more specific models (each prefixed with its app_label) to renaturalize',
|
||||
)
|
||||
|
||||
def _get_models(self, names):
|
||||
"""
|
||||
Compile a list of models to be renaturalized. If no names are specified, all models which have one or more
|
||||
NaturalOrderingFields will be included.
|
||||
"""
|
||||
models = []
|
||||
|
||||
if names:
|
||||
# Collect all NaturalOrderingFields present on the specified models
|
||||
for name in names:
|
||||
try:
|
||||
app_label, model_name = name.split('.')
|
||||
except ValueError:
|
||||
raise CommandError(
|
||||
"Invalid format: {}. Models must be specified in the form app_label.ModelName.".format(name)
|
||||
)
|
||||
try:
|
||||
app_config = apps.get_app_config(app_label)
|
||||
except LookupError as e:
|
||||
raise CommandError(str(e))
|
||||
try:
|
||||
model = app_config.get_model(model_name)
|
||||
except LookupError:
|
||||
raise CommandError("Unknown model: {}.{}".format(app_label, model_name))
|
||||
fields = [
|
||||
field for field in model._meta.concrete_fields if type(field) is NaturalOrderingField
|
||||
]
|
||||
if not fields:
|
||||
raise CommandError(
|
||||
"Invalid model: {}.{} does not employ natural ordering".format(app_label, model_name)
|
||||
)
|
||||
models.append(
|
||||
(model, fields)
|
||||
)
|
||||
|
||||
else:
|
||||
# Find *all* models with NaturalOrderingFields
|
||||
for app_config in apps.get_app_configs():
|
||||
for model in app_config.models.values():
|
||||
fields = [
|
||||
field for field in model._meta.concrete_fields if type(field) is NaturalOrderingField
|
||||
]
|
||||
if fields:
|
||||
models.append(
|
||||
(model, fields)
|
||||
)
|
||||
|
||||
return models
|
||||
|
||||
def handle(self, *args, **options):
|
||||
|
||||
models = self._get_models(args)
|
||||
|
||||
if options['verbosity']:
|
||||
self.stdout.write("Renaturalizing {} models.".format(len(models)))
|
||||
|
||||
for model, fields in models:
|
||||
for field in fields:
|
||||
target_field = field.target_field
|
||||
naturalize = field.naturalize_function
|
||||
count = 0
|
||||
|
||||
# Print the model and field name
|
||||
if options['verbosity']:
|
||||
self.stdout.write(
|
||||
"{}.{} ({})... ".format(model._meta.label, field.target_field, field.name),
|
||||
ending='\n' if options['verbosity'] >= 2 else ''
|
||||
)
|
||||
self.stdout.flush()
|
||||
|
||||
# Find all unique values for the field
|
||||
queryset = model.objects.values_list(target_field, flat=True).order_by(target_field).distinct()
|
||||
for value in queryset:
|
||||
naturalized_value = naturalize(value, max_length=field.max_length)
|
||||
|
||||
if options['verbosity'] >= 2:
|
||||
self.stdout.write(" {} -> {}".format(value, naturalized_value), ending='')
|
||||
self.stdout.flush()
|
||||
|
||||
# Update each unique field value in bulk
|
||||
changed = model.objects.filter(name=value).update(**{field.name: naturalized_value})
|
||||
|
||||
if options['verbosity'] >= 2:
|
||||
self.stdout.write(" ({})".format(changed))
|
||||
count += changed
|
||||
|
||||
# Print the total count of alterations for the field
|
||||
if options['verbosity'] >= 2:
|
||||
self.stdout.write(self.style.SUCCESS("{} {} updated ({} unique values)".format(
|
||||
count, model._meta.verbose_name_plural, queryset.count()
|
||||
)))
|
||||
elif options['verbosity']:
|
||||
self.stdout.write(self.style.SUCCESS(str(count)))
|
||||
|
||||
if options['verbosity']:
|
||||
self.stdout.write(self.style.SUCCESS("Done."))
|
||||
@@ -5,11 +5,14 @@ from copy import deepcopy
|
||||
from datetime import timedelta
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.db.models.signals import pre_delete, post_save
|
||||
from django.utils import timezone
|
||||
from django_prometheus.models import model_deletes, model_inserts, model_updates
|
||||
from redis.exceptions import RedisError
|
||||
|
||||
from extras.utils import is_taggable
|
||||
from utilities.api import is_api_request
|
||||
from utilities.querysets import DummyQuerySet
|
||||
from .choices import ObjectChangeActionChoices
|
||||
from .models import ObjectChange
|
||||
@@ -98,7 +101,12 @@ class ObjectChangeMiddleware(object):
|
||||
if not _thread_locals.changed_objects:
|
||||
return response
|
||||
|
||||
# Disconnect our receivers from the post_save and post_delete signals.
|
||||
post_save.disconnect(handle_changed_object, dispatch_uid='handle_changed_object')
|
||||
pre_delete.disconnect(handle_deleted_object, dispatch_uid='handle_deleted_object')
|
||||
|
||||
# Create records for any cached objects that were changed.
|
||||
redis_failed = False
|
||||
for instance, action in _thread_locals.changed_objects:
|
||||
|
||||
# Refresh cached custom field values
|
||||
@@ -114,7 +122,16 @@ class ObjectChangeMiddleware(object):
|
||||
objectchange.save()
|
||||
|
||||
# Enqueue webhooks
|
||||
enqueue_webhooks(instance, request.user, request.id, action)
|
||||
try:
|
||||
enqueue_webhooks(instance, request.user, request.id, action)
|
||||
except RedisError as e:
|
||||
if not redis_failed and not is_api_request(request):
|
||||
messages.error(
|
||||
request,
|
||||
"There was an error processing webhooks for this request. Check that the Redis service is "
|
||||
"running and reachable. The full error details were: {}".format(e)
|
||||
)
|
||||
redis_failed = True
|
||||
|
||||
# Increment metric counters
|
||||
if action == ObjectChangeActionChoices.ACTION_CREATE:
|
||||
|
||||
48
netbox/extras/migrations/0038_webhook_template_support.py
Normal file
48
netbox/extras/migrations/0038_webhook_template_support.py
Normal file
@@ -0,0 +1,48 @@
|
||||
import json
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def json_to_text(apps, schema_editor):
|
||||
"""
|
||||
Convert a JSON representation of HTTP headers to key-value pairs (one header per line)
|
||||
"""
|
||||
Webhook = apps.get_model('extras', 'Webhook')
|
||||
for webhook in Webhook.objects.exclude(additional_headers=''):
|
||||
data = json.loads(webhook.additional_headers)
|
||||
headers = ['{}: {}'.format(k, v) for k, v in data.items()]
|
||||
Webhook.objects.filter(pk=webhook.pk).update(additional_headers='\n'.join(headers))
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('extras', '0037_configcontexts_clusters'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='webhook',
|
||||
name='http_method',
|
||||
field=models.CharField(default='POST', max_length=30),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='webhook',
|
||||
name='body_template',
|
||||
field=models.TextField(blank=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='webhook',
|
||||
name='additional_headers',
|
||||
field=models.TextField(blank=True, default=''),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='webhook',
|
||||
name='http_content_type',
|
||||
field=models.CharField(default='application/json', max_length=100),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=json_to_text
|
||||
),
|
||||
]
|
||||
@@ -1,3 +1,4 @@
|
||||
import json
|
||||
from collections import OrderedDict
|
||||
from datetime import date
|
||||
|
||||
@@ -12,6 +13,7 @@ from django.http import HttpResponse
|
||||
from django.template import Template, Context
|
||||
from django.urls import reverse
|
||||
from django.utils.text import slugify
|
||||
from rest_framework.utils.encoders import JSONEncoder
|
||||
from taggit.models import TagBase, GenericTaggedItemBase
|
||||
|
||||
from utilities.fields import ColorField
|
||||
@@ -52,7 +54,6 @@ class Webhook(models.Model):
|
||||
delete in NetBox. The request will contain a representation of the object, which the remote application can act on.
|
||||
Each Webhook can be limited to firing only on certain actions or certain object types.
|
||||
"""
|
||||
|
||||
obj_type = models.ManyToManyField(
|
||||
to=ContentType,
|
||||
related_name='webhooks',
|
||||
@@ -81,17 +82,33 @@ class Webhook(models.Model):
|
||||
verbose_name='URL',
|
||||
help_text="A POST will be sent to this URL when the webhook is called."
|
||||
)
|
||||
http_content_type = models.CharField(
|
||||
max_length=50,
|
||||
choices=WebhookContentTypeChoices,
|
||||
default=WebhookContentTypeChoices.CONTENTTYPE_JSON,
|
||||
verbose_name='HTTP content type'
|
||||
enabled = models.BooleanField(
|
||||
default=True
|
||||
)
|
||||
additional_headers = JSONField(
|
||||
null=True,
|
||||
http_method = models.CharField(
|
||||
max_length=30,
|
||||
choices=WebhookHttpMethodChoices,
|
||||
default=WebhookHttpMethodChoices.METHOD_POST,
|
||||
verbose_name='HTTP method'
|
||||
)
|
||||
http_content_type = models.CharField(
|
||||
max_length=100,
|
||||
default=HTTP_CONTENT_TYPE_JSON,
|
||||
verbose_name='HTTP content type',
|
||||
help_text='The complete list of official content types is available '
|
||||
'<a href="https://www.iana.org/assignments/media-types/media-types.xhtml">here</a>.'
|
||||
)
|
||||
additional_headers = models.TextField(
|
||||
blank=True,
|
||||
help_text="User supplied headers which should be added to the request in addition to the HTTP content type. "
|
||||
"Headers are supplied as key/value pairs in a JSON object."
|
||||
help_text="User-supplied HTTP headers to be sent with the request in addition to the HTTP content type. "
|
||||
"Headers should be defined in the format <code>Name: Value</code>. Jinja2 template processing is "
|
||||
"support with the same context as the request body (below)."
|
||||
)
|
||||
body_template = models.TextField(
|
||||
blank=True,
|
||||
help_text='Jinja2 template for a custom request body. If blank, a JSON object representing the change will be '
|
||||
'included. Available context data includes: <code>event</code>, <code>model</code>, '
|
||||
'<code>timestamp</code>, <code>username</code>, <code>request_id</code>, and <code>data</code>.'
|
||||
)
|
||||
secret = models.CharField(
|
||||
max_length=255,
|
||||
@@ -101,9 +118,6 @@ class Webhook(models.Model):
|
||||
"the secret as the key. The secret is not transmitted in "
|
||||
"the request."
|
||||
)
|
||||
enabled = models.BooleanField(
|
||||
default=True
|
||||
)
|
||||
ssl_verification = models.BooleanField(
|
||||
default=True,
|
||||
verbose_name='SSL verification',
|
||||
@@ -126,9 +140,6 @@ class Webhook(models.Model):
|
||||
return self.name
|
||||
|
||||
def clean(self):
|
||||
"""
|
||||
Validate model
|
||||
"""
|
||||
if not self.type_create and not self.type_delete and not self.type_update:
|
||||
raise ValidationError(
|
||||
"You must select at least one type: create, update, and/or delete."
|
||||
@@ -136,14 +147,30 @@ class Webhook(models.Model):
|
||||
|
||||
if not self.ssl_verification and self.ca_file_path:
|
||||
raise ValidationError({
|
||||
'ca_file_path': 'Do not specify a CA certificate file if SSL verification is dissabled.'
|
||||
'ca_file_path': 'Do not specify a CA certificate file if SSL verification is disabled.'
|
||||
})
|
||||
|
||||
# Verify that JSON data is provided as an object
|
||||
if self.additional_headers and type(self.additional_headers) is not dict:
|
||||
raise ValidationError({
|
||||
'additional_headers': 'Header JSON data must be in object form. Example: {"X-API-KEY": "abc123"}'
|
||||
})
|
||||
def render_headers(self, context):
|
||||
"""
|
||||
Render additional_headers and return a dict of Header: Value pairs.
|
||||
"""
|
||||
if not self.additional_headers:
|
||||
return {}
|
||||
ret = {}
|
||||
data = render_jinja2(self.additional_headers, context)
|
||||
for line in data.splitlines():
|
||||
header, value = line.split(':')
|
||||
ret[header.strip()] = value.strip()
|
||||
return ret
|
||||
|
||||
def render_body(self, context):
|
||||
"""
|
||||
Render the body template, if defined. Otherwise, jump the context as a JSON object.
|
||||
"""
|
||||
if self.body_template:
|
||||
return render_jinja2(self.body_template, context)
|
||||
else:
|
||||
return json.dumps(context, cls=JSONEncoder)
|
||||
|
||||
|
||||
#
|
||||
|
||||
@@ -48,7 +48,7 @@ class ScriptVariable:
|
||||
"""
|
||||
form_field = forms.CharField
|
||||
|
||||
def __init__(self, label='', description='', default=None, required=True):
|
||||
def __init__(self, label='', description='', default=None, required=True, widget=None):
|
||||
|
||||
# Initialize field attributes
|
||||
if not hasattr(self, 'field_attrs'):
|
||||
@@ -59,19 +59,20 @@ class ScriptVariable:
|
||||
self.field_attrs['help_text'] = description
|
||||
if default:
|
||||
self.field_attrs['initial'] = default
|
||||
if widget:
|
||||
self.field_attrs['widget'] = widget
|
||||
self.field_attrs['required'] = required
|
||||
|
||||
# Initialize the list of optional validators if none have already been defined
|
||||
if 'validators' not in self.field_attrs:
|
||||
self.field_attrs['validators'] = []
|
||||
|
||||
def as_field(self):
|
||||
"""
|
||||
Render the variable as a Django form field.
|
||||
"""
|
||||
form_field = self.form_field(**self.field_attrs)
|
||||
if not isinstance(form_field.widget, forms.CheckboxInput):
|
||||
form_field.widget.attrs['class'] = 'form-control'
|
||||
if form_field.widget.attrs and 'class' in form_field.widget.attrs.keys():
|
||||
form_field.widget.attrs['class'] += ' form-control'
|
||||
else:
|
||||
form_field.widget.attrs['class'] = 'form-control'
|
||||
|
||||
return form_field
|
||||
|
||||
@@ -222,14 +223,12 @@ class IPNetworkVar(ScriptVariable):
|
||||
An IPv4 or IPv6 prefix.
|
||||
"""
|
||||
form_field = IPNetworkFormField
|
||||
field_attrs = {
|
||||
'validators': [prefix_validator]
|
||||
}
|
||||
|
||||
def __init__(self, min_prefix_length=None, max_prefix_length=None, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Optional minimum/maximum prefix lengths
|
||||
# Set prefix validator and optional minimum/maximum prefix lengths
|
||||
self.field_attrs['validators'] = [prefix_validator]
|
||||
if min_prefix_length is not None:
|
||||
self.field_attrs['validators'].append(
|
||||
MinPrefixLengthValidator(min_prefix_length)
|
||||
@@ -287,7 +286,7 @@ class BaseScript:
|
||||
|
||||
return vars
|
||||
|
||||
def run(self, data):
|
||||
def run(self, data, commit):
|
||||
raise NotImplementedError("The script must define a run() method.")
|
||||
|
||||
def as_form(self, data=None, files=None, initial=None):
|
||||
@@ -384,10 +383,17 @@ def run_script(script, data, request, commit=True):
|
||||
# Add the current request as a property of the script
|
||||
script.request = request
|
||||
|
||||
# Determine whether the script accepts a 'commit' argument (this was introduced in v2.7.8)
|
||||
kwargs = {
|
||||
'data': data
|
||||
}
|
||||
if 'commit' in inspect.signature(script.run).parameters:
|
||||
kwargs['commit'] = commit
|
||||
|
||||
try:
|
||||
with transaction.atomic():
|
||||
start_time = time.time()
|
||||
output = script.run(data)
|
||||
output = script.run(**kwargs)
|
||||
end_time = time.time()
|
||||
if not commit:
|
||||
raise AbortTransaction()
|
||||
|
||||
@@ -5,7 +5,7 @@ from utilities.tables import BaseTable, BooleanColumn, ColorColumn, ToggleColumn
|
||||
from .models import ConfigContext, ObjectChange, Tag, TaggedItem
|
||||
|
||||
TAG_ACTIONS = """
|
||||
<a href="{% url 'extras:tag_changelog' slug=record.slug %}" class="btn btn-default btn-xs" title="Changelog">
|
||||
<a href="{% url 'extras:tag_changelog' slug=record.slug %}" class="btn btn-default btn-xs" title="Change log">
|
||||
<i class="fa fa-history"></i>
|
||||
</a>
|
||||
{% if perms.taggit.change_tag %}
|
||||
|
||||
@@ -163,17 +163,17 @@ class ExportTemplateTest(APITestCase):
|
||||
|
||||
super().setUp()
|
||||
|
||||
self.content_type = ContentType.objects.get_for_model(Device)
|
||||
content_type = ContentType.objects.get_for_model(Device)
|
||||
self.exporttemplate1 = ExportTemplate.objects.create(
|
||||
content_type=self.content_type, name='Test Export Template 1',
|
||||
content_type=content_type, name='Test Export Template 1',
|
||||
template_code='{% for obj in queryset %}{{ obj.name }}\n{% endfor %}'
|
||||
)
|
||||
self.exporttemplate2 = ExportTemplate.objects.create(
|
||||
content_type=self.content_type, name='Test Export Template 2',
|
||||
content_type=content_type, name='Test Export Template 2',
|
||||
template_code='{% for obj in queryset %}{{ obj.name }}\n{% endfor %}'
|
||||
)
|
||||
self.exporttemplate3 = ExportTemplate.objects.create(
|
||||
content_type=self.content_type, name='Test Export Template 3',
|
||||
content_type=content_type, name='Test Export Template 3',
|
||||
template_code='{% for obj in queryset %}{{ obj.name }}\n{% endfor %}'
|
||||
)
|
||||
|
||||
@@ -194,7 +194,7 @@ class ExportTemplateTest(APITestCase):
|
||||
def test_create_exporttemplate(self):
|
||||
|
||||
data = {
|
||||
'content_type': self.content_type.pk,
|
||||
'content_type': 'dcim.device',
|
||||
'name': 'Test Export Template 4',
|
||||
'template_code': '{% for obj in queryset %}{{ obj.name }}\n{% endfor %}',
|
||||
}
|
||||
@@ -205,7 +205,7 @@ class ExportTemplateTest(APITestCase):
|
||||
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||
self.assertEqual(ExportTemplate.objects.count(), 4)
|
||||
exporttemplate4 = ExportTemplate.objects.get(pk=response.data['id'])
|
||||
self.assertEqual(exporttemplate4.content_type_id, data['content_type'])
|
||||
self.assertEqual(exporttemplate4.content_type, ContentType.objects.get_for_model(Device))
|
||||
self.assertEqual(exporttemplate4.name, data['name'])
|
||||
self.assertEqual(exporttemplate4.template_code, data['template_code'])
|
||||
|
||||
@@ -213,17 +213,17 @@ class ExportTemplateTest(APITestCase):
|
||||
|
||||
data = [
|
||||
{
|
||||
'content_type': self.content_type.pk,
|
||||
'content_type': 'dcim.device',
|
||||
'name': 'Test Export Template 4',
|
||||
'template_code': '{% for obj in queryset %}{{ obj.name }}\n{% endfor %}',
|
||||
},
|
||||
{
|
||||
'content_type': self.content_type.pk,
|
||||
'content_type': 'dcim.device',
|
||||
'name': 'Test Export Template 5',
|
||||
'template_code': '{% for obj in queryset %}{{ obj.name }}\n{% endfor %}',
|
||||
},
|
||||
{
|
||||
'content_type': self.content_type.pk,
|
||||
'content_type': 'dcim.device',
|
||||
'name': 'Test Export Template 6',
|
||||
'template_code': '{% for obj in queryset %}{{ obj.name }}\n{% endfor %}',
|
||||
},
|
||||
@@ -241,7 +241,7 @@ class ExportTemplateTest(APITestCase):
|
||||
def test_update_exporttemplate(self):
|
||||
|
||||
data = {
|
||||
'content_type': self.content_type.pk,
|
||||
'content_type': 'dcim.device',
|
||||
'name': 'Test Export Template X',
|
||||
'template_code': '{% for obj in queryset %}{{ obj.name }}\n{% endfor %}',
|
||||
}
|
||||
|
||||
@@ -7,10 +7,10 @@ from django.urls import reverse
|
||||
from dcim.models import Site
|
||||
from extras.choices import ObjectChangeActionChoices
|
||||
from extras.models import ConfigContext, ObjectChange, Tag
|
||||
from utilities.testing import StandardTestCases, TestCase
|
||||
from utilities.testing import ViewTestCases, TestCase
|
||||
|
||||
|
||||
class TagTestCase(StandardTestCases.Views):
|
||||
class TagTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
model = Tag
|
||||
|
||||
# Disable inapplicable tests
|
||||
@@ -38,7 +38,7 @@ class TagTestCase(StandardTestCases.Views):
|
||||
}
|
||||
|
||||
|
||||
class ConfigContextTestCase(StandardTestCases.Views):
|
||||
class ConfigContextTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
model = ConfigContext
|
||||
|
||||
# Disable inapplicable tests
|
||||
|
||||
@@ -34,7 +34,7 @@ class WebhookTest(APITestCase):
|
||||
DUMMY_SECRET = "LOOKATMEIMASECRETSTRING"
|
||||
|
||||
webhooks = Webhook.objects.bulk_create((
|
||||
Webhook(name='Site Create Webhook', type_create=True, payload_url=DUMMY_URL, secret=DUMMY_SECRET, additional_headers={'X-Foo': 'Bar'}),
|
||||
Webhook(name='Site Create Webhook', type_create=True, payload_url=DUMMY_URL, secret=DUMMY_SECRET, additional_headers='X-Foo: Bar'),
|
||||
Webhook(name='Site Update Webhook', type_update=True, payload_url=DUMMY_URL, secret=DUMMY_SECRET),
|
||||
Webhook(name='Site Delete Webhook', type_delete=True, payload_url=DUMMY_URL, secret=DUMMY_SECRET),
|
||||
))
|
||||
|
||||
@@ -8,38 +8,38 @@ app_name = 'extras'
|
||||
urlpatterns = [
|
||||
|
||||
# Tags
|
||||
path(r'tags/', views.TagListView.as_view(), name='tag_list'),
|
||||
path(r'tags/edit/', views.TagBulkEditView.as_view(), name='tag_bulk_edit'),
|
||||
path(r'tags/delete/', views.TagBulkDeleteView.as_view(), name='tag_bulk_delete'),
|
||||
path(r'tags/<str:slug>/', views.TagView.as_view(), name='tag'),
|
||||
path(r'tags/<str:slug>/edit/', views.TagEditView.as_view(), name='tag_edit'),
|
||||
path(r'tags/<str:slug>/delete/', views.TagDeleteView.as_view(), name='tag_delete'),
|
||||
path(r'tags/<str:slug>/changelog/', views.ObjectChangeLogView.as_view(), name='tag_changelog', kwargs={'model': Tag}),
|
||||
path('tags/', views.TagListView.as_view(), name='tag_list'),
|
||||
path('tags/edit/', views.TagBulkEditView.as_view(), name='tag_bulk_edit'),
|
||||
path('tags/delete/', views.TagBulkDeleteView.as_view(), name='tag_bulk_delete'),
|
||||
path('tags/<str:slug>/', views.TagView.as_view(), name='tag'),
|
||||
path('tags/<str:slug>/edit/', views.TagEditView.as_view(), name='tag_edit'),
|
||||
path('tags/<str:slug>/delete/', views.TagDeleteView.as_view(), name='tag_delete'),
|
||||
path('tags/<str:slug>/changelog/', views.ObjectChangeLogView.as_view(), name='tag_changelog', kwargs={'model': Tag}),
|
||||
|
||||
# Config contexts
|
||||
path(r'config-contexts/', views.ConfigContextListView.as_view(), name='configcontext_list'),
|
||||
path(r'config-contexts/add/', views.ConfigContextCreateView.as_view(), name='configcontext_add'),
|
||||
path(r'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(r'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(r'config-contexts/<int:pk>/delete/', views.ConfigContextDeleteView.as_view(), name='configcontext_delete'),
|
||||
path('config-contexts/', views.ConfigContextListView.as_view(), name='configcontext_list'),
|
||||
path('config-contexts/add/', views.ConfigContextCreateView.as_view(), name='configcontext_add'),
|
||||
path('config-contexts/edit/', views.ConfigContextBulkEditView.as_view(), name='configcontext_bulk_edit'),
|
||||
path('config-contexts/delete/', views.ConfigContextBulkDeleteView.as_view(), name='configcontext_bulk_delete'),
|
||||
path('config-contexts/<int:pk>/', views.ConfigContextView.as_view(), name='configcontext'),
|
||||
path('config-contexts/<int:pk>/edit/', views.ConfigContextEditView.as_view(), name='configcontext_edit'),
|
||||
path('config-contexts/<int:pk>/delete/', views.ConfigContextDeleteView.as_view(), name='configcontext_delete'),
|
||||
|
||||
# Image attachments
|
||||
path(r'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>/edit/', views.ImageAttachmentEditView.as_view(), name='imageattachment_edit'),
|
||||
path('image-attachments/<int:pk>/delete/', views.ImageAttachmentDeleteView.as_view(), name='imageattachment_delete'),
|
||||
|
||||
# Change logging
|
||||
path(r'changelog/', views.ObjectChangeListView.as_view(), name='objectchange_list'),
|
||||
path(r'changelog/<int:pk>/', views.ObjectChangeView.as_view(), name='objectchange'),
|
||||
path('changelog/', views.ObjectChangeListView.as_view(), name='objectchange_list'),
|
||||
path('changelog/<int:pk>/', views.ObjectChangeView.as_view(), name='objectchange'),
|
||||
|
||||
# Reports
|
||||
path(r'reports/', views.ReportListView.as_view(), name='report_list'),
|
||||
path(r'reports/<str:name>/', views.ReportView.as_view(), name='report'),
|
||||
path(r'reports/<str:name>/run/', views.ReportRunView.as_view(), name='report_run'),
|
||||
path('reports/', views.ReportListView.as_view(), name='report_list'),
|
||||
path('reports/<str:name>/', views.ReportView.as_view(), name='report'),
|
||||
path('reports/<str:name>/run/', views.ReportRunView.as_view(), name='report_run'),
|
||||
|
||||
# Scripts
|
||||
path(r'scripts/', views.ScriptListView.as_view(), name='script_list'),
|
||||
path(r'scripts/<str:module>/<str:name>/', views.ScriptView.as_view(), name='script'),
|
||||
path('scripts/', views.ScriptListView.as_view(), name='script_list'),
|
||||
path('scripts/<str:module>/<str:name>/', views.ScriptView.as_view(), name='script'),
|
||||
|
||||
]
|
||||
|
||||
@@ -12,6 +12,7 @@ from django_tables2 import RequestConfig
|
||||
|
||||
from utilities.forms import ConfirmationForm
|
||||
from utilities.paginator import EnhancedPaginator
|
||||
from utilities.utils import shallow_compare_dict
|
||||
from utilities.views import BulkDeleteView, BulkEditView, ObjectDeleteView, ObjectEditView, ObjectListView
|
||||
from . import filters, forms
|
||||
from .models import ConfigContext, ImageAttachment, ObjectChange, ReportResult, Tag, TaggedItem
|
||||
@@ -34,7 +35,7 @@ class TagListView(PermissionRequiredMixin, ObjectListView):
|
||||
filterset = filters.TagFilterSet
|
||||
filterset_form = forms.TagFilterForm
|
||||
table = TagTable
|
||||
template_name = 'extras/tag_list.html'
|
||||
action_buttons = ()
|
||||
|
||||
|
||||
class TagView(PermissionRequiredMixin, View):
|
||||
@@ -111,7 +112,7 @@ class ConfigContextListView(PermissionRequiredMixin, ObjectListView):
|
||||
filterset = filters.ConfigContextFilterSet
|
||||
filterset_form = forms.ConfigContextFilterForm
|
||||
table = ConfigContextTable
|
||||
template_name = 'extras/configcontext_list.html'
|
||||
action_buttons = ('add',)
|
||||
|
||||
|
||||
class ConfigContextView(PermissionRequiredMixin, View):
|
||||
@@ -191,6 +192,7 @@ class ObjectChangeListView(PermissionRequiredMixin, ObjectListView):
|
||||
filterset_form = forms.ObjectChangeFilterForm
|
||||
table = ObjectChangeTable
|
||||
template_name = 'extras/objectchange_list.html'
|
||||
action_buttons = ('export',)
|
||||
|
||||
|
||||
class ObjectChangeView(PermissionRequiredMixin, View):
|
||||
@@ -206,8 +208,31 @@ class ObjectChangeView(PermissionRequiredMixin, View):
|
||||
orderable=False
|
||||
)
|
||||
|
||||
objectchanges = ObjectChange.objects.filter(
|
||||
changed_object_type=objectchange.changed_object_type,
|
||||
changed_object_id=objectchange.changed_object_id,
|
||||
)
|
||||
|
||||
next_change = objectchanges.filter(time__gt=objectchange.time).order_by('time').first()
|
||||
prev_change = objectchanges.filter(time__lt=objectchange.time).order_by('-time').first()
|
||||
|
||||
if prev_change:
|
||||
diff_added = shallow_compare_dict(
|
||||
prev_change.object_data,
|
||||
objectchange.object_data,
|
||||
exclude=['last_updated'],
|
||||
)
|
||||
diff_removed = {x: prev_change.object_data.get(x) for x in diff_added}
|
||||
else:
|
||||
# No previous change; this is the initial change that added the object
|
||||
diff_added = diff_removed = objectchange.object_data
|
||||
|
||||
return render(request, 'extras/objectchange.html', {
|
||||
'objectchange': objectchange,
|
||||
'diff_added': diff_added,
|
||||
'diff_removed': diff_removed,
|
||||
'next_change': next_change,
|
||||
'prev_change': prev_change,
|
||||
'related_changes_table': related_changes_table,
|
||||
'related_changes_count': related_changes.count()
|
||||
})
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import datetime
|
||||
import hashlib
|
||||
import hmac
|
||||
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
import json
|
||||
import logging
|
||||
|
||||
import requests
|
||||
from django_rq import job
|
||||
from rest_framework.utils.encoders import JSONEncoder
|
||||
from jinja2.exceptions import TemplateError
|
||||
|
||||
from .choices import ObjectChangeActionChoices, WebhookContentTypeChoices
|
||||
from .choices import ObjectChangeActionChoices
|
||||
from .webhooks import generate_signature
|
||||
|
||||
logger = logging.getLogger('netbox.webhooks_worker')
|
||||
|
||||
|
||||
@job('default')
|
||||
def process_webhook(webhook, data, model_name, event, timestamp, username, request_id):
|
||||
"""
|
||||
Make a POST request to the defined Webhook
|
||||
"""
|
||||
payload = {
|
||||
context = {
|
||||
'event': dict(ObjectChangeActionChoices)[event].lower(),
|
||||
'timestamp': timestamp,
|
||||
'model': model_name,
|
||||
@@ -21,29 +23,48 @@ def process_webhook(webhook, data, model_name, event, timestamp, username, reque
|
||||
'request_id': request_id,
|
||||
'data': data
|
||||
}
|
||||
|
||||
# Build the headers for the HTTP request
|
||||
headers = {
|
||||
'Content-Type': webhook.http_content_type,
|
||||
}
|
||||
if webhook.additional_headers:
|
||||
headers.update(webhook.additional_headers)
|
||||
try:
|
||||
headers.update(webhook.render_headers(context))
|
||||
except (TemplateError, ValueError) as e:
|
||||
logger.error("Error parsing HTTP headers for webhook {}: {}".format(webhook, e))
|
||||
raise e
|
||||
|
||||
# Render the request body
|
||||
try:
|
||||
body = webhook.render_body(context)
|
||||
except TemplateError as e:
|
||||
logger.error("Error rendering request body for webhook {}: {}".format(webhook, e))
|
||||
raise e
|
||||
|
||||
# Prepare the HTTP request
|
||||
params = {
|
||||
'method': 'POST',
|
||||
'method': webhook.http_method,
|
||||
'url': webhook.payload_url,
|
||||
'headers': headers
|
||||
'headers': headers,
|
||||
'data': body,
|
||||
}
|
||||
logger.info(
|
||||
"Sending {} request to {} ({} {})".format(
|
||||
params['method'], params['url'], context['model'], context['event']
|
||||
)
|
||||
)
|
||||
logger.debug(params)
|
||||
try:
|
||||
prepared_request = requests.Request(**params).prepare()
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error("Error forming HTTP request: {}".format(e))
|
||||
raise e
|
||||
|
||||
if webhook.http_content_type == WebhookContentTypeChoices.CONTENTTYPE_JSON:
|
||||
params.update({'data': json.dumps(payload, cls=JSONEncoder)})
|
||||
elif webhook.http_content_type == WebhookContentTypeChoices.CONTENTTYPE_FORMDATA:
|
||||
params.update({'data': payload})
|
||||
|
||||
prepared_request = requests.Request(**params).prepare()
|
||||
|
||||
# If a secret key is defined, sign the request with a hash of the key and its content
|
||||
if webhook.secret != '':
|
||||
# Sign the request with a hash of the secret key and its content.
|
||||
prepared_request.headers['X-Hook-Signature'] = generate_signature(prepared_request.body, webhook.secret)
|
||||
|
||||
# Send the request
|
||||
with requests.Session() as session:
|
||||
session.verify = webhook.ssl_verification
|
||||
if webhook.ca_file_path:
|
||||
@@ -51,8 +72,10 @@ def process_webhook(webhook, data, model_name, event, timestamp, username, reque
|
||||
response = session.send(prepared_request)
|
||||
|
||||
if 200 <= response.status_code <= 299:
|
||||
logger.info("Request succeeded; response status {}".format(response.status_code))
|
||||
return 'Status {} returned, webhook successfully processed.'.format(response.status_code)
|
||||
else:
|
||||
logger.warning("Request failed; response status {}: {}".format(response.status_code, response.content))
|
||||
raise requests.exceptions.RequestException(
|
||||
"Status {} returned with content '{}', webhook FAILED to process.".format(
|
||||
response.status_code, response.content
|
||||
|
||||
@@ -202,7 +202,7 @@ class IPAddressSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
vrf = NestedVRFSerializer(required=False, allow_null=True)
|
||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||
status = ChoiceField(choices=IPAddressStatusChoices, required=False)
|
||||
role = ChoiceField(choices=IPAddressRoleChoices, required=False, allow_null=True)
|
||||
role = ChoiceField(choices=IPAddressRoleChoices, allow_blank=True, required=False)
|
||||
interface = IPAddressInterfaceSerializer(required=False, allow_null=True)
|
||||
nat_inside = NestedIPAddressSerializer(required=False, allow_null=True)
|
||||
nat_outside = NestedIPAddressSerializer(read_only=True)
|
||||
@@ -240,7 +240,7 @@ class AvailableIPSerializer(serializers.Serializer):
|
||||
class ServiceSerializer(TaggitSerializer, CustomFieldModelSerializer):
|
||||
device = NestedDeviceSerializer(required=False, allow_null=True)
|
||||
virtual_machine = NestedVirtualMachineSerializer(required=False, allow_null=True)
|
||||
protocol = ChoiceField(choices=ServiceProtocolChoices)
|
||||
protocol = ChoiceField(choices=ServiceProtocolChoices, required=False)
|
||||
ipaddresses = SerializedPKRelatedField(
|
||||
queryset=IPAddress.objects.all(),
|
||||
serializer=NestedIPAddressSerializer,
|
||||
|
||||
@@ -15,30 +15,30 @@ router = routers.DefaultRouter()
|
||||
router.APIRootView = IPAMRootView
|
||||
|
||||
# Field choices
|
||||
router.register(r'_choices', views.IPAMFieldChoicesViewSet, basename='field-choice')
|
||||
router.register('_choices', views.IPAMFieldChoicesViewSet, basename='field-choice')
|
||||
|
||||
# VRFs
|
||||
router.register(r'vrfs', views.VRFViewSet)
|
||||
router.register('vrfs', views.VRFViewSet)
|
||||
|
||||
# RIRs
|
||||
router.register(r'rirs', views.RIRViewSet)
|
||||
router.register('rirs', views.RIRViewSet)
|
||||
|
||||
# Aggregates
|
||||
router.register(r'aggregates', views.AggregateViewSet)
|
||||
router.register('aggregates', views.AggregateViewSet)
|
||||
|
||||
# Prefixes
|
||||
router.register(r'roles', views.RoleViewSet)
|
||||
router.register(r'prefixes', views.PrefixViewSet)
|
||||
router.register('roles', views.RoleViewSet)
|
||||
router.register('prefixes', views.PrefixViewSet)
|
||||
|
||||
# IP addresses
|
||||
router.register(r'ip-addresses', views.IPAddressViewSet)
|
||||
router.register('ip-addresses', views.IPAddressViewSet)
|
||||
|
||||
# VLANs
|
||||
router.register(r'vlan-groups', views.VLANGroupViewSet)
|
||||
router.register(r'vlans', views.VLANViewSet)
|
||||
router.register('vlan-groups', views.VLANGroupViewSet)
|
||||
router.register('vlans', views.VLANViewSet)
|
||||
|
||||
# Services
|
||||
router.register(r'services', views.ServiceViewSet)
|
||||
router.register('services', views.ServiceViewSet)
|
||||
|
||||
app_name = 'ipam-api'
|
||||
urlpatterns = router.urls
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from django.conf import settings
|
||||
from django.db.models import Count
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django_pglocks import advisory_lock
|
||||
from rest_framework import status
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
@@ -10,6 +11,7 @@ from extras.api.views import CustomFieldModelViewSet
|
||||
from ipam import filters
|
||||
from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
|
||||
from utilities.api import FieldChoicesViewSet, ModelViewSet
|
||||
from utilities.constants import ADVISORY_LOCK_KEYS
|
||||
from utilities.utils import get_subquery
|
||||
from . import serializers
|
||||
|
||||
@@ -86,9 +88,13 @@ class PrefixViewSet(CustomFieldModelViewSet):
|
||||
filterset_class = filters.PrefixFilterSet
|
||||
|
||||
@action(detail=True, url_path='available-prefixes', methods=['get', 'post'])
|
||||
@advisory_lock(ADVISORY_LOCK_KEYS['available-prefixes'])
|
||||
def available_prefixes(self, request, pk=None):
|
||||
"""
|
||||
A convenience method for returning available child prefixes within a parent.
|
||||
|
||||
The advisory lock decorator uses a PostgreSQL advisory lock to prevent this API from being
|
||||
invoked in parallel, which results in a race condition where multiple insertions can occur.
|
||||
"""
|
||||
prefix = get_object_or_404(Prefix, pk=pk)
|
||||
available_prefixes = prefix.get_available_prefixes()
|
||||
@@ -180,11 +186,15 @@ class PrefixViewSet(CustomFieldModelViewSet):
|
||||
return Response(serializer.data)
|
||||
|
||||
@action(detail=True, url_path='available-ips', methods=['get', 'post'])
|
||||
@advisory_lock(ADVISORY_LOCK_KEYS['available-ips'])
|
||||
def available_ips(self, request, pk=None):
|
||||
"""
|
||||
A convenience method for returning available IP addresses within a prefix. By default, the number of IPs
|
||||
returned will be equivalent to PAGINATE_COUNT. An arbitrary limit (up to MAX_PAGE_SIZE, if set) may be passed,
|
||||
however results will not be paginated.
|
||||
|
||||
The advisory lock decorator uses a PostgreSQL advisory lock to prevent this API from being
|
||||
invoked in parallel, which results in a race condition where multiple insertions can occur.
|
||||
"""
|
||||
prefix = get_object_or_404(Prefix, pk=pk)
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ from dcim.models import Device, Interface, Region, Site
|
||||
from extras.filters import CustomFieldFilterSet, CreatedUpdatedFilterSet
|
||||
from tenancy.filters import TenancyFilterSet
|
||||
from utilities.filters import (
|
||||
MultiValueCharFilter, NameSlugSearchFilterSet, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter,
|
||||
MultiValueCharFilter, MultiValueNumberFilter, NameSlugSearchFilterSet, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter,
|
||||
)
|
||||
from virtualization.models import VirtualMachine
|
||||
from .choices import *
|
||||
@@ -304,12 +304,12 @@ class IPAddressFilterSet(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedF
|
||||
to_field_name='rd',
|
||||
label='VRF (RD)',
|
||||
)
|
||||
device = django_filters.CharFilter(
|
||||
device = MultiValueCharFilter(
|
||||
method='filter_device',
|
||||
field_name='name',
|
||||
label='Device',
|
||||
label='Device (name)',
|
||||
)
|
||||
device_id = django_filters.NumberFilter(
|
||||
device_id = MultiValueNumberFilter(
|
||||
method='filter_device',
|
||||
field_name='pk',
|
||||
label='Device (ID)',
|
||||
@@ -385,8 +385,10 @@ class IPAddressFilterSet(TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedF
|
||||
|
||||
def filter_device(self, queryset, name, value):
|
||||
try:
|
||||
device = Device.objects.prefetch_related('device_type').get(**{name: value})
|
||||
vc_interface_ids = [i['id'] for i in device.vc_interfaces.values('id')]
|
||||
devices = Device.objects.prefetch_related('device_type').filter(**{'{}__in'.format(name): value})
|
||||
vc_interface_ids = []
|
||||
for device in devices:
|
||||
vc_interface_ids.extend([i['id'] for i in device.vc_interfaces.values('id')])
|
||||
return queryset.filter(interface_id__in=vc_interface_ids)
|
||||
except Device.DoesNotExist:
|
||||
return queryset.none()
|
||||
|
||||
@@ -10,9 +10,10 @@ from extras.forms import (
|
||||
from tenancy.forms import TenancyFilterForm, TenancyForm
|
||||
from tenancy.models import Tenant
|
||||
from utilities.forms import (
|
||||
add_blank_choice, APISelect, APISelectMultiple, BootstrapMixin, BulkEditNullBooleanSelect, ChainedModelChoiceField,
|
||||
CSVChoiceField, DatePicker, ExpandableIPAddressField, FilterChoiceField, FlexibleModelChoiceField, ReturnURLForm,
|
||||
SlugField, StaticSelect2, StaticSelect2Multiple, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES
|
||||
add_blank_choice, APISelect, APISelectMultiple, BootstrapMixin, BulkEditNullBooleanSelect, CSVChoiceField,
|
||||
DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableIPAddressField,
|
||||
FlexibleModelChoiceField, ReturnURLForm, SlugField, StaticSelect2, StaticSelect2Multiple, TagFilterField,
|
||||
BOOLEAN_WITH_BLANK_CHOICES,
|
||||
)
|
||||
from virtualization.models import VirtualMachine
|
||||
from .constants import *
|
||||
@@ -75,7 +76,7 @@ class VRFBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm
|
||||
queryset=VRF.objects.all(),
|
||||
widget=forms.MultipleHiddenInput()
|
||||
)
|
||||
tenant = forms.ModelChoiceField(
|
||||
tenant = DynamicModelChoiceField(
|
||||
queryset=Tenant.objects.all(),
|
||||
required=False,
|
||||
widget=APISelect(
|
||||
@@ -148,6 +149,12 @@ class RIRFilterForm(BootstrapMixin, forms.Form):
|
||||
#
|
||||
|
||||
class AggregateForm(BootstrapMixin, CustomFieldModelForm):
|
||||
rir = DynamicModelChoiceField(
|
||||
queryset=RIR.objects.all(),
|
||||
widget=APISelect(
|
||||
api_url="/api/ipam/rirs/"
|
||||
)
|
||||
)
|
||||
tags = TagField(
|
||||
required=False
|
||||
)
|
||||
@@ -162,9 +169,6 @@ class AggregateForm(BootstrapMixin, CustomFieldModelForm):
|
||||
'rir': "Regional Internet Registry responsible for this prefix",
|
||||
}
|
||||
widgets = {
|
||||
'rir': APISelect(
|
||||
api_url="/api/ipam/rirs/"
|
||||
),
|
||||
'date_added': DatePicker(),
|
||||
}
|
||||
|
||||
@@ -189,7 +193,7 @@ class AggregateBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEd
|
||||
queryset=Aggregate.objects.all(),
|
||||
widget=forms.MultipleHiddenInput()
|
||||
)
|
||||
rir = forms.ModelChoiceField(
|
||||
rir = DynamicModelChoiceField(
|
||||
queryset=RIR.objects.all(),
|
||||
required=False,
|
||||
label='RIR',
|
||||
@@ -226,9 +230,10 @@ class AggregateFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
label='Address family',
|
||||
widget=StaticSelect2()
|
||||
)
|
||||
rir = FilterChoiceField(
|
||||
rir = DynamicModelMultipleChoiceField(
|
||||
queryset=RIR.objects.all(),
|
||||
to_field_name='slug',
|
||||
required=False,
|
||||
label='RIR',
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/ipam/rirs/",
|
||||
@@ -268,10 +273,17 @@ class RoleCSVForm(forms.ModelForm):
|
||||
#
|
||||
|
||||
class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||
site = forms.ModelChoiceField(
|
||||
vrf = DynamicModelChoiceField(
|
||||
queryset=VRF.objects.all(),
|
||||
required=False,
|
||||
label='VRF',
|
||||
widget=APISelect(
|
||||
api_url="/api/ipam/vrfs/",
|
||||
)
|
||||
)
|
||||
site = DynamicModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
required=False,
|
||||
label='Site',
|
||||
widget=APISelect(
|
||||
api_url="/api/dcim/sites/",
|
||||
filter_for={
|
||||
@@ -283,11 +295,8 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||
}
|
||||
)
|
||||
)
|
||||
vlan_group = ChainedModelChoiceField(
|
||||
vlan_group = DynamicModelChoiceField(
|
||||
queryset=VLANGroup.objects.all(),
|
||||
chains=(
|
||||
('site', 'site'),
|
||||
),
|
||||
required=False,
|
||||
label='VLAN group',
|
||||
widget=APISelect(
|
||||
@@ -300,12 +309,8 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||
}
|
||||
)
|
||||
)
|
||||
vlan = ChainedModelChoiceField(
|
||||
vlan = DynamicModelChoiceField(
|
||||
queryset=VLAN.objects.all(),
|
||||
chains=(
|
||||
('site', 'site'),
|
||||
('group', 'vlan_group'),
|
||||
),
|
||||
required=False,
|
||||
label='VLAN',
|
||||
widget=APISelect(
|
||||
@@ -313,6 +318,13 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||
display_field='display_name'
|
||||
)
|
||||
)
|
||||
role = DynamicModelChoiceField(
|
||||
queryset=Role.objects.all(),
|
||||
required=False,
|
||||
widget=APISelect(
|
||||
api_url="/api/ipam/roles/"
|
||||
)
|
||||
)
|
||||
tags = TagField(required=False)
|
||||
|
||||
class Meta:
|
||||
@@ -322,13 +334,7 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||
'tags',
|
||||
]
|
||||
widgets = {
|
||||
'vrf': APISelect(
|
||||
api_url="/api/ipam/vrfs/"
|
||||
),
|
||||
'status': StaticSelect2(),
|
||||
'role': APISelect(
|
||||
api_url="/api/ipam/roles/"
|
||||
)
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -439,14 +445,14 @@ class PrefixBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF
|
||||
queryset=Prefix.objects.all(),
|
||||
widget=forms.MultipleHiddenInput()
|
||||
)
|
||||
site = forms.ModelChoiceField(
|
||||
site = DynamicModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
required=False,
|
||||
widget=APISelect(
|
||||
api_url="/api/dcim/sites/"
|
||||
)
|
||||
)
|
||||
vrf = forms.ModelChoiceField(
|
||||
vrf = DynamicModelChoiceField(
|
||||
queryset=VRF.objects.all(),
|
||||
required=False,
|
||||
label='VRF',
|
||||
@@ -459,7 +465,7 @@ class PrefixBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF
|
||||
max_value=PREFIX_LENGTH_MAX,
|
||||
required=False
|
||||
)
|
||||
tenant = forms.ModelChoiceField(
|
||||
tenant = DynamicModelChoiceField(
|
||||
queryset=Tenant.objects.all(),
|
||||
required=False,
|
||||
widget=APISelect(
|
||||
@@ -471,7 +477,7 @@ class PrefixBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF
|
||||
required=False,
|
||||
widget=StaticSelect2()
|
||||
)
|
||||
role = forms.ModelChoiceField(
|
||||
role = DynamicModelChoiceField(
|
||||
queryset=Role.objects.all(),
|
||||
required=False,
|
||||
widget=APISelect(
|
||||
@@ -525,10 +531,10 @@ class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm)
|
||||
label='Mask length',
|
||||
widget=StaticSelect2()
|
||||
)
|
||||
vrf_id = FilterChoiceField(
|
||||
vrf_id = DynamicModelMultipleChoiceField(
|
||||
queryset=VRF.objects.all(),
|
||||
required=False,
|
||||
label='VRF',
|
||||
null_label='-- Global --',
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/ipam/vrfs/",
|
||||
null_option=True,
|
||||
@@ -539,7 +545,7 @@ class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm)
|
||||
required=False,
|
||||
widget=StaticSelect2Multiple()
|
||||
)
|
||||
region = FilterChoiceField(
|
||||
region = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
to_field_name='slug',
|
||||
required=False,
|
||||
@@ -551,20 +557,20 @@ class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm)
|
||||
}
|
||||
)
|
||||
)
|
||||
site = FilterChoiceField(
|
||||
site = DynamicModelMultipleChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
null_label='-- None --',
|
||||
required=False,
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/dcim/sites/",
|
||||
value_field="slug",
|
||||
null_option=True,
|
||||
)
|
||||
)
|
||||
role = FilterChoiceField(
|
||||
role = DynamicModelMultipleChoiceField(
|
||||
queryset=Role.objects.all(),
|
||||
to_field_name='slug',
|
||||
null_label='-- None --',
|
||||
required=False,
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/ipam/roles/",
|
||||
value_field="slug",
|
||||
@@ -594,7 +600,15 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldModel
|
||||
queryset=Interface.objects.all(),
|
||||
required=False
|
||||
)
|
||||
nat_site = forms.ModelChoiceField(
|
||||
vrf = DynamicModelChoiceField(
|
||||
queryset=VRF.objects.all(),
|
||||
required=False,
|
||||
label='VRF',
|
||||
widget=APISelect(
|
||||
api_url="/api/ipam/vrfs/"
|
||||
)
|
||||
)
|
||||
nat_site = DynamicModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
required=False,
|
||||
label='Site',
|
||||
@@ -606,11 +620,8 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldModel
|
||||
}
|
||||
)
|
||||
)
|
||||
nat_rack = ChainedModelChoiceField(
|
||||
nat_rack = DynamicModelChoiceField(
|
||||
queryset=Rack.objects.all(),
|
||||
chains=(
|
||||
('site', 'nat_site'),
|
||||
),
|
||||
required=False,
|
||||
label='Rack',
|
||||
widget=APISelect(
|
||||
@@ -624,12 +635,8 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldModel
|
||||
}
|
||||
)
|
||||
)
|
||||
nat_device = ChainedModelChoiceField(
|
||||
nat_device = DynamicModelChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
chains=(
|
||||
('site', 'nat_site'),
|
||||
('rack', 'nat_rack'),
|
||||
),
|
||||
required=False,
|
||||
label='Device',
|
||||
widget=APISelect(
|
||||
@@ -651,11 +658,8 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldModel
|
||||
}
|
||||
)
|
||||
)
|
||||
nat_inside = ChainedModelChoiceField(
|
||||
nat_inside = DynamicModelChoiceField(
|
||||
queryset=IPAddress.objects.all(),
|
||||
chains=(
|
||||
('interface__device', 'nat_device'),
|
||||
),
|
||||
required=False,
|
||||
label='IP Address',
|
||||
widget=APISelect(
|
||||
@@ -680,9 +684,6 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldModel
|
||||
widgets = {
|
||||
'status': StaticSelect2(),
|
||||
'role': StaticSelect2(),
|
||||
'vrf': APISelect(
|
||||
api_url="/api/ipam/vrfs/"
|
||||
)
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -757,6 +758,14 @@ class IPAddressBulkCreateForm(BootstrapMixin, forms.Form):
|
||||
|
||||
|
||||
class IPAddressBulkAddForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||
vrf = DynamicModelChoiceField(
|
||||
queryset=VRF.objects.all(),
|
||||
required=False,
|
||||
label='VRF',
|
||||
widget=APISelect(
|
||||
api_url="/api/ipam/vrfs/"
|
||||
)
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = IPAddress
|
||||
@@ -766,9 +775,6 @@ class IPAddressBulkAddForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||
widgets = {
|
||||
'status': StaticSelect2(),
|
||||
'role': StaticSelect2(),
|
||||
'vrf': APISelect(
|
||||
api_url="/api/ipam/vrfs/"
|
||||
)
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -904,7 +910,7 @@ class IPAddressBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEd
|
||||
queryset=IPAddress.objects.all(),
|
||||
widget=forms.MultipleHiddenInput()
|
||||
)
|
||||
vrf = forms.ModelChoiceField(
|
||||
vrf = DynamicModelChoiceField(
|
||||
queryset=VRF.objects.all(),
|
||||
required=False,
|
||||
label='VRF',
|
||||
@@ -917,7 +923,7 @@ class IPAddressBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEd
|
||||
max_value=IPADDRESS_MASK_LENGTH_MAX,
|
||||
required=False
|
||||
)
|
||||
tenant = forms.ModelChoiceField(
|
||||
tenant = DynamicModelChoiceField(
|
||||
queryset=Tenant.objects.all(),
|
||||
required=False,
|
||||
widget=APISelect(
|
||||
@@ -950,7 +956,7 @@ class IPAddressBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEd
|
||||
|
||||
|
||||
class IPAddressAssignForm(BootstrapMixin, forms.Form):
|
||||
vrf_id = forms.ModelChoiceField(
|
||||
vrf_id = DynamicModelChoiceField(
|
||||
queryset=VRF.objects.all(),
|
||||
required=False,
|
||||
label='VRF',
|
||||
@@ -996,10 +1002,10 @@ class IPAddressFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterFo
|
||||
label='Mask length',
|
||||
widget=StaticSelect2()
|
||||
)
|
||||
vrf_id = FilterChoiceField(
|
||||
vrf_id = DynamicModelMultipleChoiceField(
|
||||
queryset=VRF.objects.all(),
|
||||
required=False,
|
||||
label='VRF',
|
||||
null_label='-- Global --',
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/ipam/vrfs/",
|
||||
null_option=True,
|
||||
@@ -1030,6 +1036,13 @@ class IPAddressFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterFo
|
||||
#
|
||||
|
||||
class VLANGroupForm(BootstrapMixin, forms.ModelForm):
|
||||
site = DynamicModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
required=False,
|
||||
widget=APISelect(
|
||||
api_url="/api/dcim/sites/"
|
||||
)
|
||||
)
|
||||
slug = SlugField()
|
||||
|
||||
class Meta:
|
||||
@@ -1037,11 +1050,6 @@ class VLANGroupForm(BootstrapMixin, forms.ModelForm):
|
||||
fields = [
|
||||
'site', 'name', 'slug',
|
||||
]
|
||||
widgets = {
|
||||
'site': APISelect(
|
||||
api_url="/api/dcim/sites/"
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
class VLANGroupCSVForm(forms.ModelForm):
|
||||
@@ -1065,7 +1073,7 @@ class VLANGroupCSVForm(forms.ModelForm):
|
||||
|
||||
|
||||
class VLANGroupFilterForm(BootstrapMixin, forms.Form):
|
||||
region = FilterChoiceField(
|
||||
region = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
to_field_name='slug',
|
||||
required=False,
|
||||
@@ -1077,10 +1085,10 @@ class VLANGroupFilterForm(BootstrapMixin, forms.Form):
|
||||
}
|
||||
)
|
||||
)
|
||||
site = FilterChoiceField(
|
||||
site = DynamicModelMultipleChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
null_label='-- Global --',
|
||||
required=False,
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/dcim/sites/",
|
||||
value_field="slug",
|
||||
@@ -1094,7 +1102,7 @@ class VLANGroupFilterForm(BootstrapMixin, forms.Form):
|
||||
#
|
||||
|
||||
class VLANForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||
site = forms.ModelChoiceField(
|
||||
site = DynamicModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
required=False,
|
||||
widget=APISelect(
|
||||
@@ -1107,17 +1115,20 @@ class VLANForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||
}
|
||||
)
|
||||
)
|
||||
group = ChainedModelChoiceField(
|
||||
group = DynamicModelChoiceField(
|
||||
queryset=VLANGroup.objects.all(),
|
||||
chains=(
|
||||
('site', 'site'),
|
||||
),
|
||||
required=False,
|
||||
label='Group',
|
||||
widget=APISelect(
|
||||
api_url='/api/ipam/vlan-groups/',
|
||||
)
|
||||
)
|
||||
role = DynamicModelChoiceField(
|
||||
queryset=Role.objects.all(),
|
||||
required=False,
|
||||
widget=APISelect(
|
||||
api_url="/api/ipam/roles/"
|
||||
)
|
||||
)
|
||||
tags = TagField(required=False)
|
||||
|
||||
class Meta:
|
||||
@@ -1135,9 +1146,6 @@ class VLANForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||
}
|
||||
widgets = {
|
||||
'status': StaticSelect2(),
|
||||
'role': APISelect(
|
||||
api_url="/api/ipam/roles/"
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1212,21 +1220,21 @@ class VLANBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor
|
||||
queryset=VLAN.objects.all(),
|
||||
widget=forms.MultipleHiddenInput()
|
||||
)
|
||||
site = forms.ModelChoiceField(
|
||||
site = DynamicModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
required=False,
|
||||
widget=APISelect(
|
||||
api_url="/api/dcim/sites/"
|
||||
)
|
||||
)
|
||||
group = forms.ModelChoiceField(
|
||||
group = DynamicModelChoiceField(
|
||||
queryset=VLANGroup.objects.all(),
|
||||
required=False,
|
||||
widget=APISelect(
|
||||
api_url="/api/ipam/vlan-groups/"
|
||||
)
|
||||
)
|
||||
tenant = forms.ModelChoiceField(
|
||||
tenant = DynamicModelChoiceField(
|
||||
queryset=Tenant.objects.all(),
|
||||
required=False,
|
||||
widget=APISelect(
|
||||
@@ -1238,7 +1246,7 @@ class VLANBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor
|
||||
required=False,
|
||||
widget=StaticSelect2()
|
||||
)
|
||||
role = forms.ModelChoiceField(
|
||||
role = DynamicModelChoiceField(
|
||||
queryset=Role.objects.all(),
|
||||
required=False,
|
||||
widget=APISelect(
|
||||
@@ -1263,7 +1271,7 @@ class VLANFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
|
||||
required=False,
|
||||
label='Search'
|
||||
)
|
||||
region = FilterChoiceField(
|
||||
region = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
to_field_name='slug',
|
||||
required=False,
|
||||
@@ -1276,20 +1284,20 @@ class VLANFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
|
||||
}
|
||||
)
|
||||
)
|
||||
site = FilterChoiceField(
|
||||
site = DynamicModelMultipleChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
null_label='-- Global --',
|
||||
required=False,
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/dcim/sites/",
|
||||
value_field="slug",
|
||||
null_option=True,
|
||||
)
|
||||
)
|
||||
group_id = FilterChoiceField(
|
||||
group_id = DynamicModelMultipleChoiceField(
|
||||
queryset=VLANGroup.objects.all(),
|
||||
required=False,
|
||||
label='VLAN group',
|
||||
null_label='-- None --',
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/ipam/vlan-groups/",
|
||||
null_option=True,
|
||||
@@ -1300,10 +1308,10 @@ class VLANFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
|
||||
required=False,
|
||||
widget=StaticSelect2Multiple()
|
||||
)
|
||||
role = FilterChoiceField(
|
||||
role = DynamicModelMultipleChoiceField(
|
||||
queryset=Role.objects.all(),
|
||||
to_field_name='slug',
|
||||
null_label='-- None --',
|
||||
required=False,
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/ipam/roles/",
|
||||
value_field="slug",
|
||||
|
||||
@@ -154,10 +154,24 @@ class NetHostContained(Lookup):
|
||||
return 'CAST(HOST(%s) AS INET) << %s' % (lhs, rhs), params
|
||||
|
||||
|
||||
#
|
||||
# Transforms
|
||||
#
|
||||
|
||||
class NetMaskLength(Transform):
|
||||
lookup_name = 'net_mask_length'
|
||||
function = 'MASKLEN'
|
||||
lookup_name = 'net_mask_length'
|
||||
|
||||
@property
|
||||
def output_field(self):
|
||||
return IntegerField()
|
||||
|
||||
|
||||
class Host(Transform):
|
||||
function = 'HOST'
|
||||
lookup_name = 'host'
|
||||
|
||||
|
||||
class Inet(Transform):
|
||||
function = 'INET'
|
||||
lookup_name = 'inet'
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from django.db import models
|
||||
from django.db.models.expressions import RawSQL
|
||||
|
||||
from ipam.lookups import Host, Inet
|
||||
|
||||
|
||||
class IPAddressManager(models.Manager):
|
||||
@@ -13,4 +14,4 @@ class IPAddressManager(models.Manager):
|
||||
IP address as a /32 or /128.
|
||||
"""
|
||||
qs = super().get_queryset()
|
||||
return qs.annotate(host=RawSQL('INET(HOST(ipam_ipaddress.address))', [])).order_by('family', 'host')
|
||||
return qs.order_by('family', Inet(Host('address')))
|
||||
|
||||
@@ -26,7 +26,7 @@ RIR_UTILIZATION = """
|
||||
"""
|
||||
|
||||
RIR_ACTIONS = """
|
||||
<a href="{% url 'ipam:rir_changelog' slug=record.slug %}" class="btn btn-default btn-xs" title="Changelog">
|
||||
<a href="{% url 'ipam:rir_changelog' slug=record.slug %}" class="btn btn-default btn-xs" title="Change log">
|
||||
<i class="fa fa-history"></i>
|
||||
</a>
|
||||
{% if perms.ipam.change_rir %}
|
||||
@@ -48,7 +48,7 @@ ROLE_VLAN_COUNT = """
|
||||
"""
|
||||
|
||||
ROLE_ACTIONS = """
|
||||
<a href="{% url 'ipam:role_changelog' slug=record.slug %}" class="btn btn-default btn-xs" title="Changelog">
|
||||
<a href="{% url 'ipam:role_changelog' slug=record.slug %}" class="btn btn-default btn-xs" title="Change log">
|
||||
<i class="fa fa-history"></i>
|
||||
</a>
|
||||
{% if perms.ipam.change_role %}
|
||||
@@ -145,7 +145,7 @@ VLAN_ROLE_LINK = """
|
||||
"""
|
||||
|
||||
VLANGROUP_ACTIONS = """
|
||||
<a href="{% url 'ipam:vlangroup_changelog' pk=record.pk %}" class="btn btn-default btn-xs" title="Changelog">
|
||||
<a href="{% url 'ipam:vlangroup_changelog' pk=record.pk %}" class="btn btn-default btn-xs" title="Change log">
|
||||
<i class="fa fa-history"></i>
|
||||
</a>
|
||||
{% with next_vid=record.get_next_available_vid %}
|
||||
@@ -385,7 +385,7 @@ class InterfaceIPAddressTable(BaseTable):
|
||||
"""
|
||||
List IP addresses assigned to a specific Interface.
|
||||
"""
|
||||
address = tables.TemplateColumn(IPADDRESS_ASSIGN_LINK, verbose_name='IP Address')
|
||||
address = tables.LinkColumn(verbose_name='IP Address')
|
||||
vrf = tables.TemplateColumn(VRF_LINK, verbose_name='VRF')
|
||||
status = tables.TemplateColumn(STATUS_LABEL)
|
||||
tenant = tables.TemplateColumn(template_code=TENANT_LINK)
|
||||
|
||||
@@ -392,13 +392,12 @@ class IPAddressTestCase(TestCase):
|
||||
params = {'vrf': [vrfs[0].rd, vrfs[1].rd]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
# TODO: Test for multiple values
|
||||
def test_device(self):
|
||||
device = Device.objects.first()
|
||||
params = {'device_id': device.pk}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
params = {'device': device.name}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
devices = Device.objects.all()[:2]
|
||||
params = {'device_id': [devices[0].pk, devices[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'device': [devices[0].name, devices[1].name]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_virtual_machine(self):
|
||||
vms = VirtualMachine.objects.all()[:2]
|
||||
|
||||
176
netbox/ipam/tests/test_ordering.py
Normal file
176
netbox/ipam/tests/test_ordering.py
Normal file
@@ -0,0 +1,176 @@
|
||||
from django.test import TestCase
|
||||
|
||||
from ipam.choices import IPAddressStatusChoices, PrefixStatusChoices
|
||||
from ipam.models import IPAddress, Prefix, VRF
|
||||
|
||||
import netaddr
|
||||
|
||||
|
||||
class OrderingTestBase(TestCase):
|
||||
vrfs = None
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Setup the VRFs for the class as a whole
|
||||
"""
|
||||
self.vrfs = (VRF(name="VRF A"), VRF(name="VRF B"), VRF(name="VRF C"))
|
||||
VRF.objects.bulk_create(self.vrfs)
|
||||
|
||||
def _compare(self, queryset, objectset):
|
||||
"""
|
||||
Perform the comparison of the queryset object and the object used to instantiate the queryset.
|
||||
"""
|
||||
for i, obj in enumerate(queryset):
|
||||
self.assertEqual(obj, objectset[i])
|
||||
|
||||
def _compare_ne(self, queryset, objectset):
|
||||
"""
|
||||
Perform the comparison of the queryset object and the object used to instantiate the queryset.
|
||||
"""
|
||||
for i, obj in enumerate(queryset):
|
||||
self.assertNotEqual(obj, objectset[i])
|
||||
|
||||
|
||||
class PrefixOrderingTestCase(OrderingTestBase):
|
||||
|
||||
def test_prefix_vrf_ordering(self):
|
||||
"""
|
||||
This is a very basic test, which tests both prefixes without VRFs and prefixes with VRFs
|
||||
"""
|
||||
# Setup VRFs
|
||||
vrfa, vrfb, vrfc = self.vrfs
|
||||
|
||||
# Setup Prefixes
|
||||
prefixes = (
|
||||
Prefix(status=PrefixStatusChoices.STATUS_CONTAINER, vrf=None, family=4, prefix=netaddr.IPNetwork('192.168.0.0/16')),
|
||||
Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=None, family=4, prefix=netaddr.IPNetwork('192.168.0.0/24')),
|
||||
Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=None, family=4, prefix=netaddr.IPNetwork('192.168.1.0/24')),
|
||||
Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=None, family=4, prefix=netaddr.IPNetwork('192.168.2.0/24')),
|
||||
Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=None, family=4, prefix=netaddr.IPNetwork('192.168.3.0/24')),
|
||||
Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=None, family=4, prefix=netaddr.IPNetwork('192.168.4.0/24')),
|
||||
Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=None, family=4, prefix=netaddr.IPNetwork('192.168.5.0/24')),
|
||||
|
||||
Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, prefix=netaddr.IPNetwork('10.0.0.0/8')),
|
||||
Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, prefix=netaddr.IPNetwork('10.0.0.0/16')),
|
||||
Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, prefix=netaddr.IPNetwork('10.0.0.0/24')),
|
||||
Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, prefix=netaddr.IPNetwork('10.0.1.0/24')),
|
||||
Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, prefix=netaddr.IPNetwork('10.0.2.0/24')),
|
||||
Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, prefix=netaddr.IPNetwork('10.0.3.0/24')),
|
||||
Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, prefix=netaddr.IPNetwork('10.0.4.0/24')),
|
||||
Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, prefix=netaddr.IPNetwork('10.1.0.0/16')),
|
||||
Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, prefix=netaddr.IPNetwork('10.1.1.0/24')),
|
||||
Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, prefix=netaddr.IPNetwork('10.1.2.0/24')),
|
||||
Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, prefix=netaddr.IPNetwork('10.1.3.0/24')),
|
||||
Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, prefix=netaddr.IPNetwork('10.1.4.0/24')),
|
||||
Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, prefix=netaddr.IPNetwork('10.2.0.0/16')),
|
||||
Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, prefix=netaddr.IPNetwork('10.2.1.0/24')),
|
||||
Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, prefix=netaddr.IPNetwork('10.2.2.0/24')),
|
||||
Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, prefix=netaddr.IPNetwork('10.2.3.0/24')),
|
||||
Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, prefix=netaddr.IPNetwork('10.2.4.0/24')),
|
||||
|
||||
Prefix(status=PrefixStatusChoices.STATUS_CONTAINER, vrf=vrfb, family=4, prefix=netaddr.IPNetwork('172.16.0.0/12')),
|
||||
Prefix(status=PrefixStatusChoices.STATUS_CONTAINER, vrf=vrfb, family=4, prefix=netaddr.IPNetwork('172.16.0.0/16')),
|
||||
Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfb, family=4, prefix=netaddr.IPNetwork('172.16.0.0/24')),
|
||||
Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfb, family=4, prefix=netaddr.IPNetwork('172.16.1.0/24')),
|
||||
Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfb, family=4, prefix=netaddr.IPNetwork('172.16.2.0/24')),
|
||||
Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfb, family=4, prefix=netaddr.IPNetwork('172.16.3.0/24')),
|
||||
Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfb, family=4, prefix=netaddr.IPNetwork('172.16.4.0/24')),
|
||||
Prefix(status=PrefixStatusChoices.STATUS_CONTAINER, vrf=vrfb, family=4, prefix=netaddr.IPNetwork('172.17.0.0/16')),
|
||||
Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfb, family=4, prefix=netaddr.IPNetwork('172.17.0.0/24')),
|
||||
Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfb, family=4, prefix=netaddr.IPNetwork('172.17.1.0/24')),
|
||||
Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfb, family=4, prefix=netaddr.IPNetwork('172.17.2.0/24')),
|
||||
Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfb, family=4, prefix=netaddr.IPNetwork('172.17.3.0/24')),
|
||||
Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfb, family=4, prefix=netaddr.IPNetwork('172.17.4.0/24')),
|
||||
)
|
||||
|
||||
Prefix.objects.bulk_create(prefixes)
|
||||
|
||||
# Test
|
||||
self._compare(Prefix.objects.all(), prefixes)
|
||||
|
||||
def test_prefix_complex_ordering(self):
|
||||
"""
|
||||
This function tests a complex ordering of interwoven prefixes and vrfs. This is the current expected ordering of VRFs
|
||||
This includes the testing of the Container status.
|
||||
|
||||
The proper ordering, to get proper containerization should be:
|
||||
None:10.0.0.0/8
|
||||
None:10.0.0.0/16
|
||||
VRF A:10.0.0.0/24
|
||||
VRF A:10.0.1.0/24
|
||||
VRF A:10.0.1.0/25
|
||||
None:10.1.0.0/16
|
||||
VRF A:10.1.0.0/24
|
||||
VRF A:10.1.1.0/24
|
||||
None: 192.168.0.0/16
|
||||
"""
|
||||
# Setup VRFs
|
||||
vrfa, vrfb, vrfc = self.vrfs
|
||||
|
||||
# Setup Prefixes
|
||||
prefixes = [
|
||||
Prefix(status=PrefixStatusChoices.STATUS_CONTAINER, vrf=None, family=4, prefix=netaddr.IPNetwork('10.0.0.0/8')),
|
||||
Prefix(status=PrefixStatusChoices.STATUS_CONTAINER, vrf=None, family=4, prefix=netaddr.IPNetwork('10.0.0.0/16')),
|
||||
Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=None, family=4, prefix=netaddr.IPNetwork('10.1.0.0/16')),
|
||||
Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=None, family=4, prefix=netaddr.IPNetwork('192.168.0.0/16')),
|
||||
Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, prefix=netaddr.IPNetwork('10.0.0.0/24')),
|
||||
Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, prefix=netaddr.IPNetwork('10.0.1.0/24')),
|
||||
Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, prefix=netaddr.IPNetwork('10.0.1.0/25')),
|
||||
Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, prefix=netaddr.IPNetwork('10.1.0.0/24')),
|
||||
Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, prefix=netaddr.IPNetwork('10.1.1.0/24')),
|
||||
]
|
||||
Prefix.objects.bulk_create(prefixes)
|
||||
|
||||
# Test
|
||||
self._compare(Prefix.objects.all(), prefixes)
|
||||
|
||||
|
||||
class IPAddressOrderingTestCase(OrderingTestBase):
|
||||
|
||||
def test_address_vrf_ordering(self):
|
||||
"""
|
||||
This function tests ordering with the inclusion of vrfs
|
||||
"""
|
||||
# Setup VRFs
|
||||
vrfa, vrfb, vrfc = self.vrfs
|
||||
|
||||
# Setup Addresses
|
||||
addresses = (
|
||||
IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, address=netaddr.IPNetwork('10.0.0.1/24')),
|
||||
IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, address=netaddr.IPNetwork('10.0.1.1/24')),
|
||||
IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, address=netaddr.IPNetwork('10.0.2.1/24')),
|
||||
IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, address=netaddr.IPNetwork('10.0.3.1/24')),
|
||||
IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, address=netaddr.IPNetwork('10.0.4.1/24')),
|
||||
IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, address=netaddr.IPNetwork('10.1.0.1/24')),
|
||||
IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, address=netaddr.IPNetwork('10.1.1.1/24')),
|
||||
IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, address=netaddr.IPNetwork('10.1.2.1/24')),
|
||||
IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, address=netaddr.IPNetwork('10.1.3.1/24')),
|
||||
IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, address=netaddr.IPNetwork('10.1.4.1/24')),
|
||||
IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, address=netaddr.IPNetwork('10.2.0.1/24')),
|
||||
IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, address=netaddr.IPNetwork('10.2.1.1/24')),
|
||||
IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, address=netaddr.IPNetwork('10.2.2.1/24')),
|
||||
IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, address=netaddr.IPNetwork('10.2.3.1/24')),
|
||||
IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfa, family=4, address=netaddr.IPNetwork('10.2.4.1/24')),
|
||||
|
||||
IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfb, family=4, address=netaddr.IPNetwork('172.16.0.1/24')),
|
||||
IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfb, family=4, address=netaddr.IPNetwork('172.16.1.1/24')),
|
||||
IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfb, family=4, address=netaddr.IPNetwork('172.16.2.1/24')),
|
||||
IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfb, family=4, address=netaddr.IPNetwork('172.16.3.1/24')),
|
||||
IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfb, family=4, address=netaddr.IPNetwork('172.16.4.1/24')),
|
||||
IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfb, family=4, address=netaddr.IPNetwork('172.17.0.1/24')),
|
||||
IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfb, family=4, address=netaddr.IPNetwork('172.17.1.1/24')),
|
||||
IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfb, family=4, address=netaddr.IPNetwork('172.17.2.1/24')),
|
||||
IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfb, family=4, address=netaddr.IPNetwork('172.17.3.1/24')),
|
||||
IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrfb, family=4, address=netaddr.IPNetwork('172.17.4.1/24')),
|
||||
|
||||
IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=None, family=4, address=netaddr.IPNetwork('192.168.0.1/24')),
|
||||
IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=None, family=4, address=netaddr.IPNetwork('192.168.1.1/24')),
|
||||
IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=None, family=4, address=netaddr.IPNetwork('192.168.2.1/24')),
|
||||
IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=None, family=4, address=netaddr.IPNetwork('192.168.3.1/24')),
|
||||
IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=None, family=4, address=netaddr.IPNetwork('192.168.4.1/24')),
|
||||
IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=None, family=4, address=netaddr.IPNetwork('192.168.5.1/24')),
|
||||
)
|
||||
IPAddress.objects.bulk_create(addresses)
|
||||
|
||||
# Test
|
||||
self._compare(IPAddress.objects.all(), addresses)
|
||||
@@ -5,10 +5,10 @@ from netaddr import IPNetwork
|
||||
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
|
||||
from ipam.choices import *
|
||||
from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
|
||||
from utilities.testing import StandardTestCases
|
||||
from utilities.testing import ViewTestCases
|
||||
|
||||
|
||||
class VRFTestCase(StandardTestCases.Views):
|
||||
class VRFTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
model = VRF
|
||||
|
||||
@classmethod
|
||||
@@ -43,14 +43,9 @@ class VRFTestCase(StandardTestCases.Views):
|
||||
}
|
||||
|
||||
|
||||
class RIRTestCase(StandardTestCases.Views):
|
||||
class RIRTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
||||
model = RIR
|
||||
|
||||
# Disable inapplicable tests
|
||||
test_get_object = None
|
||||
test_delete_object = None
|
||||
test_bulk_edit_objects = None
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
@@ -74,7 +69,7 @@ class RIRTestCase(StandardTestCases.Views):
|
||||
)
|
||||
|
||||
|
||||
class AggregateTestCase(StandardTestCases.Views):
|
||||
class AggregateTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
model = Aggregate
|
||||
|
||||
@classmethod
|
||||
@@ -115,14 +110,9 @@ class AggregateTestCase(StandardTestCases.Views):
|
||||
}
|
||||
|
||||
|
||||
class RoleTestCase(StandardTestCases.Views):
|
||||
class RoleTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
||||
model = Role
|
||||
|
||||
# Disable inapplicable tests
|
||||
test_get_object = None
|
||||
test_delete_object = None
|
||||
test_bulk_edit_objects = None
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
@@ -147,7 +137,7 @@ class RoleTestCase(StandardTestCases.Views):
|
||||
)
|
||||
|
||||
|
||||
class PrefixTestCase(StandardTestCases.Views):
|
||||
class PrefixTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
model = Prefix
|
||||
|
||||
@classmethod
|
||||
@@ -207,7 +197,7 @@ class PrefixTestCase(StandardTestCases.Views):
|
||||
}
|
||||
|
||||
|
||||
class IPAddressTestCase(StandardTestCases.Views):
|
||||
class IPAddressTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
model = IPAddress
|
||||
|
||||
@classmethod
|
||||
@@ -254,14 +244,9 @@ class IPAddressTestCase(StandardTestCases.Views):
|
||||
}
|
||||
|
||||
|
||||
class VLANGroupTestCase(StandardTestCases.Views):
|
||||
class VLANGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
||||
model = VLANGroup
|
||||
|
||||
# Disable inapplicable tests
|
||||
test_get_object = None
|
||||
test_delete_object = None
|
||||
test_bulk_edit_objects = None
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
@@ -287,7 +272,7 @@ class VLANGroupTestCase(StandardTestCases.Views):
|
||||
)
|
||||
|
||||
|
||||
class VLANTestCase(StandardTestCases.Views):
|
||||
class VLANTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
model = VLAN
|
||||
|
||||
@classmethod
|
||||
@@ -346,7 +331,7 @@ class VLANTestCase(StandardTestCases.Views):
|
||||
}
|
||||
|
||||
|
||||
class ServiceTestCase(StandardTestCases.Views):
|
||||
class ServiceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
model = Service
|
||||
|
||||
# Disable inapplicable tests
|
||||
|
||||
@@ -8,97 +8,97 @@ app_name = 'ipam'
|
||||
urlpatterns = [
|
||||
|
||||
# VRFs
|
||||
path(r'vrfs/', views.VRFListView.as_view(), name='vrf_list'),
|
||||
path(r'vrfs/add/', views.VRFCreateView.as_view(), name='vrf_add'),
|
||||
path(r'vrfs/import/', views.VRFBulkImportView.as_view(), name='vrf_import'),
|
||||
path(r'vrfs/edit/', views.VRFBulkEditView.as_view(), name='vrf_bulk_edit'),
|
||||
path(r'vrfs/delete/', views.VRFBulkDeleteView.as_view(), name='vrf_bulk_delete'),
|
||||
path(r'vrfs/<int:pk>/', views.VRFView.as_view(), name='vrf'),
|
||||
path(r'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(r'vrfs/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='vrf_changelog', kwargs={'model': VRF}),
|
||||
path('vrfs/', views.VRFListView.as_view(), name='vrf_list'),
|
||||
path('vrfs/add/', views.VRFCreateView.as_view(), name='vrf_add'),
|
||||
path('vrfs/import/', views.VRFBulkImportView.as_view(), name='vrf_import'),
|
||||
path('vrfs/edit/', views.VRFBulkEditView.as_view(), name='vrf_bulk_edit'),
|
||||
path('vrfs/delete/', views.VRFBulkDeleteView.as_view(), name='vrf_bulk_delete'),
|
||||
path('vrfs/<int:pk>/', views.VRFView.as_view(), name='vrf'),
|
||||
path('vrfs/<int:pk>/edit/', views.VRFEditView.as_view(), name='vrf_edit'),
|
||||
path('vrfs/<int:pk>/delete/', views.VRFDeleteView.as_view(), name='vrf_delete'),
|
||||
path('vrfs/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='vrf_changelog', kwargs={'model': VRF}),
|
||||
|
||||
# RIRs
|
||||
path(r'rirs/', views.RIRListView.as_view(), name='rir_list'),
|
||||
path(r'rirs/add/', views.RIRCreateView.as_view(), name='rir_add'),
|
||||
path(r'rirs/import/', views.RIRBulkImportView.as_view(), name='rir_import'),
|
||||
path(r'rirs/delete/', views.RIRBulkDeleteView.as_view(), name='rir_bulk_delete'),
|
||||
path(r'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('rirs/', views.RIRListView.as_view(), name='rir_list'),
|
||||
path('rirs/add/', views.RIRCreateView.as_view(), name='rir_add'),
|
||||
path('rirs/import/', views.RIRBulkImportView.as_view(), name='rir_import'),
|
||||
path('rirs/delete/', views.RIRBulkDeleteView.as_view(), name='rir_bulk_delete'),
|
||||
path('rirs/<slug:slug>/edit/', views.RIREditView.as_view(), name='rir_edit'),
|
||||
path('vrfs/<slug:slug>/changelog/', ObjectChangeLogView.as_view(), name='rir_changelog', kwargs={'model': RIR}),
|
||||
|
||||
# Aggregates
|
||||
path(r'aggregates/', views.AggregateListView.as_view(), name='aggregate_list'),
|
||||
path(r'aggregates/add/', views.AggregateCreateView.as_view(), name='aggregate_add'),
|
||||
path(r'aggregates/import/', views.AggregateBulkImportView.as_view(), name='aggregate_import'),
|
||||
path(r'aggregates/edit/', views.AggregateBulkEditView.as_view(), name='aggregate_bulk_edit'),
|
||||
path(r'aggregates/delete/', views.AggregateBulkDeleteView.as_view(), name='aggregate_bulk_delete'),
|
||||
path(r'aggregates/<int:pk>/', views.AggregateView.as_view(), name='aggregate'),
|
||||
path(r'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(r'aggregates/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='aggregate_changelog', kwargs={'model': Aggregate}),
|
||||
path('aggregates/', views.AggregateListView.as_view(), name='aggregate_list'),
|
||||
path('aggregates/add/', views.AggregateCreateView.as_view(), name='aggregate_add'),
|
||||
path('aggregates/import/', views.AggregateBulkImportView.as_view(), name='aggregate_import'),
|
||||
path('aggregates/edit/', views.AggregateBulkEditView.as_view(), name='aggregate_bulk_edit'),
|
||||
path('aggregates/delete/', views.AggregateBulkDeleteView.as_view(), name='aggregate_bulk_delete'),
|
||||
path('aggregates/<int:pk>/', views.AggregateView.as_view(), name='aggregate'),
|
||||
path('aggregates/<int:pk>/edit/', views.AggregateEditView.as_view(), name='aggregate_edit'),
|
||||
path('aggregates/<int:pk>/delete/', views.AggregateDeleteView.as_view(), name='aggregate_delete'),
|
||||
path('aggregates/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='aggregate_changelog', kwargs={'model': Aggregate}),
|
||||
|
||||
# Roles
|
||||
path(r'roles/', views.RoleListView.as_view(), name='role_list'),
|
||||
path(r'roles/add/', views.RoleCreateView.as_view(), name='role_add'),
|
||||
path(r'roles/import/', views.RoleBulkImportView.as_view(), name='role_import'),
|
||||
path(r'roles/delete/', views.RoleBulkDeleteView.as_view(), name='role_bulk_delete'),
|
||||
path(r'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/', views.RoleListView.as_view(), name='role_list'),
|
||||
path('roles/add/', views.RoleCreateView.as_view(), name='role_add'),
|
||||
path('roles/import/', views.RoleBulkImportView.as_view(), name='role_import'),
|
||||
path('roles/delete/', views.RoleBulkDeleteView.as_view(), name='role_bulk_delete'),
|
||||
path('roles/<slug:slug>/edit/', views.RoleEditView.as_view(), name='role_edit'),
|
||||
path('roles/<slug:slug>/changelog/', ObjectChangeLogView.as_view(), name='role_changelog', kwargs={'model': Role}),
|
||||
|
||||
# Prefixes
|
||||
path(r'prefixes/', views.PrefixListView.as_view(), name='prefix_list'),
|
||||
path(r'prefixes/add/', views.PrefixCreateView.as_view(), name='prefix_add'),
|
||||
path(r'prefixes/import/', views.PrefixBulkImportView.as_view(), name='prefix_import'),
|
||||
path(r'prefixes/edit/', views.PrefixBulkEditView.as_view(), name='prefix_bulk_edit'),
|
||||
path(r'prefixes/delete/', views.PrefixBulkDeleteView.as_view(), name='prefix_bulk_delete'),
|
||||
path(r'prefixes/<int:pk>/', views.PrefixView.as_view(), name='prefix'),
|
||||
path(r'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(r'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(r'prefixes/<int:pk>/ip-addresses/', views.PrefixIPAddressesView.as_view(), name='prefix_ipaddresses'),
|
||||
path('prefixes/', views.PrefixListView.as_view(), name='prefix_list'),
|
||||
path('prefixes/add/', views.PrefixCreateView.as_view(), name='prefix_add'),
|
||||
path('prefixes/import/', views.PrefixBulkImportView.as_view(), name='prefix_import'),
|
||||
path('prefixes/edit/', views.PrefixBulkEditView.as_view(), name='prefix_bulk_edit'),
|
||||
path('prefixes/delete/', views.PrefixBulkDeleteView.as_view(), name='prefix_bulk_delete'),
|
||||
path('prefixes/<int:pk>/', views.PrefixView.as_view(), name='prefix'),
|
||||
path('prefixes/<int:pk>/edit/', views.PrefixEditView.as_view(), name='prefix_edit'),
|
||||
path('prefixes/<int:pk>/delete/', views.PrefixDeleteView.as_view(), name='prefix_delete'),
|
||||
path('prefixes/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='prefix_changelog', kwargs={'model': Prefix}),
|
||||
path('prefixes/<int:pk>/prefixes/', views.PrefixPrefixesView.as_view(), name='prefix_prefixes'),
|
||||
path('prefixes/<int:pk>/ip-addresses/', views.PrefixIPAddressesView.as_view(), name='prefix_ipaddresses'),
|
||||
|
||||
# IP addresses
|
||||
path(r'ip-addresses/', views.IPAddressListView.as_view(), name='ipaddress_list'),
|
||||
path(r'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(r'ip-addresses/import/', views.IPAddressBulkImportView.as_view(), name='ipaddress_import'),
|
||||
path(r'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(r'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(r'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(r'ip-addresses/<int:pk>/delete/', views.IPAddressDeleteView.as_view(), name='ipaddress_delete'),
|
||||
path('ip-addresses/', views.IPAddressListView.as_view(), name='ipaddress_list'),
|
||||
path('ip-addresses/add/', views.IPAddressCreateView.as_view(), name='ipaddress_add'),
|
||||
path('ip-addresses/bulk-add/', views.IPAddressBulkCreateView.as_view(), name='ipaddress_bulk_add'),
|
||||
path('ip-addresses/import/', views.IPAddressBulkImportView.as_view(), name='ipaddress_import'),
|
||||
path('ip-addresses/edit/', views.IPAddressBulkEditView.as_view(), name='ipaddress_bulk_edit'),
|
||||
path('ip-addresses/delete/', views.IPAddressBulkDeleteView.as_view(), name='ipaddress_bulk_delete'),
|
||||
path('ip-addresses/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='ipaddress_changelog', kwargs={'model': IPAddress}),
|
||||
path('ip-addresses/assign/', views.IPAddressAssignView.as_view(), name='ipaddress_assign'),
|
||||
path('ip-addresses/<int:pk>/', views.IPAddressView.as_view(), name='ipaddress'),
|
||||
path('ip-addresses/<int:pk>/edit/', views.IPAddressEditView.as_view(), name='ipaddress_edit'),
|
||||
path('ip-addresses/<int:pk>/delete/', views.IPAddressDeleteView.as_view(), name='ipaddress_delete'),
|
||||
|
||||
# VLAN groups
|
||||
path(r'vlan-groups/', views.VLANGroupListView.as_view(), name='vlangroup_list'),
|
||||
path(r'vlan-groups/add/', views.VLANGroupCreateView.as_view(), name='vlangroup_add'),
|
||||
path(r'vlan-groups/import/', views.VLANGroupBulkImportView.as_view(), name='vlangroup_import'),
|
||||
path(r'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(r'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/', views.VLANGroupListView.as_view(), name='vlangroup_list'),
|
||||
path('vlan-groups/add/', views.VLANGroupCreateView.as_view(), name='vlangroup_add'),
|
||||
path('vlan-groups/import/', views.VLANGroupBulkImportView.as_view(), name='vlangroup_import'),
|
||||
path('vlan-groups/delete/', views.VLANGroupBulkDeleteView.as_view(), name='vlangroup_bulk_delete'),
|
||||
path('vlan-groups/<int:pk>/edit/', views.VLANGroupEditView.as_view(), name='vlangroup_edit'),
|
||||
path('vlan-groups/<int:pk>/vlans/', views.VLANGroupVLANsView.as_view(), name='vlangroup_vlans'),
|
||||
path('vlan-groups/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='vlangroup_changelog', kwargs={'model': VLANGroup}),
|
||||
|
||||
# VLANs
|
||||
path(r'vlans/', views.VLANListView.as_view(), name='vlan_list'),
|
||||
path(r'vlans/add/', views.VLANCreateView.as_view(), name='vlan_add'),
|
||||
path(r'vlans/import/', views.VLANBulkImportView.as_view(), name='vlan_import'),
|
||||
path(r'vlans/edit/', views.VLANBulkEditView.as_view(), name='vlan_bulk_edit'),
|
||||
path(r'vlans/delete/', views.VLANBulkDeleteView.as_view(), name='vlan_bulk_delete'),
|
||||
path(r'vlans/<int:pk>/', views.VLANView.as_view(), name='vlan'),
|
||||
path(r'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(r'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/', views.VLANListView.as_view(), name='vlan_list'),
|
||||
path('vlans/add/', views.VLANCreateView.as_view(), name='vlan_add'),
|
||||
path('vlans/import/', views.VLANBulkImportView.as_view(), name='vlan_import'),
|
||||
path('vlans/edit/', views.VLANBulkEditView.as_view(), name='vlan_bulk_edit'),
|
||||
path('vlans/delete/', views.VLANBulkDeleteView.as_view(), name='vlan_bulk_delete'),
|
||||
path('vlans/<int:pk>/', views.VLANView.as_view(), name='vlan'),
|
||||
path('vlans/<int:pk>/members/', views.VLANMembersView.as_view(), name='vlan_members'),
|
||||
path('vlans/<int:pk>/edit/', views.VLANEditView.as_view(), name='vlan_edit'),
|
||||
path('vlans/<int:pk>/delete/', views.VLANDeleteView.as_view(), name='vlan_delete'),
|
||||
path('vlans/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='vlan_changelog', kwargs={'model': VLAN}),
|
||||
|
||||
# Services
|
||||
path(r'services/', views.ServiceListView.as_view(), name='service_list'),
|
||||
path(r'services/edit/', views.ServiceBulkEditView.as_view(), name='service_bulk_edit'),
|
||||
path(r'services/delete/', views.ServiceBulkDeleteView.as_view(), name='service_bulk_delete'),
|
||||
path(r'services/<int:pk>/', views.ServiceView.as_view(), name='service'),
|
||||
path(r'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(r'services/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='service_changelog', kwargs={'model': Service}),
|
||||
path('services/', views.ServiceListView.as_view(), name='service_list'),
|
||||
path('services/edit/', views.ServiceBulkEditView.as_view(), name='service_bulk_edit'),
|
||||
path('services/delete/', views.ServiceBulkDeleteView.as_view(), name='service_bulk_delete'),
|
||||
path('services/<int:pk>/', views.ServiceView.as_view(), name='service'),
|
||||
path('services/<int:pk>/edit/', views.ServiceEditView.as_view(), name='service_edit'),
|
||||
path('services/<int:pk>/delete/', views.ServiceDeleteView.as_view(), name='service_delete'),
|
||||
path('services/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='service_changelog', kwargs={'model': Service}),
|
||||
|
||||
]
|
||||
|
||||
@@ -118,7 +118,6 @@ class VRFListView(PermissionRequiredMixin, ObjectListView):
|
||||
filterset = filters.VRFFilterSet
|
||||
filterset_form = forms.VRFFilterForm
|
||||
table = tables.VRFTable
|
||||
template_name = 'ipam/vrf_list.html'
|
||||
|
||||
|
||||
class VRFView(PermissionRequiredMixin, View):
|
||||
@@ -293,7 +292,6 @@ class AggregateListView(PermissionRequiredMixin, ObjectListView):
|
||||
queryset = Aggregate.objects.prefetch_related('rir').annotate(
|
||||
child_count=RawSQL('SELECT COUNT(*) FROM ipam_prefix WHERE ipam_prefix.prefix <<= ipam_aggregate.prefix', ())
|
||||
)
|
||||
|
||||
filterset = filters.AggregateFilterSet
|
||||
filterset_form = forms.AggregateFilterForm
|
||||
table = tables.AggregateDetailTable
|
||||
@@ -411,7 +409,6 @@ class RoleListView(PermissionRequiredMixin, ObjectListView):
|
||||
permission_required = 'ipam.view_role'
|
||||
queryset = Role.objects.all()
|
||||
table = tables.RoleTable
|
||||
template_name = 'ipam/role_list.html'
|
||||
|
||||
|
||||
class RoleCreateView(PermissionRequiredMixin, ObjectEditView):
|
||||
@@ -644,7 +641,6 @@ class IPAddressListView(PermissionRequiredMixin, ObjectListView):
|
||||
filterset = filters.IPAddressFilterSet
|
||||
filterset_form = forms.IPAddressFilterForm
|
||||
table = tables.IPAddressDetailTable
|
||||
template_name = 'ipam/ipaddress_list.html'
|
||||
|
||||
|
||||
class IPAddressView(PermissionRequiredMixin, View):
|
||||
@@ -817,7 +813,6 @@ class VLANGroupListView(PermissionRequiredMixin, ObjectListView):
|
||||
filterset = filters.VLANGroupFilterSet
|
||||
filterset_form = forms.VLANGroupFilterForm
|
||||
table = tables.VLANGroupTable
|
||||
template_name = 'ipam/vlangroup_list.html'
|
||||
|
||||
|
||||
class VLANGroupCreateView(PermissionRequiredMixin, ObjectEditView):
|
||||
@@ -893,7 +888,6 @@ class VLANListView(PermissionRequiredMixin, ObjectListView):
|
||||
filterset = filters.VLANFilterSet
|
||||
filterset_form = forms.VLANFilterForm
|
||||
table = tables.VLANDetailTable
|
||||
template_name = 'ipam/vlan_list.html'
|
||||
|
||||
|
||||
class VLANView(PermissionRequiredMixin, View):
|
||||
@@ -989,7 +983,7 @@ class ServiceListView(PermissionRequiredMixin, ObjectListView):
|
||||
filterset = filters.ServiceFilterSet
|
||||
filterset_form = forms.ServiceFilterForm
|
||||
table = tables.ServiceTable
|
||||
template_name = 'ipam/service_list.html'
|
||||
action_buttons = ('export',)
|
||||
|
||||
|
||||
class ServiceView(PermissionRequiredMixin, View):
|
||||
|
||||
2
netbox/media/devicetype-images/.gitignore
vendored
Normal file
2
netbox/media/devicetype-images/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
@@ -10,7 +10,8 @@
|
||||
# Example: ALLOWED_HOSTS = ['netbox.example.com', 'netbox.internal.local']
|
||||
ALLOWED_HOSTS = []
|
||||
|
||||
# PostgreSQL database configuration.
|
||||
# PostgreSQL database configuration. See the Django documentation for a complete list of available parameters:
|
||||
# https://docs.djangoproject.com/en/stable/ref/settings/#databases
|
||||
DATABASE = {
|
||||
'NAME': 'netbox', # Database name
|
||||
'USER': '', # PostgreSQL username
|
||||
@@ -27,6 +28,9 @@ REDIS = {
|
||||
'webhooks': {
|
||||
'HOST': 'localhost',
|
||||
'PORT': 6379,
|
||||
# Comment out `HOST` and `PORT` lines and uncomment the following if using Redis Sentinel
|
||||
# 'SENTINELS': [('mysentinel.redis.example.com', 6379)],
|
||||
# 'SENTINEL_SERVICE': 'netbox',
|
||||
'PASSWORD': '',
|
||||
'DATABASE': 0,
|
||||
'DEFAULT_TIMEOUT': 300,
|
||||
@@ -35,6 +39,9 @@ REDIS = {
|
||||
'caching': {
|
||||
'HOST': 'localhost',
|
||||
'PORT': 6379,
|
||||
# Comment out `HOST` and `PORT` lines and uncomment the following if using Redis Sentinel
|
||||
# 'SENTINELS': [('mysentinel.redis.example.com', 6379)],
|
||||
# 'SENTINEL_SERVICE': 'netbox',
|
||||
'PASSWORD': '',
|
||||
'DATABASE': 1,
|
||||
'DEFAULT_TIMEOUT': 300,
|
||||
|
||||
@@ -12,7 +12,7 @@ from django.core.exceptions import ImproperlyConfigured
|
||||
# Environment setup
|
||||
#
|
||||
|
||||
VERSION = '2.7.4'
|
||||
VERSION = '2.7.8'
|
||||
|
||||
# Hostname
|
||||
HOSTNAME = platform.node()
|
||||
@@ -170,18 +170,31 @@ if 'caching' not in REDIS:
|
||||
WEBHOOKS_REDIS = REDIS.get('webhooks', {})
|
||||
WEBHOOKS_REDIS_HOST = WEBHOOKS_REDIS.get('HOST', 'localhost')
|
||||
WEBHOOKS_REDIS_PORT = WEBHOOKS_REDIS.get('PORT', 6379)
|
||||
WEBHOOKS_REDIS_SENTINELS = WEBHOOKS_REDIS.get('SENTINELS', [])
|
||||
WEBHOOKS_REDIS_USING_SENTINEL = all([
|
||||
isinstance(WEBHOOKS_REDIS_SENTINELS, (list, tuple)),
|
||||
len(WEBHOOKS_REDIS_SENTINELS) > 0
|
||||
])
|
||||
WEBHOOKS_REDIS_SENTINEL_SERVICE = WEBHOOKS_REDIS.get('SENTINEL_SERVICE', 'default')
|
||||
WEBHOOKS_REDIS_PASSWORD = WEBHOOKS_REDIS.get('PASSWORD', '')
|
||||
WEBHOOKS_REDIS_DATABASE = WEBHOOKS_REDIS.get('DATABASE', 0)
|
||||
WEBHOOKS_REDIS_DEFAULT_TIMEOUT = WEBHOOKS_REDIS.get('DEFAULT_TIMEOUT', 300)
|
||||
WEBHOOKS_REDIS_SSL = WEBHOOKS_REDIS.get('SSL', False)
|
||||
|
||||
|
||||
CACHING_REDIS = REDIS.get('caching', {})
|
||||
CACHING_REDIS_HOST = WEBHOOKS_REDIS.get('HOST', 'localhost')
|
||||
CACHING_REDIS_PORT = WEBHOOKS_REDIS.get('PORT', 6379)
|
||||
CACHING_REDIS_PASSWORD = WEBHOOKS_REDIS.get('PASSWORD', '')
|
||||
CACHING_REDIS_DATABASE = WEBHOOKS_REDIS.get('DATABASE', 0)
|
||||
CACHING_REDIS_DEFAULT_TIMEOUT = WEBHOOKS_REDIS.get('DEFAULT_TIMEOUT', 300)
|
||||
CACHING_REDIS_SSL = WEBHOOKS_REDIS.get('SSL', False)
|
||||
CACHING_REDIS_HOST = CACHING_REDIS.get('HOST', 'localhost')
|
||||
CACHING_REDIS_PORT = CACHING_REDIS.get('PORT', 6379)
|
||||
CACHING_REDIS_SENTINELS = CACHING_REDIS.get('SENTINELS', [])
|
||||
CACHING_REDIS_USING_SENTINEL = all([
|
||||
isinstance(CACHING_REDIS_SENTINELS, (list, tuple)),
|
||||
len(CACHING_REDIS_SENTINELS) > 0
|
||||
])
|
||||
CACHING_REDIS_SENTINEL_SERVICE = CACHING_REDIS.get('SENTINEL_SERVICE', 'default')
|
||||
CACHING_REDIS_PASSWORD = CACHING_REDIS.get('PASSWORD', '')
|
||||
CACHING_REDIS_DATABASE = CACHING_REDIS.get('DATABASE', 0)
|
||||
CACHING_REDIS_DEFAULT_TIMEOUT = CACHING_REDIS.get('DEFAULT_TIMEOUT', 300)
|
||||
CACHING_REDIS_SSL = CACHING_REDIS.get('SSL', False)
|
||||
|
||||
|
||||
#
|
||||
@@ -394,28 +407,35 @@ if LDAP_CONFIG is not None:
|
||||
#
|
||||
# Caching
|
||||
#
|
||||
|
||||
if CACHING_REDIS_SSL:
|
||||
REDIS_CACHE_CON_STRING = 'rediss://'
|
||||
if CACHING_REDIS_USING_SENTINEL:
|
||||
CACHEOPS_SENTINEL = {
|
||||
'locations': CACHING_REDIS_SENTINELS,
|
||||
'service_name': CACHING_REDIS_SENTINEL_SERVICE,
|
||||
'db': CACHING_REDIS_DATABASE,
|
||||
}
|
||||
else:
|
||||
REDIS_CACHE_CON_STRING = 'redis://'
|
||||
if CACHING_REDIS_SSL:
|
||||
REDIS_CACHE_CON_STRING = 'rediss://'
|
||||
else:
|
||||
REDIS_CACHE_CON_STRING = 'redis://'
|
||||
|
||||
if CACHING_REDIS_PASSWORD:
|
||||
REDIS_CACHE_CON_STRING = '{}:{}@'.format(REDIS_CACHE_CON_STRING, CACHING_REDIS_PASSWORD)
|
||||
if CACHING_REDIS_PASSWORD:
|
||||
REDIS_CACHE_CON_STRING = '{}:{}@'.format(REDIS_CACHE_CON_STRING, CACHING_REDIS_PASSWORD)
|
||||
|
||||
REDIS_CACHE_CON_STRING = '{}{}:{}/{}'.format(
|
||||
REDIS_CACHE_CON_STRING,
|
||||
CACHING_REDIS_HOST,
|
||||
CACHING_REDIS_PORT,
|
||||
CACHING_REDIS_DATABASE
|
||||
)
|
||||
REDIS_CACHE_CON_STRING = '{}{}:{}/{}'.format(
|
||||
REDIS_CACHE_CON_STRING,
|
||||
CACHING_REDIS_HOST,
|
||||
CACHING_REDIS_PORT,
|
||||
CACHING_REDIS_DATABASE
|
||||
)
|
||||
CACHEOPS_REDIS = REDIS_CACHE_CON_STRING
|
||||
|
||||
if not CACHE_TIMEOUT:
|
||||
CACHEOPS_ENABLED = False
|
||||
else:
|
||||
CACHEOPS_ENABLED = True
|
||||
|
||||
CACHEOPS_REDIS = REDIS_CACHE_CON_STRING
|
||||
|
||||
CACHEOPS_DEFAULTS = {
|
||||
'timeout': CACHE_TIMEOUT
|
||||
}
|
||||
@@ -534,6 +554,15 @@ RQ_QUEUES = {
|
||||
'PASSWORD': WEBHOOKS_REDIS_PASSWORD,
|
||||
'DEFAULT_TIMEOUT': WEBHOOKS_REDIS_DEFAULT_TIMEOUT,
|
||||
'SSL': WEBHOOKS_REDIS_SSL,
|
||||
} if not WEBHOOKS_REDIS_USING_SENTINEL else {
|
||||
'SENTINELS': WEBHOOKS_REDIS_SENTINELS,
|
||||
'MASTER_NAME': WEBHOOKS_REDIS_SENTINEL_SERVICE,
|
||||
'DB': WEBHOOKS_REDIS_DATABASE,
|
||||
'PASSWORD': WEBHOOKS_REDIS_PASSWORD,
|
||||
'SOCKET_TIMEOUT': None,
|
||||
'CONNECTION_KWARGS': {
|
||||
'socket_connect_timeout': WEBHOOKS_REDIS_DEFAULT_TIMEOUT
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,49 +26,49 @@ schema_view = get_schema_view(
|
||||
_patterns = [
|
||||
|
||||
# Base views
|
||||
path(r'', HomeView.as_view(), name='home'),
|
||||
path(r'search/', SearchView.as_view(), name='search'),
|
||||
path('', HomeView.as_view(), name='home'),
|
||||
path('search/', SearchView.as_view(), name='search'),
|
||||
|
||||
# Login/logout
|
||||
path(r'login/', LoginView.as_view(), name='login'),
|
||||
path(r'logout/', LogoutView.as_view(), name='logout'),
|
||||
path('login/', LoginView.as_view(), name='login'),
|
||||
path('logout/', LogoutView.as_view(), name='logout'),
|
||||
|
||||
# Apps
|
||||
path(r'circuits/', include('circuits.urls')),
|
||||
path(r'dcim/', include('dcim.urls')),
|
||||
path(r'extras/', include('extras.urls')),
|
||||
path(r'ipam/', include('ipam.urls')),
|
||||
path(r'secrets/', include('secrets.urls')),
|
||||
path(r'tenancy/', include('tenancy.urls')),
|
||||
path(r'user/', include('users.urls')),
|
||||
path(r'virtualization/', include('virtualization.urls')),
|
||||
path('circuits/', include('circuits.urls')),
|
||||
path('dcim/', include('dcim.urls')),
|
||||
path('extras/', include('extras.urls')),
|
||||
path('ipam/', include('ipam.urls')),
|
||||
path('secrets/', include('secrets.urls')),
|
||||
path('tenancy/', include('tenancy.urls')),
|
||||
path('user/', include('users.urls')),
|
||||
path('virtualization/', include('virtualization.urls')),
|
||||
|
||||
# API
|
||||
path(r'api/', APIRootView.as_view(), name='api-root'),
|
||||
path(r'api/circuits/', include('circuits.api.urls')),
|
||||
path(r'api/dcim/', include('dcim.api.urls')),
|
||||
path(r'api/extras/', include('extras.api.urls')),
|
||||
path(r'api/ipam/', include('ipam.api.urls')),
|
||||
path(r'api/secrets/', include('secrets.api.urls')),
|
||||
path(r'api/tenancy/', include('tenancy.api.urls')),
|
||||
path(r'api/virtualization/', include('virtualization.api.urls')),
|
||||
path(r'api/docs/', schema_view.with_ui('swagger'), name='api_docs'),
|
||||
path(r'api/redoc/', schema_view.with_ui('redoc'), name='api_redocs'),
|
||||
path('api/', APIRootView.as_view(), name='api-root'),
|
||||
path('api/circuits/', include('circuits.api.urls')),
|
||||
path('api/dcim/', include('dcim.api.urls')),
|
||||
path('api/extras/', include('extras.api.urls')),
|
||||
path('api/ipam/', include('ipam.api.urls')),
|
||||
path('api/secrets/', include('secrets.api.urls')),
|
||||
path('api/tenancy/', include('tenancy.api.urls')),
|
||||
path('api/virtualization/', include('virtualization.api.urls')),
|
||||
path('api/docs/', schema_view.with_ui('swagger'), name='api_docs'),
|
||||
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'),
|
||||
|
||||
# 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
|
||||
path(r'admin/', admin_site.urls),
|
||||
path(r'admin/webhook-backend-status/', include('django_rq.urls')),
|
||||
path('admin/', admin_site.urls),
|
||||
path('admin/webhook-backend-status/', include('django_rq.urls')),
|
||||
|
||||
]
|
||||
|
||||
if settings.DEBUG:
|
||||
import debug_toolbar
|
||||
_patterns += [
|
||||
path(r'__debug__/', include(debug_toolbar.urls)),
|
||||
path('__debug__/', include(debug_toolbar.urls)),
|
||||
]
|
||||
|
||||
if settings.METRICS_ENABLED:
|
||||
@@ -78,7 +78,7 @@ if settings.METRICS_ENABLED:
|
||||
|
||||
# Prepend BASE_PATH
|
||||
urlpatterns = [
|
||||
path(r'{}'.format(settings.BASE_PATH), include(_patterns))
|
||||
path('{}'.format(settings.BASE_PATH), include(_patterns))
|
||||
]
|
||||
|
||||
handler500 = 'utilities.views.server_error'
|
||||
|
||||
@@ -252,7 +252,7 @@ class HomeView(View):
|
||||
'search_form': SearchForm(),
|
||||
'stats': stats,
|
||||
'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]
|
||||
})
|
||||
|
||||
|
||||
|
||||
@@ -62,8 +62,20 @@ footer p {
|
||||
}
|
||||
}
|
||||
|
||||
/* Scroll the drop-down menus at or above 768px wide to match bootstrap's behavior for hiding dropdown menus */
|
||||
@media (min-width: 768px) {
|
||||
.navbar-nav>li>ul {
|
||||
max-height: calc(80vh - 50px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* Collapse the nav menu on displays less than 980px wide */
|
||||
@media (max-width: 979px) {
|
||||
#navbar {
|
||||
max-height: calc(80vh - 50px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
.navbar-header {
|
||||
float: none;
|
||||
}
|
||||
@@ -167,25 +179,9 @@ nav ul.pagination {
|
||||
|
||||
/* Racks */
|
||||
div.rack_header {
|
||||
margin-left: 36px;
|
||||
margin-left: 32px;
|
||||
text-align: center;
|
||||
width: 230px;
|
||||
}
|
||||
ul.rack_legend {
|
||||
float: left;
|
||||
list-style-type: none;
|
||||
margin-right: 6px;
|
||||
padding: 0;
|
||||
width: 30px;
|
||||
}
|
||||
ul.rack_legend li {
|
||||
color: #c0c0c0;
|
||||
display: block;
|
||||
font-size: 10px;
|
||||
height: 20px;
|
||||
overflow: hidden;
|
||||
padding: 5px 0;
|
||||
text-align: right;
|
||||
width: 220px;
|
||||
}
|
||||
|
||||
/* Devices */
|
||||
|
||||
@@ -14,7 +14,7 @@ text {
|
||||
background-color: #f0f0f0;
|
||||
fill: none;
|
||||
stroke: black;
|
||||
stroke-width: 3px;
|
||||
stroke-width: 2px;
|
||||
}
|
||||
.slot {
|
||||
fill: #f7f7f7;
|
||||
@@ -56,3 +56,14 @@ text {
|
||||
.blocked:hover+.add-device {
|
||||
fill: none;
|
||||
}
|
||||
.unit {
|
||||
margin: 0;
|
||||
padding: 5px 0px;
|
||||
|
||||
fill: #c0c0c0;
|
||||
font-size: 10px;
|
||||
font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;
|
||||
}
|
||||
.hidden {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
@@ -42,17 +42,23 @@ $(document).ready(function() {
|
||||
return s.substring(0, num_chars); // Trim to first num_chars chars
|
||||
}
|
||||
var slug_field = $('#id_slug');
|
||||
slug_field.change(function() {
|
||||
$(this).attr('_changed', true);
|
||||
});
|
||||
if (slug_field) {
|
||||
var slug_source = $('#id_' + slug_field.attr('slug-source'));
|
||||
var slug_length = slug_field.attr('maxlength');
|
||||
if (slug_field.val()) {
|
||||
slug_field.attr('_changed', true);
|
||||
}
|
||||
slug_field.change(function() {
|
||||
$(this).attr('_changed', true);
|
||||
});
|
||||
slug_source.on('keyup change', function() {
|
||||
if (slug_field && !slug_field.attr('_changed')) {
|
||||
slug_field.val(slugify($(this).val(), (slug_length ? slug_length : 50)));
|
||||
}
|
||||
})
|
||||
});
|
||||
$('button.reslugify').click(function() {
|
||||
slug_field.val(slugify(slug_source.val(), (slug_length ? slug_length : 50)));
|
||||
});
|
||||
}
|
||||
|
||||
// Bulk edit nullification
|
||||
@@ -190,15 +196,18 @@ $(document).ready(function() {
|
||||
$.each(element.attributes, function(index, attr){
|
||||
if (attr.name.includes("data-additional-query-param-")){
|
||||
var param_name = attr.name.split("data-additional-query-param-")[1];
|
||||
if (param_name in parameters) {
|
||||
if (Array.isArray(parameters[param_name])) {
|
||||
parameters[param_name].push(attr.value)
|
||||
|
||||
$.each($.parseJSON(attr.value), function(index, value) {
|
||||
if (param_name in parameters) {
|
||||
if (Array.isArray(parameters[param_name])) {
|
||||
parameters[param_name].push(value);
|
||||
} else {
|
||||
parameters[param_name] = [parameters[param_name], value];
|
||||
}
|
||||
} else {
|
||||
parameters[param_name] = [parameters[param_name], attr.value]
|
||||
parameters[param_name] = value;
|
||||
}
|
||||
} else {
|
||||
parameters[param_name] = attr.value;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -220,19 +229,19 @@ $(document).ready(function() {
|
||||
}
|
||||
|
||||
if( record.group !== undefined && record.group !== null && record.site !== undefined && record.site !== null ) {
|
||||
results[record.site.name + ":" + record.group.name] = results[record.site.name + ":" + record.group.name] || { text: record.site.name + " / " + record.group.name, children: [] }
|
||||
results[record.site.name + ":" + record.group.name] = results[record.site.name + ":" + record.group.name] || { text: record.site.name + " / " + record.group.name, children: [] };
|
||||
results[record.site.name + ":" + record.group.name].children.push(record);
|
||||
}
|
||||
else if( record.group !== undefined && record.group !== null ) {
|
||||
results[record.group.name] = results[record.group.name] || { text: record.group.name, children: [] }
|
||||
results[record.group.name] = results[record.group.name] || { text: record.group.name, children: [] };
|
||||
results[record.group.name].children.push(record);
|
||||
}
|
||||
else if( record.site !== undefined && record.site !== null ) {
|
||||
results[record.site.name] = results[record.site.name] || { text: record.site.name, children: [] }
|
||||
results[record.site.name] = results[record.site.name] || { text: record.site.name, children: [] };
|
||||
results[record.site.name].children.push(record);
|
||||
}
|
||||
else if ( (record.group !== undefined || record.group == null) && (record.site !== undefined || record.site === null) ) {
|
||||
results['global'] = results['global'] || { text: 'Global', children: [] }
|
||||
results['global'] = results['global'] || { text: 'Global', children: [] };
|
||||
results['global'].children.push(record);
|
||||
}
|
||||
else {
|
||||
@@ -246,10 +255,9 @@ $(document).ready(function() {
|
||||
|
||||
// Handle the null option, but only add it once
|
||||
if (element.getAttribute('data-null-option') && data.previous === null) {
|
||||
var null_option = $(element).children()[0];
|
||||
results.unshift({
|
||||
id: null_option.value,
|
||||
text: null_option.text
|
||||
id: 'null',
|
||||
text: 'None'
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
16
netbox/project-static/js/rack_elevations.js
Normal file
16
netbox/project-static/js/rack_elevations.js
Normal file
@@ -0,0 +1,16 @@
|
||||
// Toggle the display of device images within an SVG rack elevation
|
||||
$('button.toggle-images').click(function() {
|
||||
var selected = $(this).attr('selected');
|
||||
var rack_front = $("#rack_front");
|
||||
var rack_rear = $("#rack_rear");
|
||||
if (selected) {
|
||||
$('.device-image', rack_front.contents()).addClass('hidden');
|
||||
$('.device-image', rack_rear.contents()).addClass('hidden');
|
||||
} else {
|
||||
$('.device-image', rack_front.contents()).removeClass('hidden');
|
||||
$('.device-image', rack_rear.contents()).removeClass('hidden');
|
||||
}
|
||||
$(this).attr('selected', !selected);
|
||||
$(this).children('span').toggleClass('glyphicon-check glyphicon-unchecked');
|
||||
return false;
|
||||
});
|
||||
@@ -15,15 +15,15 @@ router = routers.DefaultRouter()
|
||||
router.APIRootView = SecretsRootView
|
||||
|
||||
# Field choices
|
||||
router.register(r'_choices', views.SecretsFieldChoicesViewSet, basename='field-choice')
|
||||
router.register('_choices', views.SecretsFieldChoicesViewSet, basename='field-choice')
|
||||
|
||||
# Secrets
|
||||
router.register(r'secret-roles', views.SecretRoleViewSet)
|
||||
router.register(r'secrets', views.SecretViewSet)
|
||||
router.register('secret-roles', views.SecretRoleViewSet)
|
||||
router.register('secrets', views.SecretViewSet)
|
||||
|
||||
# Miscellaneous
|
||||
router.register(r'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('get-session-key', views.GetSessionKeyViewSet, basename='get-session-key')
|
||||
router.register('generate-rsa-key-pair', views.GenerateRSAKeyPairViewSet, basename='generate-rsa-key-pair')
|
||||
|
||||
app_name = 'secrets-api'
|
||||
urlpatterns = router.urls
|
||||
|
||||
@@ -93,8 +93,8 @@ class SecretViewSet(ModelViewSet):
|
||||
|
||||
secret = self.get_object()
|
||||
|
||||
# Attempt to decrypt the secret if the master key is known
|
||||
if self.master_key is not None:
|
||||
# Attempt to decrypt the secret if the user is permitted and the master key is known
|
||||
if secret.decryptable_by(request.user) and self.master_key is not None:
|
||||
secret.decrypt(self.master_key)
|
||||
|
||||
serializer = self.get_serializer(secret)
|
||||
@@ -111,7 +111,9 @@ class SecretViewSet(ModelViewSet):
|
||||
if self.master_key is not None:
|
||||
secrets = []
|
||||
for secret in page:
|
||||
secret.decrypt(self.master_key)
|
||||
# Enforce role permissions
|
||||
if secret.decryptable_by(request.user):
|
||||
secret.decrypt(self.master_key)
|
||||
secrets.append(secret)
|
||||
serializer = self.get_serializer(secrets, many=True)
|
||||
else:
|
||||
|
||||
@@ -8,8 +8,8 @@ from extras.forms import (
|
||||
AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldFilterForm, CustomFieldModelForm, CustomFieldModelCSVForm,
|
||||
)
|
||||
from utilities.forms import (
|
||||
APISelect, APISelectMultiple, BootstrapMixin, FilterChoiceField, FlexibleModelChoiceField, SlugField,
|
||||
StaticSelect2Multiple, TagFilterField
|
||||
APISelect, APISelectMultiple, BootstrapMixin, DynamicModelChoiceField, DynamicModelMultipleChoiceField,
|
||||
FlexibleModelChoiceField, SlugField, StaticSelect2Multiple, TagFilterField,
|
||||
)
|
||||
from .constants import *
|
||||
from .models import Secret, SecretRole, UserKey
|
||||
@@ -87,6 +87,12 @@ class SecretForm(BootstrapMixin, CustomFieldModelForm):
|
||||
label='Plaintext (verify)',
|
||||
widget=forms.PasswordInput()
|
||||
)
|
||||
role = DynamicModelChoiceField(
|
||||
queryset=SecretRole.objects.all(),
|
||||
widget=APISelect(
|
||||
api_url="/api/secrets/secret-roles/"
|
||||
)
|
||||
)
|
||||
tags = TagField(
|
||||
required=False
|
||||
)
|
||||
@@ -96,11 +102,6 @@ class SecretForm(BootstrapMixin, CustomFieldModelForm):
|
||||
fields = [
|
||||
'role', 'name', 'plaintext', 'plaintext2', 'tags',
|
||||
]
|
||||
widgets = {
|
||||
'role': APISelect(
|
||||
api_url="/api/secrets/secret-roles/"
|
||||
)
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -157,7 +158,7 @@ class SecretBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF
|
||||
queryset=Secret.objects.all(),
|
||||
widget=forms.MultipleHiddenInput()
|
||||
)
|
||||
role = forms.ModelChoiceField(
|
||||
role = DynamicModelChoiceField(
|
||||
queryset=SecretRole.objects.all(),
|
||||
required=False,
|
||||
widget=APISelect(
|
||||
@@ -181,9 +182,10 @@ class SecretFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
required=False,
|
||||
label='Search'
|
||||
)
|
||||
role = FilterChoiceField(
|
||||
role = DynamicModelMultipleChoiceField(
|
||||
queryset=SecretRole.objects.all(),
|
||||
to_field_name='slug',
|
||||
required=False,
|
||||
widget=APISelectMultiple(
|
||||
api_url="/api/secrets/secret-roles/",
|
||||
value_field="slug",
|
||||
|
||||
@@ -302,8 +302,8 @@ class Secret(ChangeLoggedModel, CustomFieldModel):
|
||||
Device; Devices may have multiple Secrets associated with them. A name can optionally be defined along with the
|
||||
ciphertext; this string is stored as plain text in the database.
|
||||
|
||||
A Secret can be up to 65,536 bytes (64KB) in length. Each secret string will be padded with random data to a minimum
|
||||
of 64 bytes during encryption in order to protect short strings from ciphertext analysis.
|
||||
A Secret can be up to 65,535 bytes (64KB - 1B) in length. Each secret string will be padded with random data to
|
||||
a minimum of 64 bytes during encryption in order to protect short strings from ciphertext analysis.
|
||||
"""
|
||||
device = models.ForeignKey(
|
||||
to='dcim.Device',
|
||||
@@ -320,7 +320,7 @@ class Secret(ChangeLoggedModel, CustomFieldModel):
|
||||
blank=True
|
||||
)
|
||||
ciphertext = models.BinaryField(
|
||||
max_length=65568, # 16B IV + 2B pad length + {62-65550}B padded
|
||||
max_length=65568, # 128-bit IV + 16-bit pad length + 65535B secret + 15B padding
|
||||
editable=False
|
||||
)
|
||||
hash = models.CharField(
|
||||
@@ -388,11 +388,7 @@ class Secret(ChangeLoggedModel, CustomFieldModel):
|
||||
else:
|
||||
pad_length = 0
|
||||
|
||||
# Python 2 compatibility
|
||||
if sys.version_info[0] < 3:
|
||||
header = chr(len(s) >> 8) + chr(len(s) % 256)
|
||||
else:
|
||||
header = bytes([len(s) >> 8]) + bytes([len(s) % 256])
|
||||
header = bytes([len(s) >> 8]) + bytes([len(s) % 256])
|
||||
|
||||
return header + s + os.urandom(pad_length)
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ from utilities.tables import BaseTable, ToggleColumn
|
||||
from .models import SecretRole, Secret
|
||||
|
||||
SECRETROLE_ACTIONS = """
|
||||
<a href="{% url 'secrets:secretrole_changelog' slug=record.slug %}" class="btn btn-default btn-xs" title="Changelog">
|
||||
<a href="{% url 'secrets:secretrole_changelog' slug=record.slug %}" class="btn btn-default btn-xs" title="Change log">
|
||||
<i class="fa fa-history"></i>
|
||||
</a>
|
||||
{% if perms.secrets.change_secretrole %}
|
||||
|
||||
@@ -5,7 +5,8 @@ from rest_framework import status
|
||||
|
||||
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
|
||||
from secrets.models import Secret, SecretRole, SessionKey, UserKey
|
||||
from utilities.testing import APITestCase
|
||||
from users.models import Token
|
||||
from utilities.testing import APITestCase, create_test_user
|
||||
from .constants import PRIVATE_KEY, PUBLIC_KEY
|
||||
|
||||
|
||||
@@ -131,7 +132,15 @@ class SecretTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
super().setUp()
|
||||
# Create a non-superuser test user
|
||||
self.user = create_test_user('testuser', permissions=(
|
||||
'secrets.add_secret',
|
||||
'secrets.change_secret',
|
||||
'secrets.delete_secret',
|
||||
'secrets.view_secret',
|
||||
))
|
||||
self.token = Token.objects.create(user=self.user)
|
||||
self.header = {'HTTP_AUTHORIZATION': 'Token {}'.format(self.token.key)}
|
||||
|
||||
userkey = UserKey(user=self.user, public_key=PUBLIC_KEY)
|
||||
userkey.save()
|
||||
@@ -144,11 +153,11 @@ class SecretTest(APITestCase):
|
||||
'HTTP_X_SESSION_KEY': base64.b64encode(session_key.key),
|
||||
}
|
||||
|
||||
self.plaintext = {
|
||||
'secret1': 'Secret #1 Plaintext',
|
||||
'secret2': 'Secret #2 Plaintext',
|
||||
'secret3': 'Secret #3 Plaintext',
|
||||
}
|
||||
self.plaintexts = (
|
||||
'Secret #1 Plaintext',
|
||||
'Secret #2 Plaintext',
|
||||
'Secret #3 Plaintext',
|
||||
)
|
||||
|
||||
site = Site.objects.create(name='Test Site 1', slug='test-site-1')
|
||||
manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
|
||||
@@ -160,17 +169,17 @@ class SecretTest(APITestCase):
|
||||
self.secretrole1 = SecretRole.objects.create(name='Test Secret Role 1', slug='test-secret-role-1')
|
||||
self.secretrole2 = SecretRole.objects.create(name='Test Secret Role 2', slug='test-secret-role-2')
|
||||
self.secret1 = Secret(
|
||||
device=self.device, role=self.secretrole1, name='Test Secret 1', plaintext=self.plaintext['secret1']
|
||||
device=self.device, role=self.secretrole1, name='Test Secret 1', plaintext=self.plaintexts[0]
|
||||
)
|
||||
self.secret1.encrypt(self.master_key)
|
||||
self.secret1.save()
|
||||
self.secret2 = Secret(
|
||||
device=self.device, role=self.secretrole1, name='Test Secret 2', plaintext=self.plaintext['secret2']
|
||||
device=self.device, role=self.secretrole1, name='Test Secret 2', plaintext=self.plaintexts[1]
|
||||
)
|
||||
self.secret2.encrypt(self.master_key)
|
||||
self.secret2.save()
|
||||
self.secret3 = Secret(
|
||||
device=self.device, role=self.secretrole1, name='Test Secret 3', plaintext=self.plaintext['secret3']
|
||||
device=self.device, role=self.secretrole1, name='Test Secret 3', plaintext=self.plaintexts[2]
|
||||
)
|
||||
self.secret3.encrypt(self.master_key)
|
||||
self.secret3.save()
|
||||
@@ -178,16 +187,32 @@ class SecretTest(APITestCase):
|
||||
def test_get_secret(self):
|
||||
|
||||
url = reverse('secrets-api:secret-detail', kwargs={'pk': self.secret1.pk})
|
||||
response = self.client.get(url, **self.header)
|
||||
|
||||
self.assertEqual(response.data['plaintext'], self.plaintext['secret1'])
|
||||
# Secret plaintext not be decrypted as the user has not been assigned to the role
|
||||
response = self.client.get(url, **self.header)
|
||||
self.assertIsNone(response.data['plaintext'])
|
||||
|
||||
# The plaintext should be present once the user has been assigned to the role
|
||||
self.secretrole1.users.add(self.user)
|
||||
response = self.client.get(url, **self.header)
|
||||
self.assertEqual(response.data['plaintext'], self.plaintexts[0])
|
||||
|
||||
def test_list_secrets(self):
|
||||
|
||||
url = reverse('secrets-api:secret-list')
|
||||
response = self.client.get(url, **self.header)
|
||||
|
||||
# Secret plaintext not be decrypted as the user has not been assigned to the role
|
||||
response = self.client.get(url, **self.header)
|
||||
self.assertEqual(response.data['count'], 3)
|
||||
for secret in response.data['results']:
|
||||
self.assertIsNone(secret['plaintext'])
|
||||
|
||||
# The plaintext should be present once the user has been assigned to the role
|
||||
self.secretrole1.users.add(self.user)
|
||||
response = self.client.get(url, **self.header)
|
||||
self.assertEqual(response.data['count'], 3)
|
||||
for i, secret in enumerate(response.data['results']):
|
||||
self.assertEqual(secret['plaintext'], self.plaintexts[i])
|
||||
|
||||
def test_create_secret(self):
|
||||
|
||||
|
||||
@@ -85,14 +85,19 @@ class UserKeyTestCase(TestCase):
|
||||
|
||||
class SecretTestCase(TestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
# Generate a random key for encryption/decryption of secrets
|
||||
cls.secret_key = generate_random_key()
|
||||
|
||||
def test_01_encrypt_decrypt(self):
|
||||
"""
|
||||
Test basic encryption and decryption functionality using a random master key.
|
||||
"""
|
||||
plaintext = string.printable * 2
|
||||
secret_key = generate_random_key()
|
||||
s = Secret(plaintext=plaintext)
|
||||
s.encrypt(secret_key)
|
||||
s.encrypt(self.secret_key)
|
||||
|
||||
# Ensure plaintext is deleted upon encryption
|
||||
self.assertIsNone(s.plaintext, "Plaintext must be None after encrypting.")
|
||||
@@ -112,7 +117,7 @@ class SecretTestCase(TestCase):
|
||||
self.assertFalse(s.validate("Invalid plaintext"), "Invalid plaintext validated against hash")
|
||||
|
||||
# Test decryption
|
||||
s.decrypt(secret_key)
|
||||
s.decrypt(self.secret_key)
|
||||
self.assertEqual(plaintext, s.plaintext, "Decrypting Secret returned incorrect plaintext")
|
||||
|
||||
def test_02_ciphertext_uniqueness(self):
|
||||
@@ -120,15 +125,45 @@ class SecretTestCase(TestCase):
|
||||
Generate 50 Secrets using the same plaintext and check for duplicate IVs or payloads.
|
||||
"""
|
||||
plaintext = "1234567890abcdef"
|
||||
secret_key = generate_random_key()
|
||||
ivs = []
|
||||
ciphertexts = []
|
||||
for i in range(1, 51):
|
||||
s = Secret(plaintext=plaintext)
|
||||
s.encrypt(secret_key)
|
||||
s.encrypt(self.secret_key)
|
||||
ivs.append(s.ciphertext[0:16])
|
||||
ciphertexts.append(s.ciphertext[16:32])
|
||||
duplicate_ivs = [i for i, x in enumerate(ivs) if ivs.count(x) > 1]
|
||||
self.assertEqual(duplicate_ivs, [], "One or more duplicate IVs found!")
|
||||
duplicate_ciphertexts = [i for i, x in enumerate(ciphertexts) if ciphertexts.count(x) > 1]
|
||||
self.assertEqual(duplicate_ciphertexts, [], "One or more duplicate ciphertexts (first blocks) found!")
|
||||
|
||||
def test_minimum_length(self):
|
||||
"""
|
||||
Test enforcement of the minimum length for ciphertexts.
|
||||
"""
|
||||
plaintext = 'A' # One-byte plaintext
|
||||
secret = Secret(plaintext=plaintext)
|
||||
secret.encrypt(self.secret_key)
|
||||
|
||||
# 16B IV + 2B length + 1B secret + 61B padding = 80 bytes
|
||||
self.assertEqual(len(secret.ciphertext), 80)
|
||||
self.assertIsNone(secret.plaintext)
|
||||
|
||||
secret.decrypt(self.secret_key)
|
||||
self.assertEqual(secret.plaintext, plaintext)
|
||||
|
||||
def test_maximum_length(self):
|
||||
"""
|
||||
Test encrypting a plaintext value of the maximum length.
|
||||
"""
|
||||
plaintext = '0123456789abcdef' * 4096
|
||||
plaintext = plaintext[:65535] # 65,535 chars
|
||||
secret = Secret(plaintext=plaintext)
|
||||
secret.encrypt(self.secret_key)
|
||||
|
||||
# 16B IV + 2B length + 65535B secret + 15B padding = 65568 bytes
|
||||
self.assertEqual(len(secret.ciphertext), 65568)
|
||||
self.assertIsNone(secret.plaintext)
|
||||
|
||||
secret.decrypt(self.secret_key)
|
||||
self.assertEqual(secret.plaintext, plaintext)
|
||||
|
||||
@@ -4,18 +4,13 @@ from django.urls import reverse
|
||||
|
||||
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
|
||||
from secrets.models import Secret, SecretRole, SessionKey, UserKey
|
||||
from utilities.testing import StandardTestCases
|
||||
from utilities.testing import ViewTestCases
|
||||
from .constants import PRIVATE_KEY, PUBLIC_KEY
|
||||
|
||||
|
||||
class SecretRoleTestCase(StandardTestCases.Views):
|
||||
class SecretRoleTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
||||
model = SecretRole
|
||||
|
||||
# Disable inapplicable tests
|
||||
test_get_object = None
|
||||
test_delete_object = None
|
||||
test_bulk_edit_objects = None
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
@@ -41,7 +36,7 @@ class SecretRoleTestCase(StandardTestCases.Views):
|
||||
)
|
||||
|
||||
|
||||
class SecretTestCase(StandardTestCases.Views):
|
||||
class SecretTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
model = Secret
|
||||
|
||||
# Disable inapplicable tests
|
||||
|
||||
@@ -8,21 +8,21 @@ app_name = 'secrets'
|
||||
urlpatterns = [
|
||||
|
||||
# Secret roles
|
||||
path(r'secret-roles/', views.SecretRoleListView.as_view(), name='secretrole_list'),
|
||||
path(r'secret-roles/add/', views.SecretRoleCreateView.as_view(), name='secretrole_add'),
|
||||
path(r'secret-roles/import/', views.SecretRoleBulkImportView.as_view(), name='secretrole_import'),
|
||||
path(r'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(r'secret-roles/<slug:slug>/changelog/', ObjectChangeLogView.as_view(), name='secretrole_changelog', kwargs={'model': SecretRole}),
|
||||
path('secret-roles/', views.SecretRoleListView.as_view(), name='secretrole_list'),
|
||||
path('secret-roles/add/', views.SecretRoleCreateView.as_view(), name='secretrole_add'),
|
||||
path('secret-roles/import/', views.SecretRoleBulkImportView.as_view(), name='secretrole_import'),
|
||||
path('secret-roles/delete/', views.SecretRoleBulkDeleteView.as_view(), name='secretrole_bulk_delete'),
|
||||
path('secret-roles/<slug:slug>/edit/', views.SecretRoleEditView.as_view(), name='secretrole_edit'),
|
||||
path('secret-roles/<slug:slug>/changelog/', ObjectChangeLogView.as_view(), name='secretrole_changelog', kwargs={'model': SecretRole}),
|
||||
|
||||
# Secrets
|
||||
path(r'secrets/', views.SecretListView.as_view(), name='secret_list'),
|
||||
path(r'secrets/import/', views.SecretBulkImportView.as_view(), name='secret_import'),
|
||||
path(r'secrets/edit/', views.SecretBulkEditView.as_view(), name='secret_bulk_edit'),
|
||||
path(r'secrets/delete/', views.SecretBulkDeleteView.as_view(), name='secret_bulk_delete'),
|
||||
path(r'secrets/<int:pk>/', views.SecretView.as_view(), name='secret'),
|
||||
path(r'secrets/<int:pk>/edit/', views.secret_edit, name='secret_edit'),
|
||||
path(r'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/', views.SecretListView.as_view(), name='secret_list'),
|
||||
path('secrets/import/', views.SecretBulkImportView.as_view(), name='secret_import'),
|
||||
path('secrets/edit/', views.SecretBulkEditView.as_view(), name='secret_bulk_edit'),
|
||||
path('secrets/delete/', views.SecretBulkDeleteView.as_view(), name='secret_bulk_delete'),
|
||||
path('secrets/<int:pk>/', views.SecretView.as_view(), name='secret'),
|
||||
path('secrets/<int:pk>/edit/', views.secret_edit, name='secret_edit'),
|
||||
path('secrets/<int:pk>/delete/', views.SecretDeleteView.as_view(), name='secret_delete'),
|
||||
path('secrets/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='secret_changelog', kwargs={'model': Secret}),
|
||||
|
||||
]
|
||||
|
||||
@@ -35,7 +35,6 @@ class SecretRoleListView(PermissionRequiredMixin, ObjectListView):
|
||||
permission_required = 'secrets.view_secretrole'
|
||||
queryset = SecretRole.objects.annotate(secret_count=Count('secrets'))
|
||||
table = tables.SecretRoleTable
|
||||
template_name = 'secrets/secretrole_list.html'
|
||||
|
||||
|
||||
class SecretRoleCreateView(PermissionRequiredMixin, ObjectEditView):
|
||||
@@ -73,7 +72,7 @@ class SecretListView(PermissionRequiredMixin, ObjectListView):
|
||||
filterset = filters.SecretFilterSet
|
||||
filterset_form = forms.SecretFilterForm
|
||||
table = tables.SecretTable
|
||||
template_name = 'secrets/secret_list.html'
|
||||
action_buttons = ('import', 'export')
|
||||
|
||||
|
||||
class SecretView(PermissionRequiredMixin, View):
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user