Compare commits

..

73 Commits

Author SHA1 Message Date
Jeremy Stretch
27a893a9a1 Merge pull request #2688 from digitalocean/develop
Release v2.5.1
2018-12-13 15:20:09 -05:00
Jeremy Stretch
8863a3126d Merge pull request #2660 from digitalocean/develop
Release v2.5.0
2018-12-10 10:27:24 -05:00
Jeremy Stretch
bf0083552d Merge pull request #2653 from digitalocean/develop
Release v2.4.9
2018-12-07 10:25:46 -05:00
Jeremy Stretch
8d4329197a Merge pull request #2600 from digitalocean/develop
Release v2.4.8
2018-11-20 11:58:29 -05:00
Jeremy Stretch
cb83eb204b Merge pull request #2552 from digitalocean/develop
Release v2.4.7
2018-11-06 10:55:29 -05:00
Jeremy Stretch
74d525364a Merge pull request #2494 from digitalocean/develop
Release v2.4.6
2018-10-05 15:48:11 -04:00
Jeremy Stretch
125975832b Merge pull request #2478 from digitalocean/develop
Release v2.4.5
2018-10-02 15:29:13 -04:00
Jeremy Stretch
bcf22831e2 Merge pull request #2387 from digitalocean/develop
Release v2.4.4
2018-08-22 11:53:56 -04:00
Jeremy Stretch
3b26ce6501 Merge pull request #2386 from digitalocean/revert-2376-patch-1
Revert "Add missing library"
2018-08-22 11:44:31 -04:00
Jeremy Stretch
1b2d3bf08b Revert "Add missing library" 2018-08-22 11:44:07 -04:00
Jeremy Stretch
492bc9f86e Merge pull request #2376 from craig/patch-1
Add missing library
2018-08-22 11:43:46 -04:00
Craig
967feb6931 Add missing library
WSGIPassAuthorization fails if libapache2-mod-wsgi-py3 is missing
2018-08-21 00:41:29 +02:00
Jeremy Stretch
f224ad2959 Merge pull request #2346 from digitalocean/develop
Release v2.4.3
2018-08-09 16:39:45 -04:00
Jeremy Stretch
242cb7c7cb Merge pull request #2332 from digitalocean/develop
Release v2.4.2
2018-08-08 09:16:50 -04:00
Jeremy Stretch
ea7386b04b Merge pull request #2316 from digitalocean/develop
Release v2.4.1
2018-08-07 09:25:10 -04:00
Jeremy Stretch
7a27dbb374 Merge pull request #2307 from digitalocean/develop
Release v2.4.0
2018-08-06 12:40:00 -04:00
Jeremy Stretch
a85e6370a8 Merge pull request #2275 from digitalocean/develop
Release v2.3.7
2018-07-26 14:29:15 -04:00
Jeremy Stretch
09a03565d7 Merge pull request #2244 from digitalocean/develop
Release v2.3.6
2018-07-16 11:54:12 -04:00
Jeremy Stretch
6159994552 Merge pull request #2212 from digitalocean/develop
Release v2.3.5
2018-07-02 15:55:25 -04:00
Jeremy Stretch
a1f624c1cc Merge pull request #2152 from digitalocean/develop
Release v2.3.4
2018-06-07 16:14:18 -04:00
Jeremy Stretch
328958876a Merge pull request #2041 from digitalocean/develop
Release v2.3.3
2018-04-19 11:15:48 -04:00
Jeremy Stretch
68f73c7f94 Merge pull request #1987 from digitalocean/develop
Release v2.3.2
2018-03-22 15:05:59 -04:00
Jeremy Stretch
ec4d28ac6c Merge pull request #1937 from digitalocean/develop
Release v2.3.1
2018-03-01 15:36:10 -05:00
Jeremy Stretch
957074a134 Merge pull request #1913 from digitalocean/develop
Release v2.3.0
2018-02-26 14:23:03 -05:00
Jeremy Stretch
c4f7e8121a Merge pull request #1903 from digitalocean/develop
Release v2.2.10
2018-02-21 16:05:45 -05:00
Jeremy Stretch
6436d703f5 Merge pull request #1852 from digitalocean/develop
Release v2.2.9
2018-01-31 10:43:20 -05:00
Jeremy Stretch
ec0cb7a8bc Merge pull request #1789 from digitalocean/develop
Release v2.2.8
2017-12-20 15:27:22 -05:00
Jeremy Stretch
e98f0c39d1 Merge pull request #1757 from digitalocean/develop
Release v2.2.7
2017-12-07 14:52:28 -05:00
Jeremy Stretch
50a451eddc Merge pull request #1720 from digitalocean/develop
Release v2.2.6
2017-11-16 12:00:34 -05:00
Jeremy Stretch
a5a7358d26 Merge pull request #1708 from digitalocean/develop
Release v2.2.5
2017-11-14 13:25:11 -05:00
Jeremy Stretch
f9452163c5 Merge pull request #1671 from digitalocean/develop
Release v2.2.4
2017-10-31 15:21:23 -04:00
Jeremy Stretch
3067c3f262 Merge pull request #1668 from digitalocean/develop
Release v2.2.3
2017-10-31 14:02:15 -04:00
Jeremy Stretch
7a64404299 Merge pull request #1614 from digitalocean/develop
Release v2.2.2
2017-10-17 11:24:02 -04:00
Jeremy Stretch
2bda399982 Merge pull request #1577 from digitalocean/develop
Release v2.2.1
2017-10-12 16:11:17 -04:00
Jeremy Stretch
74731bc6ae Merge pull request #1575 from digitalocean/develop
Release v2.2.0
2017-10-12 14:01:28 -04:00
Jeremy Stretch
7cb287d6c6 Merge pull request #1572 from digitalocean/develop
Release v2.1.6
2017-10-11 13:02:32 -04:00
Jeremy Stretch
aa8f734bd1 Merge pull request #1537 from digitalocean/develop
Release v2.1.5
2017-09-25 14:52:43 -04:00
Jeremy Stretch
f6d1163ddd Merge pull request #1461 from digitalocean/develop
Release v2.1.4
2017-08-30 14:43:01 -04:00
Jeremy Stretch
5be30bd278 Merge pull request #1428 from digitalocean/develop
Release v2.1.3
2017-08-15 15:52:34 -04:00
Jeremy Stretch
fa7b7288c9 Merge pull request #1398 from digitalocean/develop
Release v2.1.2
2017-08-04 10:54:29 -04:00
Jeremy Stretch
9cc03aaa9a Merge pull request #1387 from digitalocean/develop
Release v2.1.1
2017-08-02 14:22:30 -04:00
Jeremy Stretch
1bda56ea23 Merge pull request #1372 from digitalocean/develop
Release v2.1.0
2017-07-25 11:21:44 -04:00
Jeremy Stretch
64a34ced72 Merge pull request #1346 from digitalocean/develop
Release v2.0.10
2017-07-14 10:09:16 -04:00
Jeremy Stretch
e05d379101 Merge pull request #1327 from digitalocean/develop
Release v2.0.9
2017-07-10 09:43:59 -04:00
Jeremy Stretch
a355783377 Merge pull request #1316 from digitalocean/develop
Release v2.0.8
2017-07-05 14:36:08 -04:00
Jeremy Stretch
88239e0b0d Merge pull request #1278 from digitalocean/develop
Release v2.0.7
2017-06-15 14:26:38 -04:00
Jeremy Stretch
5c63a499d5 Merge pull request #1259 from digitalocean/develop
Release v2.0.6
2017-06-12 09:51:15 -04:00
Jeremy Stretch
50496b1a59 Merge pull request #1251 from digitalocean/develop
Release v2.0.5
2017-06-08 10:10:41 -04:00
Jeremy Stretch
f7b0d22f86 Merge pull request #1230 from digitalocean/develop
Release v2.0.4
2017-05-25 14:45:13 -04:00
Jeremy Stretch
ad95b86fdd Merge pull request #1201 from digitalocean/develop
Release v2.0.3
2017-05-18 14:37:19 -04:00
Jeremy Stretch
43e1e0dbc8 Merge pull request #1181 from digitalocean/develop
Release v2.0.2
2017-05-15 13:23:33 -04:00
Jeremy Stretch
f731900e2f Merge pull request #1154 from digitalocean/develop
Release v2.0.1
2017-05-09 22:47:52 -04:00
Jeremy Stretch
b1bcaa33e7 Merge pull request #1148 from digitalocean/develop
Release v2.0.0
2017-05-09 15:09:28 -04:00
Jeremy Stretch
17873706b7 Merge pull request #1094 from digitalocean/develop
Release v1.9.6
2017-04-21 14:52:53 -04:00
Jeremy Stretch
e0ad2b4555 Merge pull request #1054 from digitalocean/develop
Release v1.9.5
2017-04-06 16:35:15 -04:00
Jeremy Stretch
f89d91783b Merge pull request #1035 from digitalocean/develop
Release v1.9.4-r1
2017-04-04 15:50:28 -04:00
Jeremy Stretch
3ffe36e5ed Merge pull request #1032 from digitalocean/develop
Release v1.9.4
2017-04-04 12:01:58 -04:00
Jeremy Stretch
be393a9d10 Merge pull request #989 from digitalocean/develop
Release v1.9.3
2017-03-23 16:27:06 -04:00
Jeremy Stretch
27eefd8705 Merge pull request #966 from digitalocean/develop
Release v1.9.2
2017-03-14 17:14:19 -04:00
Jeremy Stretch
097e0f38ff Merge pull request #949 from digitalocean/develop
Release v1.9.1
2017-03-08 14:40:16 -05:00
Jeremy Stretch
ce26b566a4 Merge pull request #939 from digitalocean/develop
Release v1.9.0-r1
2017-03-03 11:28:02 -05:00
Jeremy Stretch
0e14bc1e02 Merge pull request #933 from digitalocean/develop
Release v1.9.0
2017-03-02 13:27:10 -05:00
Jeremy Stretch
ce6796ed9b Merge pull request #870 from digitalocean/develop
Release v1.8.4
2017-02-03 13:59:02 -05:00
Jeremy Stretch
c90cecc2fb Merge pull request #849 from digitalocean/develop
Release v1.8.3
2017-01-26 13:58:52 -05:00
Jeremy Stretch
b6bbcb0609 Merge pull request #814 from digitalocean/develop
Release v1.8.2
2017-01-18 16:23:28 -05:00
Jeremy Stretch
23f6832d9c Merge pull request #774 from digitalocean/develop
Release v1.8.1
2017-01-04 15:30:54 -05:00
Jeremy Stretch
88dace75a1 Merge pull request #766 from digitalocean/develop
Release v1.8.0
2017-01-03 15:13:36 -05:00
Jeremy Stretch
8eb140fd65 Merge pull request #736 from digitalocean/develop
Release v1.7.3
2016-12-08 12:34:53 -05:00
Jeremy Stretch
1f09f3d096 Merge pull request #728 from digitalocean/develop
Release v1.7.2-r1
2016-12-06 15:38:52 -05:00
Jeremy Stretch
66be85a41f Merge pull request #726 from digitalocean/develop
Release v1.7.2
2016-12-06 14:55:19 -05:00
Jeremy Stretch
814c11167e Merge pull request #694 from digitalocean/develop
Release v1.7.1
2016-11-15 12:34:09 -05:00
Jeremy Stretch
57ddd5086f Merge pull request #666 from digitalocean/develop
Release v1.7.0
2016-11-03 15:12:33 -04:00
Jeremy Stretch
c171547037 Merge pull request #625 from digitalocean/develop
Release v1.6.3
2016-10-19 16:25:50 -04:00
336 changed files with 4869 additions and 25448 deletions

View File

@@ -17,20 +17,15 @@ about: Report a reproducible bug in the current release of NetBox
-->
### Environment
* Python version: <!-- Example: 3.5.4 -->
* NetBox version: <!-- Example: 2.5.2 -->
* NetBox version: <!-- Example: 2.3.6 -->
<!--
Describe in detail the exact steps that someone else can take to reproduce
this bug using the current stable release of NetBox (or the current beta
release where applicable). Begin with the creation of any necessary
database objects and call out every operation being performed explicitly.
If reporting a bug in the REST API, be sure to reconstruct the raw HTTP
request(s) being made: Don't rely on a wrapper like pynetbox.
Describe in detail the steps that someone else can take to reproduce this
bug using the current stable release of NetBox (or the current beta release
where applicable).
-->
### Steps to Reproduce
1.
2.
3.
<!-- What did you expect to happen? -->
### Expected Behavior

2
.gitignore vendored
View File

@@ -10,5 +10,3 @@
fabfile.py
*.swp
gunicorn_config.py
.DS_Store
.vscode

View File

@@ -1,7 +1,6 @@
sudo: required
services:
- postgresql
- redis-server
addons:
postgresql: "9.4"
language: python

File diff suppressed because it is too large Load Diff

View File

@@ -37,7 +37,7 @@ and run `upgrade.sh`.
## Alternative Installations
* [Docker container](https://github.com/netbox-community/netbox-docker) (via [@cimnine](https://github.com/cimnine))
* [Docker container](https://github.com/ninech/netbox-docker) (via [@cimnine](https://github.com/cimnine))
* [Vagrant deployment](https://github.com/ryanmerolle/netbox-vagrant) (via [@ryanmerolle](https://github.com/ryanmerolle))
* [Ansible deployment](https://github.com/lae/ansible-role-netbox) (via [@lae](https://github.com/lae))
@@ -45,13 +45,13 @@ and run `upgrade.sh`.
## Supported SDK
- [pynetbox](https://github.com/digitalocean/pynetbox) - A Python API client library for Netbox
- [pynetbox](https://github.com/digitalocean/pynetbox) Python API client library for Netbox.
## Community SDK
- [netbox-client-ruby](https://github.com/ninech/netbox-client-ruby) - A Ruby client library for Netbox
- [powerbox](https://github.com/BatmanAMA/powerbox) - A PowerShell library for Netbox
- [netbox-client-ruby](https://github.com/ninech/netbox-client-ruby) A ruby client library for Netbox v2.
## Ansible Inventory
- [netbox-as-ansible-inventory](https://github.com/AAbouZaid/netbox-as-ansible-inventory) - Ansible dynamic inventory script for Netbox
- [netbox-as-ansible-inventory](https://github.com/AAbouZaid/netbox-as-ansible-inventory) Ansible dynamic inventory script for Netbox.

View File

@@ -2,10 +2,6 @@
# https://github.com/django/django
Django
# Django caching using Redis
# https://github.com/Suor/django-cacheops
django-cacheops
# Django middleware which permits cross-domain API requests
# https://github.com/OttoYiu/django-cors-headers
django-cors-headers
@@ -22,14 +18,6 @@ django-filter
# https://github.com/django-mptt/django-mptt
django-mptt
# Django integration for RQ (Reqis queuing)
# https://github.com/rq/django-rq
django-rq
# Prometheus metrics library for Django
# https://github.com/korfuri/django-prometheus
django-prometheus
# Abstraction models for rendering and paginating HTML tables
# https://github.com/jieter/django-tables2
django-tables2

View File

@@ -1,21 +0,0 @@
# Caching
To improve performance, NetBox supports caching for most object and list views. Caching is implemented using Redis,
and [django-cacheops](https://github.com/Suor/django-cacheops)
Several management commands are avaliable for administrators to manaully invalidate cache entries in extenuating circumstances.
To invalidate a specifc model instance (for example a Device with ID 34):
```
python netbox/manage.py invalidate dcim.Device.34
```
To invalidate all instance of a model:
```
python netbox/manage.py invalidate dcim.Device
```
To flush the entire cache database:
```
python netbox/manage.py invalidate all
```

View File

@@ -1,22 +0,0 @@
# Prometheus Metrics
NetBox supports optionally exposing native Prometheus metrics from the application. [Prometheus](https://prometheus.io/) is a popular time series metric platform used for monitoring.
NetBox exposes metrics at the `/metrics` HTTP endpoint, e.g. `https://netbox.local/metrics`. Metric exposition can be toggled with the `METRICS_ENABLED` configuration setting. Metrics are exposed by default.
## Metric Types
NetBox makes use of the [django-prometheus](https://github.com/korfuri/django-prometheus) library to export a number of different types of metrics, including:
- Per model insert, update, and delete counters
- Per view request counters
- Per view request latency histograms
- Request body size histograms
- Response body size histograms
- Response code counters
- Database connection, execution, and error counters
- Cache hit, miss, and invalidation counters
- Django middleware latency histograms
- Other Django related metadata metrics
For the exhaustive list of exposed metrics, visit the `/metrics` endpoint on your NetBox instance.

View File

@@ -44,7 +44,7 @@ class DeviceConnectionsReport(Report):
# Check that every console port for every active device has a connection defined.
for console_port in ConsolePort.objects.select_related('device').filter(device__status=DEVICE_STATUS_ACTIVE):
if console_port.connected_endpoint is None:
if console_port.cs_port is None:
self.log_failure(
console_port.device,
"No console connection defined for {}".format(console_port.name)
@@ -63,7 +63,7 @@ class DeviceConnectionsReport(Report):
for device in Device.objects.filter(status=DEVICE_STATUS_ACTIVE):
connected_ports = 0
for power_port in PowerPort.objects.filter(device=device):
if power_port.connected_endpoint is not None:
if power_port.power_outlet is not None:
connected_ports += 1
if power_port.connection_status == CONNECTION_STATUS_PLANNED:
self.log_warning(
@@ -128,4 +128,4 @@ Reports can be run on the CLI by invoking the management command:
python3 manage.py runreport <module>
```
where ``<module>`` is the name of the python file in the ``reports`` directory without the ``.py`` extension. One or more report modules may be specified.
One or more report modules may be specified.

View File

@@ -1,6 +1,6 @@
# Tags
Tags are free-form text labels which can be applied to a variety of objects within NetBox. Tags are created on-demand as they are assigned to objects. Use commas to separate tags when adding multiple tags to an object (for example: `Inventoried, Monitored`). Use double quotes around a multi-word tag when adding only one tag, e.g. `"Core Switch"`.
Tags are free-form text labels which can be applied to a variety of objects within NetBox. Tags are created on-demand as they are assigned to objects. Use commas to separate tags when adding multiple tags to an object 9for example: `Inventoried, Monitored`). Use double quotes around a multi-word tag when adding only one tag, e.g. `"Core Switch"`.
Each tag has a label and a URL-friendly slug. For example, the slug for a tag named "Dunder Mifflin, Inc." would be `dunder-mifflin-inc`. The slug is generated automatically and makes tags easier to work with as URL parameters.

View File

@@ -4,6 +4,14 @@ A webhook defines an HTTP request that is sent to an external application when c
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.
## Installation
If you are upgrading from a previous version of Netbox and want to enable the webhook feature, please follow the directions listed in the sections below.
* [Install Redis server and djano-rq package](../installation/2-netbox/#install-python-packages)
* [Modify configuration to enable webhooks](../installation/2-netbox/#webhooks-configuration)
* [Create supervisord program to run the rqworker process](../installation/3-http-daemon/#supervisord-installation)
## Requests
The webhook POST request is structured as so (assuming `application/json` as the Content-Type):

View File

@@ -104,37 +104,24 @@ The base serializer is used to represent the default view of a model. This inclu
}
```
## Related Objects
Related objects (e.g. `ForeignKey` fields) are represented using a nested serializer. A nested serializer provides a minimal representation of an object, including only its URL and enough information to construct its name. When performing write api actions (`POST`, `PUT`, and `PATCH`), any `ForeignKey` relationships do not use the nested serializer, instead you will pass just the integer ID of the related model.
Related objects (e.g. `ForeignKey` fields) are represented using a nested serializer. A nested serializer provides a minimal representation of an object, including only its URL and enough information to display the object to a user. When performing write API actions (`POST`, `PUT`, and `PATCH`), related objects may be specified by either numeric ID (primary key), or by a set of attributes sufficiently unique to return the desired object.
For example, when creating a new device, its rack can be specified by NetBox ID (PK):
When a base serializer includes one or more nested serializers, the hierarchical structure precludes it from being used for write operations. Thus, a flat representation of an object may be provided using a writable serializer. This serializer includes only raw database values and is not typically used for retrieval, except as part of the response to the creation or updating of an object.
```
{
"name": "MyNewDevice",
"rack": 123,
...
"id": 1201,
"site": 7,
"group": 4,
"vid": 102,
"name": "Users-Floor2",
"tenant": null,
"status": 1,
"role": 9,
"description": ""
}
```
Or by a set of nested attributes used to identify the rack:
```
{
"name": "MyNewDevice",
"rack": {
"site": {
"name": "Equinix DC6"
},
"name": "R204"
},
...
}
```
Note that if the provided parameters do not return exactly one object, a validation error is raised.
## Brief Format
Most API endpoints support an optional "brief" format, which returns only a minimal representation of each object in the response. This is useful when you need only a list of the objects themselves without any related data, such as when populating a drop-down list in a form.
@@ -274,7 +261,7 @@ A list of objects retrieved via the API can be filtered by passing one or more q
GET /api/ipam/prefixes/?status=1
```
Certain filters can be included multiple times within a single request. These will effect a logical OR and return objects matching any of the given values. For example, the following will return all active and reserved prefixes:
The same filter can be incldued multiple times. These will effect a logical OR and return objects matching any of the given values. For example, the following will return all active and reserved prefixes:
```
GET /api/ipam/prefixes/?status=1&status=2

View File

@@ -44,14 +44,6 @@ BASE_PATH = 'netbox/'
---
## CACHE_TIMEOUT
Default: 900
The number of seconds to retain cache entries before automatically invalidating them.
---
## CHANGELOG_RETENTION
Default: 90
@@ -97,30 +89,6 @@ In order to send email, NetBox needs an email server configured. The following i
---
## EXEMPT_VIEW_PERMISSIONS
Default: Empty list
A list of models to exempt from the enforcement of view permissions. Models listed here will be viewable by all users and by anonymous users.
List models in the form `<app>.<model>`. For example:
```
EXEMPT_VIEW_PERMISSIONS = [
'dcim.site',
'dcim.region',
'ipam.prefix',
]
```
To exempt _all_ models from view permission enforcement, set the following. (Note that `EXEMPT_VIEW_PERMISSIONS` must be an iterable.)
```
EXEMPT_VIEW_PERMISSIONS = ['*']
```
---
# ENFORCE_GLOBAL_UNIQUE
Default: False
@@ -197,14 +165,6 @@ The file path to the location where media files (such as image attachments) are
---
## METRICS_ENABLED
Default: True
Toggle exposing Prometheus metrics at `/metrics`. See the [Prometheus Metrics](../additional-features/prometheus-metrics/) documentation for more details.
---
## NAPALM_USERNAME
## NAPALM_PASSWORD
@@ -291,7 +251,7 @@ The time zone NetBox will use when dealing with dates and times. It is recommend
Default: False
Enable this option to run the webhook backend. See the docs section on the webhook backend [here](../additional-features/webhooks/) for more information on setup and use.
Enable this option to run the webhook backend. See the docs section on the webhook backend [here](../miscellaneous/webhooks/) for more information on setup and use.
---
@@ -309,3 +269,49 @@ SHORT_TIME_FORMAT = 'H:i:s' # 13:23:00
DATETIME_FORMAT = 'N j, Y g:i a' # June 26, 2016 1:23 p.m.
SHORT_DATETIME_FORMAT = 'Y-m-d H:i' # 2016-06-27 13:23
```
---
## Redis Connection Settings
[Redis](https://redis.io/) is a key-value store which functions as a very lightweight database. It is required when enabling NetBox [webhooks](../miscellaneous/webhooks/). A Redis connection is configured using a dictionary similar to the following:
```
REDIS = {
'HOST': 'localhost',
'PORT': 6379,
'PASSWORD': '',
'DATABASE': 0,
'DEFAULT_TIMEOUT': 300,
}
```
### DATABASE
Default: 0
The Redis database ID.
### DEFAULT_TIMEOUT
Default: 300
The timeout value to use when connecting to the Redis server (in seconds).
### HOST
Default: localhost
The hostname or IP address of the Redis server.
### PORT
Default: 6379
The TCP port to use when connecting to the Redis server.
### PASSWORD
Default: None
The password to use when authenticating to the Redis server (optional).

View File

@@ -43,44 +43,3 @@ This is a secret cryptographic key is used to improve the security of cookies an
Please note that this key is **not** used for hashing user passwords or for the encrypted storage of secret data in NetBox.
`SECRET_KEY` should be at least 50 characters in length and contain a random mix of letters, digits, and symbols. The script located at `netbox/generate_secret_key.py` may be used to generate a suitable key.
---
## REDIS
[Redis](https://redis.io/) is an in-memory data store similar to memcached. While Redis has been an optional component of
NetBox since the introduction of webhooks in version 2.4, it is required starting in 2.6 to support NetBox's caching
functionality (as well as other planned features).
Redis is configured using a configuration setting similar to `DATABASE`:
* HOST - Name or IP address of the Redis server (use `localhost` if running locally)
* PORT - TCP port of the Redis service; leave blank for default port (6379)
* PASSWORD - Redis password (if set)
* DATABASE - Numeric database ID for webhooks
* CACHE_DATABASE - Numeric database ID for caching
* DEFAULT_TIMEOUT - Connection timeout in seconds
* SSL - Use SSL connection to Redis
Example:
```
REDIS = {
'HOST': 'localhost',
'PORT': 6379,
'PASSWORD': '',
'DATABASE': 0,
'CACHE_DATABASE': 1,
'DEFAULT_TIMEOUT': 300,
'SSL': False,
}
```
!!! note:
If you were using these settings in a prior release with webhooks, the `DATABASE` setting remains the same but
an additional `CACHE_DATABASE` setting has been added with a default value of 1 to support the caching backend. The
`DATABASE` setting will be renamed in a future release of NetBox to better relay the meaning of the setting.
!!! warning:
It is highly recommended to keep the webhook and cache databases seperate. Using the same database number for both may result in webhook
processing data being lost in cache flushing events.

View File

@@ -13,10 +13,6 @@ Some devices house child devices which share physical resources, like space and
!!! note
This parent/child relationship is **not** suitable for modeling chassis-based devices, wherein child members share a common control plane.
For that application you should create a single Device for the chassis, and add Interfaces directly to it. Interfaces can be created in bulk using range patterns, e.g. "Gi1/[1-24]".
Add Inventory Items if you want to record the line cards themselves as separate entities. There is no explicit relationship between each interface and its line card, but it may be implied by the naming (e.g. interfaces "Gi1/x" are on line card 1)
## Manufacturers
Each device type must be assigned to a manufacturer. The model number of a device type must be unique to its manufacturer.
@@ -81,7 +77,7 @@ Power ports connect only to power outlets. Power connections can be marked as ei
Interfaces connect to one another in a symmetric manner: If interface A connects to interface B, interface B therefore connects to interface A. Each type of connection can be classified as either *planned* or *connected*.
Each interface is a assigned a type denoting its physical properties. Two special types exist: the "virtual" type can be used to designate logical interfaces (such as SVIs), and the "LAG" type can be used to desinate link aggregation groups to which physical interfaces can be assigned.
Each interface is a assigned a form factor denoting its physical properties. Two special form factors exist: the "virtual" form factor can be used to designate logical interfaces (such as SVIs), and the "LAG" form factor can be used to desinate link aggregation groups to which physical interfaces can be assigned.
Each interface can also be enabled or disabled, and optionally designated as management-only (for out-of-band management). Fields are also provided to store an interface's MTU and MAC address.
@@ -97,10 +93,6 @@ Pass-through ports can also be used to model "bump in the wire" devices, such as
Device bays represent the ability of a device to house child devices. For example, you might install four blade servers into a 2U chassis. The chassis would appear in the rack elevation as a 2U device with four device bays. Each server within it would be defined as a 0U device installed in one of the device bays. Child devices do not appear within rack elevations, but they are included in the "Non-Racked Devices" list within the rack view.
Child devices are first-class Devices in their own right: that is, fully independent managed entities which don't share any control plane with the parent. Just like normal devices, child devices have their own platform (OS), role, tags, and interfaces. You cannot create a LAG between interfaces in different child devices.
Therefore, Device bays are **not** suitable for modeling chassis-based switches and routers. These should instead be modeled as a single Device, with the line cards as Inventory Items.
## Device Roles
Devices can be organized by functional roles. These roles are fully customizable. For example, you might create roles for core switches, distribution switches, and access switches.
@@ -119,7 +111,7 @@ The assignment of platforms to devices is an optional feature, and may be disreg
# Inventory Items
Inventory items represent hardware components installed within a device, such as a power supply or CPU or line card. Currently, these are used merely for inventory tracking, although future development might see their functionality expand. Like device types, each item can optionally be assigned a manufacturer.
Inventory items represent hardware components installed within a device, such as a power supply or CPU. Currently, these are used merely for inventory tracking, although future development might see their functionality expand. Like device types, each item can optionally be assigned a manufacturer.
---

View File

@@ -83,7 +83,7 @@ An IP address can be designated as the network address translation (NAT) inside
A VRF object in NetBox represents a virtual routing and forwarding (VRF) domain. Each VRF is essentially a separate routing table. VRFs are commonly used to isolate customers or organizations from one another within a network, or to route overlapping address space (e.g. multiple instances of the 10.0.0.0/8 space).
Each VRF is assigned a unique name and an optional route distinguisher (RD). The RD is expected to take one of the forms prescribed in [RFC 4364](https://tools.ietf.org/html/rfc4364#section-4.2), however its formatting is not strictly enforced.
Each VRF is assigned a unique name and route distinguisher (RD). The RD is expected to take one of the forms prescribed in [RFC 4364](https://tools.ietf.org/html/rfc4364#section-4.2), however its formatting is not strictly enforced.
Each prefix and IP address may be assigned to one (and only one) VRF. If you have a prefix or IP address which exists in multiple VRFs, you will need to create a separate instance of it in NetBox for each VRF. Any IP prefix or address not assigned to a VRF is said to belong to the "global" table.

View File

@@ -29,7 +29,6 @@ Update the following static libraries to their most recent stable release:
* Bootstrap 3
* Font Awesome 4
* Select2
* jQuery
* jQuery UI

View File

@@ -1,18 +1,18 @@
# Installation
This section of the documentation discusses installing and configuring the NetBox application. Begin by installing all system packages required by NetBox and its dependencies:
This section of the documentation discusses installing and configuring the NetBox application.
**Ubuntu**
```no-highlight
# apt-get install -y python3 python3-pip python3-dev build-essential libxml2-dev libxslt1-dev libffi-dev graphviz libpq-dev libssl-dev redis-server zlib1g-dev
# apt-get install -y python3 python3-pip python3-dev build-essential libxml2-dev libxslt1-dev libffi-dev graphviz libpq-dev libssl-dev zlib1g-dev
```
**CentOS**
```no-highlight
# yum install -y epel-release
# yum install -y gcc python36 python36-devel python36-setuptools libxml2-devel libxslt-devel libffi-devel graphviz openssl-devel redhat-rpm-config redis
# yum install -y gcc python36 python36-devel python36-setuptools libxml2-devel libxslt-devel libffi-devel graphviz openssl-devel redhat-rpm-config
# easy_install-3.6 pip
# ln -s /usr/bin/python36 /usr/bin/python3
```
@@ -90,6 +90,28 @@ NetBox supports integration with the [NAPALM automation](https://napalm-automati
# pip3 install napalm
```
## Webhooks (Optional)
[Webhooks](../data-model/extras/#webhooks) allow NetBox to integrate with external services by pushing out a notification each time a relevant object is created, updated, or deleted. Enabling the webhooks feature requires [Redis](https://redis.io/), a lightweight in-memory database. You may opt to install a Redis sevice locally (see below) or connect to an external one.
**Ubuntu**
```no-highlight
# apt-get install -y redis-server
```
**CentOS**
```no-highlight
# yum install -y redis
```
Enabling webhooks also requires installing the [`django-rq`](https://github.com/ui/django-rq) package. This allows NetBox to use the Redis database as a queue for outgoing webhooks.
```no-highlight
# pip3 install django-rq
```
# Configuration
Move into the NetBox configuration directory and make a copy of `configuration.example.py` named `configuration.py`.

View File

@@ -95,9 +95,6 @@ AUTH_LDAP_GROUP_TYPE = GroupOfNamesType()
# Define a group required to login.
AUTH_LDAP_REQUIRE_GROUP = "CN=NETBOX_USERS,DC=example,DC=com"
# Mirror LDAP group assignments.
AUTH_LDAP_MIRROR_GROUPS = True
# Define special user types using groups. Exercise great caution when assigning superuser status.
AUTH_LDAP_USER_FLAGS_BY_GROUP = {
"is_active": "cn=active,ou=groups,dc=example,dc=com",
@@ -116,21 +113,3 @@ AUTH_LDAP_GROUP_CACHE_TIMEOUT = 3600
* `is_active` - All users must be mapped to at least this group to enable authentication. Without this, users cannot log in.
* `is_staff` - Users mapped to this group are enabled for access to the administration tools; this is the equivalent of checking the "staff status" box on a manually created user. This doesn't grant any specific permissions.
* `is_superuser` - Users mapped to this group will be granted superuser status. Superusers are implicitly granted all permissions.
# Troubleshooting LDAP
`supervisorctl restart netbox` restarts the Netbox service, and initiates any changes made to `ldap_config.py`. If there are syntax errors present, the NetBox process will not spawn an instance, and errors should be logged to `/var/log/supervisor/`.
For troubleshooting LDAP user/group queries, add the following lines to the start of `ldap_config.py` after `import ldap`.
```python
import logging, logging.handlers
logfile = "/opt/netbox/logs/django-ldap-debug.log"
my_logger = logging.getLogger('django_auth_ldap')
my_logger.setLevel(logging.DEBUG)
handler = logging.handlers.RotatingFileHandler(
logfile, maxBytes=1024 * 500, backupCount=5)
my_logger.addHandler(handler)
```
Ensure the file and path specified in logfile exist and are writable and executable by the application service account. Restart the netbox service and attempt to log into the site to trigger log entries to this file.

View File

@@ -1,7 +1,7 @@
# Migration
!!! warning
As of version 2.5, NetBox no longer supports Python 2. Python 3 is required to run any 2.5 release or later.
Beginning with v2.5, NetBox will no longer support Python 2. It is strongly recommended that you upgrade to Python 3 as soon as possible.
## Ubuntu
@@ -36,3 +36,9 @@ If using LDAP authentication, install the `django-auth-ldap` package:
```no-highlight
# pip3 install django-auth-ldap
```
If using Webhooks, install the `django-rq` package:
```no-highlight
# pip3 install django-rq
```

View File

@@ -36,8 +36,6 @@ pages:
- Reports: 'additional-features/reports.md'
- Webhooks: 'additional-features/webhooks.md'
- Change Logging: 'additional-features/change-logging.md'
- Caching: 'additional-features/caching.md'
- Prometheus Metrics: 'additional-features/prometheus-metrics.md'
- Administration:
- Replicating NetBox: 'administration/replicating-netbox.md'
- NetBox Shell: 'administration/netbox-shell.md'

View File

@@ -17,11 +17,10 @@ __all__ = [
class NestedProviderSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provider-detail')
circuit_count = serializers.IntegerField(read_only=True)
class Meta:
model = Provider
fields = ['id', 'url', 'name', 'slug', 'circuit_count']
fields = ['id', 'url', 'name', 'slug']
#
@@ -30,11 +29,10 @@ class NestedProviderSerializer(WritableNestedSerializer):
class NestedCircuitTypeSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittype-detail')
circuit_count = serializers.IntegerField(read_only=True)
class Meta:
model = CircuitType
fields = ['id', 'url', 'name', 'slug', 'circuit_count']
fields = ['id', 'url', 'name', 'slug']
class NestedCircuitSerializer(WritableNestedSerializer):

View File

@@ -1,4 +1,3 @@
from rest_framework import serializers
from taggit_serializer.serializers import TaggitSerializer, TagListSerializerField
from circuits.constants import CIRCUIT_STATUS_CHOICES
@@ -17,13 +16,12 @@ from .nested_serializers import *
class ProviderSerializer(TaggitSerializer, CustomFieldModelSerializer):
tags = TagListSerializerField(required=False)
circuit_count = serializers.IntegerField(read_only=True)
class Meta:
model = Provider
fields = [
'id', 'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments', 'tags',
'custom_fields', 'created', 'last_updated', 'circuit_count',
'custom_fields', 'created', 'last_updated',
]
@@ -32,11 +30,10 @@ class ProviderSerializer(TaggitSerializer, CustomFieldModelSerializer):
#
class CircuitTypeSerializer(ValidatedModelSerializer):
circuit_count = serializers.IntegerField(read_only=True)
class Meta:
model = CircuitType
fields = ['id', 'name', 'slug', 'circuit_count']
fields = ['id', 'name', 'slug']
class CircuitSerializer(TaggitSerializer, CustomFieldModelSerializer):

View File

@@ -1,4 +1,3 @@
from django.db.models import Count
from django.shortcuts import get_object_or_404
from rest_framework.decorators import action
from rest_framework.response import Response
@@ -28,9 +27,7 @@ class CircuitsFieldChoicesViewSet(FieldChoicesViewSet):
#
class ProviderViewSet(CustomFieldModelViewSet):
queryset = Provider.objects.prefetch_related('tags').annotate(
circuit_count=Count('circuits')
)
queryset = Provider.objects.prefetch_related('tags')
serializer_class = serializers.ProviderSerializer
filterset_class = filters.ProviderFilter
@@ -50,9 +47,7 @@ class ProviderViewSet(CustomFieldModelViewSet):
#
class CircuitTypeViewSet(ModelViewSet):
queryset = CircuitType.objects.annotate(
circuit_count=Count('circuits')
)
queryset = CircuitType.objects.all()
serializer_class = serializers.CircuitTypeSerializer
filterset_class = filters.CircuitTypeFilter

View File

@@ -4,7 +4,7 @@ from django.db.models import Q
from dcim.models import Site
from extras.filters import CustomFieldFilterSet
from tenancy.models import Tenant
from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter
from utilities.filters import NumericInFilter, TagFilter
from .constants import CIRCUIT_STATUS_CHOICES
from .models import Provider, Circuit, CircuitTermination, CircuitType
@@ -47,7 +47,7 @@ class ProviderFilter(CustomFieldFilterSet, django_filters.FilterSet):
)
class CircuitTypeFilter(NameSlugSearchFilterSet):
class CircuitTypeFilter(django_filters.FilterSet):
class Meta:
model = CircuitType

View File

@@ -1,4 +1,5 @@
from django import forms
from django.db.models import Count
from taggit.forms import TagField
from dcim.models import Site
@@ -6,8 +7,8 @@ from extras.forms import AddRemoveTagsForm, CustomFieldForm, CustomFieldBulkEdit
from tenancy.forms import TenancyForm
from tenancy.models import Tenant
from utilities.forms import (
APISelect, APISelectMultiple, add_blank_choice, BootstrapMixin, CommentField, CSVChoiceField,
FilterChoiceField, SmallTextarea, SlugField, StaticSelect2, StaticSelect2Multiple
AnnotatedMultipleChoiceField, add_blank_choice, BootstrapMixin, CommentField, CSVChoiceField, FilterChoiceField,
SmallTextarea, SlugField,
)
from .constants import CIRCUIT_STATUS_CHOICES
from .models import Circuit, CircuitTermination, CircuitType, Provider
@@ -106,11 +107,7 @@ class ProviderFilterForm(BootstrapMixin, CustomFieldFilterForm):
)
site = FilterChoiceField(
queryset=Site.objects.all(),
to_field_name='slug',
widget=APISelectMultiple(
api_url="/api/dcim/sites/",
value_field="slug",
)
to_field_name='slug'
)
asn = forms.IntegerField(
required=False,
@@ -164,16 +161,6 @@ class CircuitForm(BootstrapMixin, TenancyForm, CustomFieldForm):
'install_date': "Format: YYYY-MM-DD",
'commit_rate': "Committed rate",
}
widgets = {
'provider': APISelect(
api_url="/api/circuits/providers/"
),
'type': APISelect(
api_url="/api/circuits/circuit-types/"
),
'status': StaticSelect2(),
}
class CircuitCSVForm(forms.ModelForm):
@@ -222,30 +209,20 @@ class CircuitBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEdit
)
type = forms.ModelChoiceField(
queryset=CircuitType.objects.all(),
required=False,
widget=APISelect(
api_url="/api/circuits/circuit-types/"
)
required=False
)
provider = forms.ModelChoiceField(
queryset=Provider.objects.all(),
required=False,
widget=APISelect(
api_url="/api/circuits/providers/"
)
required=False
)
status = forms.ChoiceField(
choices=add_blank_choice(CIRCUIT_STATUS_CHOICES),
required=False,
initial='',
widget=StaticSelect2()
initial=''
)
tenant = forms.ModelChoiceField(
queryset=Tenant.objects.all(),
required=False,
widget=APISelect(
api_url="/api/tenancy/tenants/"
)
required=False
)
commit_rate = forms.IntegerField(
required=False,
@@ -272,43 +249,35 @@ class CircuitFilterForm(BootstrapMixin, CustomFieldFilterForm):
label='Search'
)
type = FilterChoiceField(
queryset=CircuitType.objects.all(),
to_field_name='slug',
widget=APISelectMultiple(
api_url="/api/circuits/circuit-types/",
value_field="slug",
)
queryset=CircuitType.objects.annotate(
filter_count=Count('circuits')
),
to_field_name='slug'
)
provider = FilterChoiceField(
queryset=Provider.objects.all(),
to_field_name='slug',
widget=APISelectMultiple(
api_url="/api/circuits/providers/",
value_field="slug",
)
queryset=Provider.objects.annotate(
filter_count=Count('circuits')
),
to_field_name='slug'
)
status = forms.MultipleChoiceField(
status = AnnotatedMultipleChoiceField(
choices=CIRCUIT_STATUS_CHOICES,
required=False,
widget=StaticSelect2Multiple()
annotate=Circuit.objects.all(),
annotate_field='status',
required=False
)
tenant = FilterChoiceField(
queryset=Tenant.objects.all(),
queryset=Tenant.objects.annotate(
filter_count=Count('circuits')
),
to_field_name='slug',
null_label='-- None --',
widget=APISelectMultiple(
api_url="/api/tenancy/tenants/",
value_field="slug",
null_option=True,
)
null_label='-- None --'
)
site = FilterChoiceField(
queryset=Site.objects.all(),
to_field_name='slug',
widget=APISelectMultiple(
api_url="/api/dcim/sites/",
value_field="slug",
)
queryset=Site.objects.annotate(
filter_count=Count('circuit_terminations')
),
to_field_name='slug'
)
commit_rate = forms.IntegerField(
required=False,
@@ -335,7 +304,4 @@ class CircuitTerminationForm(BootstrapMixin, forms.ModelForm):
}
widgets = {
'term_side': forms.HiddenInput(),
'site': APISelect(
api_url="/api/dcim/sites/"
)
}

View File

@@ -1,25 +0,0 @@
# Generated by Django 2.1.4 on 2019-02-20 06:56
from django.db import migrations
import taggit.managers
class Migration(migrations.Migration):
dependencies = [
('circuits', '0014_circuittermination_description'),
('extras', '0019_tag_taggeditem'),
]
operations = [
migrations.AlterField(
model_name='circuit',
name='tags',
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
),
migrations.AlterField(
model_name='provider',
name='tags',
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
),
]

View File

@@ -3,10 +3,10 @@ from django.db import models
from django.urls import reverse
from taggit.managers import TaggableManager
from dcim.constants import CONNECTION_STATUS_CHOICES, STATUS_CLASSES
from dcim.constants import CONNECTION_STATUS_CHOICES, CONNECTION_STATUS_CONNECTED, STATUS_CLASSES
from dcim.fields import ASNField
from dcim.models import CableTermination
from extras.models import CustomFieldModel, ObjectChange, TaggedItem
from extras.models import CustomFieldModel, ObjectChange
from utilities.models import ChangeLoggedModel
from utilities.utils import serialize_object
from .constants import CIRCUIT_STATUS_ACTIVE, CIRCUIT_STATUS_CHOICES, TERM_SIDE_CHOICES
@@ -55,7 +55,7 @@ class Provider(ChangeLoggedModel, CustomFieldModel):
object_id_field='obj_id'
)
tags = TaggableManager(through=TaggedItem)
tags = TaggableManager()
csv_headers = ['name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments']
@@ -165,7 +165,7 @@ class Circuit(ChangeLoggedModel, CustomFieldModel):
object_id_field='obj_id'
)
tags = TaggableManager(through=TaggedItem)
tags = TaggableManager()
csv_headers = [
'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description', 'comments',
@@ -283,10 +283,6 @@ class CircuitTermination(CableTermination):
object_data=serialize_object(self)
).save()
@property
def parent(self):
return self.circuit
def get_peer_termination(self):
peer_side = 'Z' if self.term_side == 'A' else 'A'
try:

View File

@@ -11,7 +11,7 @@ CIRCUITTYPE_ACTIONS = """
<i class="fa fa-history"></i>
</a>
{% if perms.circuit.change_circuittype %}
<a href="{% url 'circuits:circuittype_edit' slug=record.slug %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
<a href="{% url 'circuits:circuittype_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
{% endif %}
"""
@@ -59,7 +59,7 @@ class CircuitTypeTable(BaseTable):
name = tables.LinkColumn()
circuit_count = tables.Column(verbose_name='Circuits')
actions = tables.TemplateColumn(
template_code=CIRCUITTYPE_ACTIONS, attrs={'td': {'class': 'text-right noprint'}}, verbose_name=''
template_code=CIRCUITTYPE_ACTIONS, attrs={'td': {'class': 'text-right'}}, verbose_name=''
)
class Meta(BaseTable.Meta):

View File

@@ -61,7 +61,7 @@ class ProviderTest(APITestCase):
self.assertEqual(
sorted(response.data['results'][0]),
['circuit_count', 'id', 'name', 'slug', 'url']
['id', 'name', 'slug', 'url']
)
def test_create_provider(self):
@@ -162,7 +162,7 @@ class CircuitTypeTest(APITestCase):
self.assertEqual(
sorted(response.data['results'][0]),
['circuit_count', 'id', 'name', 'slug', 'url']
['id', 'name', 'slug', 'url']
)
def test_create_circuittype(self):

View File

@@ -1,95 +0,0 @@
import urllib.parse
from django.test import Client, TestCase
from django.urls import reverse
from circuits.models import Circuit, CircuitType, Provider
from utilities.testing import create_test_user
class ProviderTestCase(TestCase):
def setUp(self):
user = create_test_user(permissions=['circuits.view_provider'])
self.client = Client()
self.client.force_login(user)
Provider.objects.bulk_create([
Provider(name='Provider 1', slug='provider-1', asn=65001),
Provider(name='Provider 2', slug='provider-2', asn=65002),
Provider(name='Provider 3', slug='provider-3', asn=65003),
])
def test_provider_list(self):
url = reverse('circuits:provider_list')
params = {
"q": "test",
}
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
self.assertEqual(response.status_code, 200)
def test_provider(self):
provider = Provider.objects.first()
response = self.client.get(provider.get_absolute_url())
self.assertEqual(response.status_code, 200)
class CircuitTypeTestCase(TestCase):
def setUp(self):
user = create_test_user(permissions=['circuits.view_circuittype'])
self.client = Client()
self.client.force_login(user)
CircuitType.objects.bulk_create([
CircuitType(name='Circuit Type 1', slug='circuit-type-1'),
CircuitType(name='Circuit Type 2', slug='circuit-type-2'),
CircuitType(name='Circuit Type 3', slug='circuit-type-3'),
])
def test_circuittype_list(self):
url = reverse('circuits:circuittype_list')
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
class CircuitTestCase(TestCase):
def setUp(self):
user = create_test_user(permissions=['circuits.view_circuit'])
self.client = Client()
self.client.force_login(user)
provider = Provider(name='Provider 1', slug='provider-1', asn=65001)
provider.save()
circuittype = CircuitType(name='Circuit Type 1', slug='circuit-type-1')
circuittype.save()
Circuit.objects.bulk_create([
Circuit(cid='Circuit 1', provider=provider, type=circuittype),
Circuit(cid='Circuit 2', provider=provider, type=circuittype),
Circuit(cid='Circuit 3', provider=provider, type=circuittype),
])
def test_circuit_list(self):
url = reverse('circuits:circuit_list')
params = {
"provider": Provider.objects.first().slug,
"type": CircuitType.objects.first().slug,
}
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
self.assertEqual(response.status_code, 200)
def test_circuit(self):
circuit = Circuit.objects.first()
response = self.client.get(circuit.get_absolute_url())
self.assertEqual(response.status_code, 200)

View File

@@ -43,7 +43,7 @@ urlpatterns = [
url(r'^circuits/(?P<circuit>\d+)/terminations/add/$', views.CircuitTerminationCreateView.as_view(), name='circuittermination_add'),
url(r'^circuit-terminations/(?P<pk>\d+)/edit/$', views.CircuitTerminationEditView.as_view(), name='circuittermination_edit'),
url(r'^circuit-terminations/(?P<pk>\d+)/delete/$', views.CircuitTerminationDeleteView.as_view(), name='circuittermination_delete'),
url(r'^circuit-terminations/(?P<termination_a_id>\d+)/connect/(?P<termination_b_type>[\w-]+)/$', CableCreateView.as_view(), name='circuittermination_connect', kwargs={'termination_a_type': CircuitTermination}),
url(r'^circuit-terminations/(?P<termination_a_id>\d+)/connect/$', CableCreateView.as_view(), name='circuittermination_connect', kwargs={'termination_a_type': CircuitTermination}),
url(r'^circuit-terminations/(?P<pk>\d+)/trace/$', CableTraceView.as_view(), name='circuittermination_trace', kwargs={'model': CircuitTermination}),
]

View File

@@ -1,11 +1,9 @@
from django.conf import settings
from django.contrib import messages
from django.contrib.auth.decorators import permission_required
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.db import transaction
from django.db.models import Count
from django.shortcuts import get_object_or_404, redirect, render
from django.utils.decorators import method_decorator
from django.views.generic import View
from extras.models import Graph, GRAPH_TYPE_PROVIDER
@@ -22,8 +20,7 @@ from .models import Circuit, CircuitTermination, CircuitType, Provider
# Providers
#
class ProviderListView(PermissionRequiredMixin, ObjectListView):
permission_required = 'circuits.view_provider'
class ProviderListView(ObjectListView):
queryset = Provider.objects.annotate(count_circuits=Count('circuits'))
filter = filters.ProviderFilter
filter_form = forms.ProviderFilterForm
@@ -31,8 +28,7 @@ class ProviderListView(PermissionRequiredMixin, ObjectListView):
template_name = 'circuits/provider_list.html'
class ProviderView(PermissionRequiredMixin, View):
permission_required = 'circuits.view_provider'
class ProviderView(View):
def get(self, request, slug):
@@ -97,8 +93,7 @@ class ProviderBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
# Circuit Types
#
class CircuitTypeListView(PermissionRequiredMixin, ObjectListView):
permission_required = 'circuits.view_circuittype'
class CircuitTypeListView(ObjectListView):
queryset = CircuitType.objects.annotate(circuit_count=Count('circuits'))
table = tables.CircuitTypeTable
template_name = 'circuits/circuittype_list.html'
@@ -133,8 +128,7 @@ class CircuitTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
# Circuits
#
class CircuitListView(PermissionRequiredMixin, ObjectListView):
permission_required = 'circuits.view_circuit'
class CircuitListView(ObjectListView):
queryset = Circuit.objects.select_related(
'provider', 'type', 'tenant'
).prefetch_related(
@@ -146,8 +140,7 @@ class CircuitListView(PermissionRequiredMixin, ObjectListView):
template_name = 'circuits/circuit_list.html'
class CircuitView(PermissionRequiredMixin, View):
permission_required = 'circuits.view_circuit'
class CircuitView(View):
def get(self, request, pk):

View File

@@ -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, PowerOutlet, PowerPort, Rack, RackGroup, RackRole, RearPort, RearPortTemplate,
Region, Site, VirtualChassis,
)
from utilities.api import ChoiceField, WritableNestedSerializer
@@ -21,9 +21,7 @@ __all__ = [
'NestedInterfaceSerializer',
'NestedManufacturerSerializer',
'NestedPlatformSerializer',
'NestedPowerFeedSerializer',
'NestedPowerOutletSerializer',
'NestedPowerPanelSerializer',
'NestedPowerPortSerializer',
'NestedRackGroupSerializer',
'NestedRackRoleSerializer',
@@ -42,11 +40,10 @@ __all__ = [
class NestedRegionSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:region-detail')
site_count = serializers.IntegerField(read_only=True)
class Meta:
model = Region
fields = ['id', 'url', 'name', 'slug', 'site_count']
fields = ['id', 'url', 'name', 'slug']
class NestedSiteSerializer(WritableNestedSerializer):
@@ -63,29 +60,26 @@ class NestedSiteSerializer(WritableNestedSerializer):
class NestedRackGroupSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackgroup-detail')
rack_count = serializers.IntegerField(read_only=True)
class Meta:
model = RackGroup
fields = ['id', 'url', 'name', 'slug', 'rack_count']
fields = ['id', 'url', 'name', 'slug']
class NestedRackRoleSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackrole-detail')
rack_count = serializers.IntegerField(read_only=True)
class Meta:
model = RackRole
fields = ['id', 'url', 'name', 'slug', 'rack_count']
fields = ['id', 'url', 'name', 'slug']
class NestedRackSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rack-detail')
device_count = serializers.IntegerField(read_only=True)
class Meta:
model = Rack
fields = ['id', 'url', 'name', 'display_name', 'device_count']
fields = ['id', 'url', 'name', 'display_name']
#
@@ -94,21 +88,19 @@ class NestedRackSerializer(WritableNestedSerializer):
class NestedManufacturerSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:manufacturer-detail')
devicetype_count = serializers.IntegerField(read_only=True)
class Meta:
model = Manufacturer
fields = ['id', 'url', 'name', 'slug', 'devicetype_count']
fields = ['id', 'url', 'name', 'slug']
class NestedDeviceTypeSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicetype-detail')
manufacturer = NestedManufacturerSerializer(read_only=True)
device_count = serializers.IntegerField(read_only=True)
class Meta:
model = DeviceType
fields = ['id', 'url', 'manufacturer', 'model', 'slug', 'display_name', 'device_count']
fields = ['id', 'url', 'manufacturer', 'model', 'slug']
class NestedRearPortTemplateSerializer(WritableNestedSerializer):
@@ -133,22 +125,18 @@ class NestedFrontPortTemplateSerializer(WritableNestedSerializer):
class NestedDeviceRoleSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicerole-detail')
device_count = serializers.IntegerField(read_only=True)
virtualmachine_count = serializers.IntegerField(read_only=True)
class Meta:
model = DeviceRole
fields = ['id', 'url', 'name', 'slug', 'device_count', 'virtualmachine_count']
fields = ['id', 'url', 'name', 'slug']
class NestedPlatformSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:platform-detail')
device_count = serializers.IntegerField(read_only=True)
virtualmachine_count = serializers.IntegerField(read_only=True)
class Meta:
model = Platform
fields = ['id', 'url', 'name', 'slug', 'device_count', 'virtualmachine_count']
fields = ['id', 'url', 'name', 'slug']
class NestedDeviceSerializer(WritableNestedSerializer):
@@ -255,29 +243,7 @@ class NestedCableSerializer(serializers.ModelSerializer):
class NestedVirtualChassisSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualchassis-detail')
master = NestedDeviceSerializer()
member_count = serializers.IntegerField(read_only=True)
class Meta:
model = VirtualChassis
fields = ['id', 'url', 'master', 'member_count']
#
# Power panels/feeds
#
class NestedPowerPanelSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerpanel-detail')
powerfeed_count = serializers.IntegerField(read_only=True)
class Meta:
model = PowerPanel
fields = ['id', 'url', 'name', 'powerfeed_count']
class NestedPowerFeedSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerfeed-detail')
class Meta:
model = PowerFeed
fields = ['id', 'url', 'name']
fields = ['id', 'url', 'master']

View File

@@ -1,5 +1,3 @@
from django.contrib.contenttypes.models import ContentType
from drf_yasg.utils import swagger_serializer_method
from rest_framework import serializers
from rest_framework.validators import UniqueTogetherValidator
from taggit_serializer.serializers import TaggitSerializer, TagListSerializerField
@@ -8,9 +6,8 @@ from dcim.constants import *
from dcim.models import (
Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBayTemplate, DeviceType, DeviceRole, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate,
Manufacturer, InventoryItem, Platform, PowerFeed, PowerOutlet, PowerOutletTemplate, PowerPanel, PowerPort,
PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site,
VirtualChassis,
Manufacturer, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack,
RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site, VirtualChassis,
)
from extras.api.customfields import CustomFieldModelSerializer
from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedVLANSerializer
@@ -38,7 +35,6 @@ class ConnectedEndpointSerializer(ValidatedModelSerializer):
)
return None
@swagger_serializer_method(serializer_or_field=serializers.DictField)
def get_connected_endpoint(self, obj):
"""
Return the appropriate serializer for the type of connected object.
@@ -59,11 +55,10 @@ class ConnectedEndpointSerializer(ValidatedModelSerializer):
class RegionSerializer(serializers.ModelSerializer):
parent = NestedRegionSerializer(required=False, allow_null=True)
site_count = serializers.IntegerField(read_only=True)
class Meta:
model = Region
fields = ['id', 'name', 'slug', 'parent', 'site_count']
fields = ['id', 'name', 'slug', 'parent']
class SiteSerializer(TaggitSerializer, CustomFieldModelSerializer):
@@ -72,20 +67,19 @@ class SiteSerializer(TaggitSerializer, CustomFieldModelSerializer):
tenant = NestedTenantSerializer(required=False, allow_null=True)
time_zone = TimeZoneField(required=False)
tags = TagListSerializerField(required=False)
circuit_count = serializers.IntegerField(read_only=True)
device_count = serializers.IntegerField(read_only=True)
prefix_count = serializers.IntegerField(read_only=True)
rack_count = serializers.IntegerField(read_only=True)
virtualmachine_count = serializers.IntegerField(read_only=True)
vlan_count = serializers.IntegerField(read_only=True)
count_prefixes = serializers.IntegerField(read_only=True)
count_vlans = serializers.IntegerField(read_only=True)
count_racks = serializers.IntegerField(read_only=True)
count_devices = serializers.IntegerField(read_only=True)
count_circuits = serializers.IntegerField(read_only=True)
class Meta:
model = Site
fields = [
'id', 'name', 'slug', 'status', 'region', 'tenant', 'facility', 'asn', 'time_zone', 'description',
'physical_address', 'shipping_address', 'latitude', 'longitude', 'contact_name', 'contact_phone',
'contact_email', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'circuit_count',
'device_count', 'prefix_count', 'rack_count', 'virtualmachine_count', 'vlan_count',
'contact_email', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'count_prefixes',
'count_vlans', 'count_racks', 'count_devices', 'count_circuits',
]
@@ -95,19 +89,17 @@ class SiteSerializer(TaggitSerializer, CustomFieldModelSerializer):
class RackGroupSerializer(ValidatedModelSerializer):
site = NestedSiteSerializer()
rack_count = serializers.IntegerField(read_only=True)
class Meta:
model = RackGroup
fields = ['id', 'name', 'slug', 'site', 'rack_count']
fields = ['id', 'name', 'slug', 'site']
class RackRoleSerializer(ValidatedModelSerializer):
rack_count = serializers.IntegerField(read_only=True)
class Meta:
model = RackRole
fields = ['id', 'name', 'slug', 'color', 'rack_count']
fields = ['id', 'name', 'slug', 'color']
class RackSerializer(TaggitSerializer, CustomFieldModelSerializer):
@@ -120,15 +112,13 @@ class RackSerializer(TaggitSerializer, CustomFieldModelSerializer):
width = ChoiceField(choices=RACK_WIDTH_CHOICES, required=False)
outer_unit = ChoiceField(choices=RACK_DIMENSION_UNIT_CHOICES, required=False)
tags = TagListSerializerField(required=False)
device_count = serializers.IntegerField(read_only=True)
powerfeed_count = serializers.IntegerField(read_only=True)
class Meta:
model = Rack
fields = [
'id', 'name', 'facility_id', 'display_name', 'site', 'group', 'tenant', 'status', 'role', 'serial',
'asset_tag', 'type', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit',
'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'device_count', 'powerfeed_count',
'comments', 'tags', 'custom_fields', 'created', 'last_updated',
]
# Omit the UniqueTogetherValidator that would be automatically added to validate (group, facility_id). This
# prevents facility_id from being interpreted as a required field.
@@ -175,26 +165,23 @@ class RackReservationSerializer(ValidatedModelSerializer):
#
class ManufacturerSerializer(ValidatedModelSerializer):
devicetype_count = serializers.IntegerField(read_only=True)
inventoryitem_count = serializers.IntegerField(read_only=True)
platform_count = serializers.IntegerField(read_only=True)
class Meta:
model = Manufacturer
fields = ['id', 'name', 'slug', 'devicetype_count', 'inventoryitem_count', 'platform_count']
fields = ['id', 'name', 'slug']
class DeviceTypeSerializer(TaggitSerializer, CustomFieldModelSerializer):
manufacturer = NestedManufacturerSerializer()
subdevice_role = ChoiceField(choices=SUBDEVICE_ROLE_CHOICES, required=False, allow_null=True)
instance_count = serializers.IntegerField(source='instances.count', read_only=True)
tags = TagListSerializerField(required=False)
device_count = serializers.IntegerField(read_only=True)
class Meta:
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',
'id', 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role',
'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'instance_count',
]
@@ -219,34 +206,24 @@ class PowerPortTemplateSerializer(ValidatedModelSerializer):
class Meta:
model = PowerPortTemplate
fields = ['id', 'device_type', 'name', 'maximum_draw', 'allocated_draw']
fields = ['id', 'device_type', 'name']
class PowerOutletTemplateSerializer(ValidatedModelSerializer):
device_type = NestedDeviceTypeSerializer()
power_port = PowerPortTemplateSerializer(
required=False
)
feed_leg = ChoiceField(
choices=POWERFEED_LEG_CHOICES,
required=False,
allow_null=True
)
class Meta:
model = PowerOutletTemplate
fields = ['id', 'device_type', 'name', 'power_port', 'feed_leg']
fields = ['id', 'device_type', 'name']
class InterfaceTemplateSerializer(ValidatedModelSerializer):
device_type = NestedDeviceTypeSerializer()
type = ChoiceField(choices=IFACE_TYPE_CHOICES, required=False)
# TODO: Remove in v2.7 (backward-compatibility for form_factor)
form_factor = ChoiceField(choices=IFACE_TYPE_CHOICES, required=False)
form_factor = ChoiceField(choices=IFACE_FF_CHOICES, required=False)
class Meta:
model = InterfaceTemplate
fields = ['id', 'device_type', 'name', 'type', 'form_factor', 'mgmt_only']
fields = ['id', 'device_type', 'name', 'form_factor', 'mgmt_only']
class RearPortTemplateSerializer(ValidatedModelSerializer):
@@ -281,25 +258,18 @@ class DeviceBayTemplateSerializer(ValidatedModelSerializer):
#
class DeviceRoleSerializer(ValidatedModelSerializer):
device_count = serializers.IntegerField(read_only=True)
virtualmachine_count = serializers.IntegerField(read_only=True)
class Meta:
model = DeviceRole
fields = ['id', 'name', 'slug', 'color', 'vm_role', 'device_count', 'virtualmachine_count']
fields = ['id', 'name', 'slug', 'color', 'vm_role']
class PlatformSerializer(ValidatedModelSerializer):
manufacturer = NestedManufacturerSerializer(required=False, allow_null=True)
device_count = serializers.IntegerField(read_only=True)
virtualmachine_count = serializers.IntegerField(read_only=True)
class Meta:
model = Platform
fields = [
'id', 'name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args', 'device_count',
'virtualmachine_count',
]
fields = ['id', 'name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args']
class DeviceSerializer(TaggitSerializer, CustomFieldModelSerializer):
@@ -342,7 +312,6 @@ class DeviceSerializer(TaggitSerializer, CustomFieldModelSerializer):
return data
@swagger_serializer_method(serializer_or_field=NestedDeviceSerializer)
def get_parent_device(self, obj):
try:
device_bay = obj.parent_bay
@@ -365,7 +334,6 @@ class DeviceWithConfigContextSerializer(DeviceSerializer):
'custom_fields', 'config_context', 'created', 'last_updated',
]
@swagger_serializer_method(serializer_or_field=serializers.DictField)
def get_config_context(self, obj):
return obj.get_config_context()
@@ -378,8 +346,8 @@ class ConsoleServerPortSerializer(TaggitSerializer, ConnectedEndpointSerializer)
class Meta:
model = ConsoleServerPort
fields = [
'id', 'device', 'name', 'description', 'connected_endpoint_type', 'connected_endpoint', 'connection_status',
'cable', 'tags',
'id', 'device', 'name', 'connected_endpoint_type', 'connected_endpoint', 'connection_status', 'cable',
'tags',
]
@@ -391,33 +359,21 @@ class ConsolePortSerializer(TaggitSerializer, ConnectedEndpointSerializer):
class Meta:
model = ConsolePort
fields = [
'id', 'device', 'name', 'description', 'connected_endpoint_type', 'connected_endpoint', 'connection_status',
'cable', 'tags',
'id', 'device', 'name', 'connected_endpoint_type', 'connected_endpoint', 'connection_status', 'cable',
'tags',
]
class PowerOutletSerializer(TaggitSerializer, ConnectedEndpointSerializer):
device = NestedDeviceSerializer()
power_port = NestedPowerPortSerializer(
required=False
)
feed_leg = ChoiceField(
choices=POWERFEED_LEG_CHOICES,
required=False,
allow_null=True
)
cable = NestedCableSerializer(
read_only=True
)
tags = TagListSerializerField(
required=False
)
cable = NestedCableSerializer(read_only=True)
tags = TagListSerializerField(required=False)
class Meta:
model = PowerOutlet
fields = [
'id', 'device', 'name', 'power_port', 'feed_leg', 'description', 'connected_endpoint_type',
'connected_endpoint', 'connection_status', 'cable', 'tags',
'id', 'device', 'name', 'connected_endpoint_type', 'connected_endpoint', 'connection_status', 'cable',
'tags',
]
@@ -429,16 +385,14 @@ class PowerPortSerializer(TaggitSerializer, ConnectedEndpointSerializer):
class Meta:
model = PowerPort
fields = [
'id', 'device', 'name', 'maximum_draw', 'allocated_draw', 'description', 'connected_endpoint_type',
'connected_endpoint', 'connection_status', 'cable', 'tags',
'id', 'device', 'name', 'connected_endpoint_type', 'connected_endpoint', 'connection_status', 'cable',
'tags',
]
class InterfaceSerializer(TaggitSerializer, ConnectedEndpointSerializer):
device = NestedDeviceSerializer()
type = ChoiceField(choices=IFACE_TYPE_CHOICES, required=False)
# TODO: Remove in v2.7 (backward-compatibility for form_factor)
form_factor = ChoiceField(choices=IFACE_TYPE_CHOICES, required=False)
form_factor = ChoiceField(choices=IFACE_FF_CHOICES, required=False)
lag = NestedInterfaceSerializer(required=False, allow_null=True)
mode = ChoiceField(choices=IFACE_MODE_CHOICES, required=False, allow_null=True)
untagged_vlan = NestedVLANSerializer(required=False, allow_null=True)
@@ -454,9 +408,9 @@ class InterfaceSerializer(TaggitSerializer, ConnectedEndpointSerializer):
class Meta:
model = Interface
fields = [
'id', 'device', 'name', 'type', 'form_factor', 'enabled', 'lag', 'mtu', 'mac_address', 'mgmt_only',
'description', 'connected_endpoint_type', 'connected_endpoint', 'connection_status', 'cable', 'mode',
'untagged_vlan', 'tagged_vlans', 'tags', 'count_ipaddresses',
'id', 'device', 'name', 'form_factor', 'enabled', 'lag', 'mtu', 'mac_address', 'mgmt_only', 'description',
'connected_endpoint_type', 'connected_endpoint', 'connection_status', 'cable', 'mode', 'untagged_vlan',
'tagged_vlans', 'tags', 'count_ipaddresses',
]
# TODO: This validation should be handled by Interface.clean()
@@ -521,7 +475,7 @@ class DeviceBaySerializer(TaggitSerializer, ValidatedModelSerializer):
class Meta:
model = DeviceBay
fields = ['id', 'device', 'name', 'description', 'installed_device', 'tags']
fields = ['id', 'device', 'name', 'installed_device', 'tags']
#
@@ -548,16 +502,12 @@ class InventoryItemSerializer(TaggitSerializer, ValidatedModelSerializer):
#
class CableSerializer(ValidatedModelSerializer):
termination_a_type = ContentTypeField(
queryset=ContentType.objects.filter(model__in=CABLE_TERMINATION_TYPES)
)
termination_b_type = ContentTypeField(
queryset=ContentType.objects.filter(model__in=CABLE_TERMINATION_TYPES)
)
termination_a_type = ContentTypeField()
termination_b_type = ContentTypeField()
termination_a = serializers.SerializerMethodField(read_only=True)
termination_b = serializers.SerializerMethodField(read_only=True)
status = ChoiceField(choices=CONNECTION_STATUS_CHOICES, required=False)
length_unit = ChoiceField(choices=CABLE_LENGTH_UNIT_CHOICES, required=False, allow_null=True)
length_unit = ChoiceField(choices=CABLE_LENGTH_UNIT_CHOICES, required=False)
class Meta:
model = Cable
@@ -581,11 +531,9 @@ class CableSerializer(ValidatedModelSerializer):
return data
@swagger_serializer_method(serializer_or_field=serializers.DictField)
def get_termination_a(self, obj):
return self._get_termination(obj, 'a')
@swagger_serializer_method(serializer_or_field=serializers.DictField)
def get_termination_b(self, obj):
return self._get_termination(obj, 'b')
@@ -616,7 +564,6 @@ class InterfaceConnectionSerializer(ValidatedModelSerializer):
model = Interface
fields = ['interface_a', 'interface_b', 'connection_status']
@swagger_serializer_method(serializer_or_field=NestedInterfaceSerializer)
def get_interface_a(self, obj):
context = {'request': self.context['request']}
return NestedInterfaceSerializer(instance=obj, context=context).data
@@ -629,61 +576,7 @@ class InterfaceConnectionSerializer(ValidatedModelSerializer):
class VirtualChassisSerializer(TaggitSerializer, ValidatedModelSerializer):
master = NestedDeviceSerializer()
tags = TagListSerializerField(required=False)
member_count = serializers.IntegerField(read_only=True)
class Meta:
model = VirtualChassis
fields = ['id', 'master', 'domain', 'tags', 'member_count']
#
# Power panels
#
class PowerPanelSerializer(ValidatedModelSerializer):
site = NestedSiteSerializer()
rack_group = NestedRackGroupSerializer(
required=False,
allow_null=True,
default=None
)
powerfeed_count = serializers.IntegerField(read_only=True)
class Meta:
model = PowerPanel
fields = ['id', 'site', 'rack_group', 'name', 'powerfeed_count']
class PowerFeedSerializer(TaggitSerializer, CustomFieldModelSerializer):
power_panel = NestedPowerPanelSerializer()
rack = NestedRackSerializer(
required=False,
allow_null=True,
default=None
)
type = ChoiceField(
choices=POWERFEED_TYPE_CHOICES,
default=POWERFEED_TYPE_PRIMARY
)
status = ChoiceField(
choices=POWERFEED_STATUS_CHOICES,
default=POWERFEED_STATUS_ACTIVE
)
supply = ChoiceField(
choices=POWERFEED_SUPPLY_CHOICES,
default=POWERFEED_SUPPLY_AC
)
phase = ChoiceField(
choices=POWERFEED_PHASE_CHOICES,
default=POWERFEED_PHASE_SINGLE
)
tags = TagListSerializerField(
required=False
)
class Meta:
model = PowerFeed
fields = [
'id', 'power_panel', 'rack', 'name', 'status', 'type', 'supply', 'phase', 'voltage', 'amperage',
'power_factor', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
]
fields = ['id', 'master', 'domain', 'tags']

View File

@@ -68,10 +68,6 @@ router.register(r'cables', views.CableViewSet)
# Virtual chassis
router.register(r'virtual-chassis', views.VirtualChassisViewSet)
# Power
router.register(r'power-panels', views.PowerPanelViewSet)
router.register(r'power-feeds', views.PowerFeedViewSet)
# Miscellaneous
router.register(r'connected-device', views.ConnectedDeviceViewSet, basename='connected-device')

View File

@@ -1,7 +1,7 @@
from collections import OrderedDict
from django.conf import settings
from django.db.models import Count, F
from django.db.models import F, Q
from django.http import HttpResponseForbidden
from django.shortcuts import get_object_or_404
from drf_yasg import openapi
@@ -12,24 +12,19 @@ from rest_framework.mixins import ListModelMixin
from rest_framework.response import Response
from rest_framework.viewsets import GenericViewSet, ViewSet
from circuits.models import Circuit
from dcim import filters
from dcim.models import (
Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate,
Manufacturer, InventoryItem, Platform, PowerFeed, PowerOutlet, PowerOutletTemplate, PowerPanel, PowerPort,
PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site,
VirtualChassis,
Manufacturer, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack,
RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site, VirtualChassis,
)
from extras.api.serializers import RenderedGraphSerializer
from extras.api.views import CustomFieldModelViewSet
from extras.models import Graph, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE
from ipam.models import Prefix, VLAN
from utilities.api import (
get_serializer_for_model, IsAuthenticatedOrLoginNotRequired, FieldChoicesViewSet, ModelViewSet, ServiceUnavailable,
)
from utilities.utils import get_subquery
from virtualization.models import VirtualMachine
from . import serializers
from .exceptions import MissingFilterException
@@ -40,20 +35,13 @@ from .exceptions import MissingFilterException
class DCIMFieldChoicesViewSet(FieldChoicesViewSet):
fields = (
(Cable, ['length_unit', 'status', 'termination_a_type', 'termination_b_type', 'type']),
(ConsolePort, ['connection_status']),
(Cable, ['length_unit']),
(Device, ['face', 'status']),
(DeviceType, ['subdevice_role']),
(FrontPort, ['type']),
(FrontPortTemplate, ['type']),
(Interface, ['type', 'mode']),
(InterfaceTemplate, ['type']),
(PowerOutlet, ['feed_leg']),
(PowerOutletTemplate, ['feed_leg']),
(ConsolePort, ['connection_status']),
(Interface, ['connection_status', 'form_factor', 'mode']),
(InterfaceTemplate, ['form_factor']),
(PowerPort, ['connection_status']),
(Rack, ['outer_unit', 'status', 'type', 'width']),
(RearPort, ['type']),
(RearPortTemplate, ['type']),
(Site, ['status']),
)
@@ -72,7 +60,7 @@ class CableTraceMixin(object):
# Initialize the path array
path = []
for near_end, cable, far_end in obj.trace(follow_circuits=True):
for near_end, cable, far_end in obj.trace():
# Serialize each object
serializer_a = get_serializer_for_model(near_end, prefix='Nested')
@@ -97,9 +85,7 @@ class CableTraceMixin(object):
#
class RegionViewSet(ModelViewSet):
queryset = Region.objects.annotate(
site_count=Count('sites')
)
queryset = Region.objects.all()
serializer_class = serializers.RegionSerializer
filterset_class = filters.RegionFilter
@@ -109,18 +95,7 @@ class RegionViewSet(ModelViewSet):
#
class SiteViewSet(CustomFieldModelViewSet):
queryset = Site.objects.select_related(
'region', 'tenant'
).prefetch_related(
'tags'
).annotate(
device_count=get_subquery(Device, 'site'),
rack_count=get_subquery(Rack, 'site'),
prefix_count=get_subquery(Prefix, 'site'),
vlan_count=get_subquery(VLAN, 'site'),
circuit_count=get_subquery(Circuit, 'terminations__site'),
virtualmachine_count=get_subquery(VirtualMachine, 'cluster__site'),
)
queryset = Site.objects.select_related('region', 'tenant').prefetch_related('tags')
serializer_class = serializers.SiteSerializer
filterset_class = filters.SiteFilter
@@ -140,9 +115,7 @@ class SiteViewSet(CustomFieldModelViewSet):
#
class RackGroupViewSet(ModelViewSet):
queryset = RackGroup.objects.select_related('site').annotate(
rack_count=Count('racks')
)
queryset = RackGroup.objects.select_related('site')
serializer_class = serializers.RackGroupSerializer
filterset_class = filters.RackGroupFilter
@@ -152,9 +125,7 @@ class RackGroupViewSet(ModelViewSet):
#
class RackRoleViewSet(ModelViewSet):
queryset = RackRole.objects.annotate(
rack_count=Count('racks')
)
queryset = RackRole.objects.all()
serializer_class = serializers.RackRoleSerializer
filterset_class = filters.RackRoleFilter
@@ -164,14 +135,7 @@ class RackRoleViewSet(ModelViewSet):
#
class RackViewSet(CustomFieldModelViewSet):
queryset = Rack.objects.select_related(
'site', 'group__site', 'role', 'tenant'
).prefetch_related(
'tags'
).annotate(
device_count=get_subquery(Device, 'rack'),
powerfeed_count=get_subquery(PowerFeed, 'rack')
)
queryset = Rack.objects.select_related('site', 'group__site', 'tenant').prefetch_related('tags')
serializer_class = serializers.RackSerializer
filterset_class = filters.RackFilter
@@ -190,11 +154,6 @@ class RackViewSet(CustomFieldModelViewSet):
exclude_pk = None
elevation = rack.get_rack_units(face, exclude_pk)
# Enable filtering rack units by ID
q = request.GET.get('q', None)
if q:
elevation = [u for u in elevation if q in str(u['id'])]
page = self.paginate_queryset(elevation)
if page is not None:
rack_units = serializers.RackUnitSerializer(page, many=True, context={'request': request})
@@ -220,11 +179,7 @@ class RackReservationViewSet(ModelViewSet):
#
class ManufacturerViewSet(ModelViewSet):
queryset = Manufacturer.objects.annotate(
devicetype_count=get_subquery(DeviceType, 'manufacturer'),
inventoryitem_count=get_subquery(InventoryItem, 'manufacturer'),
platform_count=get_subquery(Platform, 'manufacturer')
)
queryset = Manufacturer.objects.all()
serializer_class = serializers.ManufacturerSerializer
filterset_class = filters.ManufacturerFilter
@@ -234,9 +189,7 @@ class ManufacturerViewSet(ModelViewSet):
#
class DeviceTypeViewSet(CustomFieldModelViewSet):
queryset = DeviceType.objects.select_related('manufacturer').prefetch_related('tags').annotate(
device_count=Count('instances')
)
queryset = DeviceType.objects.select_related('manufacturer').prefetch_related('tags')
serializer_class = serializers.DeviceTypeSerializer
filterset_class = filters.DeviceTypeFilter
@@ -298,10 +251,7 @@ class DeviceBayTemplateViewSet(ModelViewSet):
#
class DeviceRoleViewSet(ModelViewSet):
queryset = DeviceRole.objects.annotate(
device_count=get_subquery(Device, 'device_role'),
virtualmachine_count=get_subquery(VirtualMachine, 'role')
)
queryset = DeviceRole.objects.all()
serializer_class = serializers.DeviceRoleSerializer
filterset_class = filters.DeviceRoleFilter
@@ -311,10 +261,7 @@ class DeviceRoleViewSet(ModelViewSet):
#
class PlatformViewSet(ModelViewSet):
queryset = Platform.objects.annotate(
device_count=get_subquery(Device, 'platform'),
virtualmachine_count=get_subquery(VirtualMachine, 'platform')
)
queryset = Platform.objects.all()
serializer_class = serializers.PlatformSerializer
filterset_class = filters.PlatformFilter
@@ -334,23 +281,16 @@ class DeviceViewSet(CustomFieldModelViewSet):
def get_serializer_class(self):
"""
Select the specific serializer based on the request context.
If the `brief` query param equates to True, return the NestedDeviceSerializer
If the `exclude` query param includes `config_context` as a value, return the DeviceSerializer
Else, return the DeviceWithConfigContextSerializer
Include rendered config context when retrieving a single Device.
"""
if self.action == 'retrieve':
return serializers.DeviceWithConfigContextSerializer
request = self.get_serializer_context()['request']
if request.query_params.get('brief', False):
return serializers.NestedDeviceSerializer
elif 'config_context' in request.query_params.get('exclude', []):
return serializers.DeviceSerializer
return serializers.DeviceWithConfigContextSerializer
return serializers.DeviceSerializer
@action(detail=True, url_path='napalm')
def napalm(self, request, pk):
@@ -450,7 +390,7 @@ class ConsoleServerPortViewSet(CableTraceMixin, ModelViewSet):
class PowerPortViewSet(CableTraceMixin, ModelViewSet):
queryset = PowerPort.objects.select_related(
'device', '_connected_poweroutlet__device', '_connected_powerfeed', 'cable'
'device', 'connected_endpoint__device', 'cable'
).prefetch_related(
'tags'
)
@@ -469,9 +409,7 @@ class PowerOutletViewSet(CableTraceMixin, ModelViewSet):
class InterfaceViewSet(CableTraceMixin, ModelViewSet):
queryset = Interface.objects.filter(
device__isnull=False
).select_related(
queryset = Interface.objects.select_related(
'device', '_connected_interface', '_connected_circuittermination', 'cable'
).prefetch_related(
'ip_addresses', 'tags'
@@ -540,7 +478,7 @@ class PowerConnectionViewSet(ListModelMixin, GenericViewSet):
queryset = PowerPort.objects.select_related(
'device', 'connected_endpoint__device'
).filter(
_connected_poweroutlet__isnull=False
connected_endpoint__isnull=False
)
serializer_class = serializers.PowerPortSerializer
filterset_class = filters.PowerConnectionFilter
@@ -548,11 +486,11 @@ class PowerConnectionViewSet(ListModelMixin, GenericViewSet):
class InterfaceConnectionViewSet(ListModelMixin, GenericViewSet):
queryset = Interface.objects.select_related(
'device', '_connected_interface__device'
'device', '_connected_interface', '_connected_circuittermination'
).filter(
# Avoid duplicate connections by only selecting the lower PK in a connected pair
_connected_interface__isnull=False,
pk__lt=F('_connected_interface')
Q(_connected_interface__isnull=False, pk__lt=F('_connected_interface')) |
Q(_connected_circuittermination__isnull=False)
)
serializer_class = serializers.InterfaceConnectionSerializer
filterset_class = filters.InterfaceConnectionFilter
@@ -575,40 +513,10 @@ class CableViewSet(ModelViewSet):
#
class VirtualChassisViewSet(ModelViewSet):
queryset = VirtualChassis.objects.prefetch_related('tags').annotate(
member_count=Count('members')
)
queryset = VirtualChassis.objects.prefetch_related('tags')
serializer_class = serializers.VirtualChassisSerializer
#
# Power panels
#
class PowerPanelViewSet(ModelViewSet):
queryset = PowerPanel.objects.select_related(
'site', 'rack_group'
).annotate(
powerfeed_count=Count('powerfeeds')
)
serializer_class = serializers.PowerPanelSerializer
filterset_class = filters.PowerPanelFilter
#
# Power feeds
#
class PowerFeedViewSet(CustomFieldModelViewSet):
queryset = PowerFeed.objects.select_related(
'power_panel', 'rack'
).prefetch_related(
'tags'
)
serializer_class = serializers.PowerFeedSerializer
filterset_class = filters.PowerFeedFilter
#
# Miscellaneous
#

View File

@@ -43,12 +43,6 @@ RACK_STATUS_CHOICES = [
[RACK_STATUS_DEPRECATED, 'Deprecated'],
]
# Device rack position
DEVICE_POSITION_CHOICES = [
# Rack.u_height is limited to 100
(i, 'Unit {}'.format(i)) for i in range(1, 101)
]
# Parent/child device roles
SUBDEVICE_ROLE_PARENT = True
SUBDEVICE_ROLE_CHILD = False
@@ -66,200 +60,180 @@ IFACE_ORDERING_CHOICES = [
[IFACE_ORDERING_NAME, 'Name (alphabetically)']
]
# Interface types
# Interface form factors
# Virtual
IFACE_TYPE_VIRTUAL = 0
IFACE_TYPE_LAG = 200
IFACE_FF_VIRTUAL = 0
IFACE_FF_LAG = 200
# Ethernet
IFACE_TYPE_100ME_FIXED = 800
IFACE_TYPE_1GE_FIXED = 1000
IFACE_TYPE_1GE_GBIC = 1050
IFACE_TYPE_1GE_SFP = 1100
IFACE_TYPE_10GE_FIXED = 1150
IFACE_TYPE_10GE_CX4 = 1170
IFACE_TYPE_10GE_SFP_PLUS = 1200
IFACE_TYPE_10GE_XFP = 1300
IFACE_TYPE_10GE_XENPAK = 1310
IFACE_TYPE_10GE_X2 = 1320
IFACE_TYPE_25GE_SFP28 = 1350
IFACE_TYPE_40GE_QSFP_PLUS = 1400
IFACE_TYPE_50GE_QSFP28 = 1420
IFACE_TYPE_100GE_CFP = 1500
IFACE_TYPE_100GE_CFP2 = 1510
IFACE_TYPE_100GE_CFP4 = 1520
IFACE_TYPE_100GE_CPAK = 1550
IFACE_TYPE_100GE_QSFP28 = 1600
IFACE_TYPE_200GE_CFP2 = 1650
IFACE_TYPE_200GE_QSFP56 = 1700
IFACE_TYPE_400GE_QSFP_DD = 1750
IFACE_FF_100ME_FIXED = 800
IFACE_FF_1GE_FIXED = 1000
IFACE_FF_1GE_GBIC = 1050
IFACE_FF_1GE_SFP = 1100
IFACE_FF_10GE_FIXED = 1150
IFACE_FF_10GE_CX4 = 1170
IFACE_FF_10GE_SFP_PLUS = 1200
IFACE_FF_10GE_XFP = 1300
IFACE_FF_10GE_XENPAK = 1310
IFACE_FF_10GE_X2 = 1320
IFACE_FF_25GE_SFP28 = 1350
IFACE_FF_40GE_QSFP_PLUS = 1400
IFACE_FF_100GE_CFP = 1500
IFACE_FF_100GE_CFP2 = 1510
IFACE_FF_100GE_CFP4 = 1520
IFACE_FF_100GE_CPAK = 1550
IFACE_FF_100GE_QSFP28 = 1600
# Wireless
IFACE_TYPE_80211A = 2600
IFACE_TYPE_80211G = 2610
IFACE_TYPE_80211N = 2620
IFACE_TYPE_80211AC = 2630
IFACE_TYPE_80211AD = 2640
# Cellular
IFACE_TYPE_GSM = 2810
IFACE_TYPE_CDMA = 2820
IFACE_TYPE_LTE = 2830
IFACE_FF_80211A = 2600
IFACE_FF_80211G = 2610
IFACE_FF_80211N = 2620
IFACE_FF_80211AC = 2630
IFACE_FF_80211AD = 2640
# SONET
IFACE_TYPE_SONET_OC3 = 6100
IFACE_TYPE_SONET_OC12 = 6200
IFACE_TYPE_SONET_OC48 = 6300
IFACE_TYPE_SONET_OC192 = 6400
IFACE_TYPE_SONET_OC768 = 6500
IFACE_TYPE_SONET_OC1920 = 6600
IFACE_TYPE_SONET_OC3840 = 6700
IFACE_FF_SONET_OC3 = 6100
IFACE_FF_SONET_OC12 = 6200
IFACE_FF_SONET_OC48 = 6300
IFACE_FF_SONET_OC192 = 6400
IFACE_FF_SONET_OC768 = 6500
IFACE_FF_SONET_OC1920 = 6600
IFACE_FF_SONET_OC3840 = 6700
# Fibrechannel
IFACE_TYPE_1GFC_SFP = 3010
IFACE_TYPE_2GFC_SFP = 3020
IFACE_TYPE_4GFC_SFP = 3040
IFACE_TYPE_8GFC_SFP_PLUS = 3080
IFACE_TYPE_16GFC_SFP_PLUS = 3160
IFACE_TYPE_32GFC_SFP28 = 3320
IFACE_TYPE_128GFC_QSFP28 = 3400
IFACE_FF_1GFC_SFP = 3010
IFACE_FF_2GFC_SFP = 3020
IFACE_FF_4GFC_SFP = 3040
IFACE_FF_8GFC_SFP_PLUS = 3080
IFACE_FF_16GFC_SFP_PLUS = 3160
IFACE_FF_32GFC_SFP28 = 3320
IFACE_FF_128GFC_QSFP28 = 3400
# Serial
IFACE_TYPE_T1 = 4000
IFACE_TYPE_E1 = 4010
IFACE_TYPE_T3 = 4040
IFACE_TYPE_E3 = 4050
IFACE_FF_T1 = 4000
IFACE_FF_E1 = 4010
IFACE_FF_T3 = 4040
IFACE_FF_E3 = 4050
# Stacking
IFACE_TYPE_STACKWISE = 5000
IFACE_TYPE_STACKWISE_PLUS = 5050
IFACE_TYPE_FLEXSTACK = 5100
IFACE_TYPE_FLEXSTACK_PLUS = 5150
IFACE_TYPE_JUNIPER_VCP = 5200
IFACE_TYPE_SUMMITSTACK = 5300
IFACE_TYPE_SUMMITSTACK128 = 5310
IFACE_TYPE_SUMMITSTACK256 = 5320
IFACE_TYPE_SUMMITSTACK512 = 5330
IFACE_FF_STACKWISE = 5000
IFACE_FF_STACKWISE_PLUS = 5050
IFACE_FF_FLEXSTACK = 5100
IFACE_FF_FLEXSTACK_PLUS = 5150
IFACE_FF_JUNIPER_VCP = 5200
IFACE_FF_SUMMITSTACK = 5300
IFACE_FF_SUMMITSTACK128 = 5310
IFACE_FF_SUMMITSTACK256 = 5320
IFACE_FF_SUMMITSTACK512 = 5330
# Other
IFACE_TYPE_OTHER = 32767
IFACE_FF_OTHER = 32767
IFACE_TYPE_CHOICES = [
IFACE_FF_CHOICES = [
[
'Virtual interfaces',
[
[IFACE_TYPE_VIRTUAL, 'Virtual'],
[IFACE_TYPE_LAG, 'Link Aggregation Group (LAG)'],
[IFACE_FF_VIRTUAL, 'Virtual'],
[IFACE_FF_LAG, 'Link Aggregation Group (LAG)'],
],
],
[
'Ethernet (fixed)',
[
[IFACE_TYPE_100ME_FIXED, '100BASE-TX (10/100ME)'],
[IFACE_TYPE_1GE_FIXED, '1000BASE-T (1GE)'],
[IFACE_TYPE_10GE_FIXED, '10GBASE-T (10GE)'],
[IFACE_TYPE_10GE_CX4, '10GBASE-CX4 (10GE)'],
[IFACE_FF_100ME_FIXED, '100BASE-TX (10/100ME)'],
[IFACE_FF_1GE_FIXED, '1000BASE-T (1GE)'],
[IFACE_FF_10GE_FIXED, '10GBASE-T (10GE)'],
[IFACE_FF_10GE_CX4, '10GBASE-CX4 (10GE)'],
]
],
[
'Ethernet (modular)',
[
[IFACE_TYPE_1GE_GBIC, 'GBIC (1GE)'],
[IFACE_TYPE_1GE_SFP, 'SFP (1GE)'],
[IFACE_TYPE_10GE_SFP_PLUS, 'SFP+ (10GE)'],
[IFACE_TYPE_10GE_XFP, 'XFP (10GE)'],
[IFACE_TYPE_10GE_XENPAK, 'XENPAK (10GE)'],
[IFACE_TYPE_10GE_X2, 'X2 (10GE)'],
[IFACE_TYPE_25GE_SFP28, 'SFP28 (25GE)'],
[IFACE_TYPE_40GE_QSFP_PLUS, 'QSFP+ (40GE)'],
[IFACE_TYPE_50GE_QSFP28, 'QSFP28 (50GE)'],
[IFACE_TYPE_100GE_CFP, 'CFP (100GE)'],
[IFACE_TYPE_100GE_CFP2, 'CFP2 (100GE)'],
[IFACE_TYPE_200GE_CFP2, 'CFP2 (200GE)'],
[IFACE_TYPE_100GE_CFP4, 'CFP4 (100GE)'],
[IFACE_TYPE_100GE_CPAK, 'Cisco CPAK (100GE)'],
[IFACE_TYPE_100GE_QSFP28, 'QSFP28 (100GE)'],
[IFACE_TYPE_200GE_QSFP56, 'QSFP56 (200GE)'],
[IFACE_TYPE_400GE_QSFP_DD, 'QSFP-DD (400GE)'],
[IFACE_FF_1GE_GBIC, 'GBIC (1GE)'],
[IFACE_FF_1GE_SFP, 'SFP (1GE)'],
[IFACE_FF_10GE_SFP_PLUS, 'SFP+ (10GE)'],
[IFACE_FF_10GE_XFP, 'XFP (10GE)'],
[IFACE_FF_10GE_XENPAK, 'XENPAK (10GE)'],
[IFACE_FF_10GE_X2, 'X2 (10GE)'],
[IFACE_FF_25GE_SFP28, 'SFP28 (25GE)'],
[IFACE_FF_40GE_QSFP_PLUS, 'QSFP+ (40GE)'],
[IFACE_FF_100GE_CFP, 'CFP (100GE)'],
[IFACE_FF_100GE_CFP2, 'CFP2 (100GE)'],
[IFACE_FF_100GE_CFP4, 'CFP4 (100GE)'],
[IFACE_FF_100GE_CPAK, 'Cisco CPAK (100GE)'],
[IFACE_FF_100GE_QSFP28, 'QSFP28 (100GE)'],
]
],
[
'Wireless',
[
[IFACE_TYPE_80211A, 'IEEE 802.11a'],
[IFACE_TYPE_80211G, 'IEEE 802.11b/g'],
[IFACE_TYPE_80211N, 'IEEE 802.11n'],
[IFACE_TYPE_80211AC, 'IEEE 802.11ac'],
[IFACE_TYPE_80211AD, 'IEEE 802.11ad'],
]
],
[
'Cellular',
[
[IFACE_TYPE_GSM, 'GSM'],
[IFACE_TYPE_CDMA, 'CDMA'],
[IFACE_TYPE_LTE, 'LTE'],
[IFACE_FF_80211A, 'IEEE 802.11a'],
[IFACE_FF_80211G, 'IEEE 802.11b/g'],
[IFACE_FF_80211N, 'IEEE 802.11n'],
[IFACE_FF_80211AC, 'IEEE 802.11ac'],
[IFACE_FF_80211AD, 'IEEE 802.11ad'],
]
],
[
'SONET',
[
[IFACE_TYPE_SONET_OC3, 'OC-3/STM-1'],
[IFACE_TYPE_SONET_OC12, 'OC-12/STM-4'],
[IFACE_TYPE_SONET_OC48, 'OC-48/STM-16'],
[IFACE_TYPE_SONET_OC192, 'OC-192/STM-64'],
[IFACE_TYPE_SONET_OC768, 'OC-768/STM-256'],
[IFACE_TYPE_SONET_OC1920, 'OC-1920/STM-640'],
[IFACE_TYPE_SONET_OC3840, 'OC-3840/STM-1234'],
[IFACE_FF_SONET_OC3, 'OC-3/STM-1'],
[IFACE_FF_SONET_OC12, 'OC-12/STM-4'],
[IFACE_FF_SONET_OC48, 'OC-48/STM-16'],
[IFACE_FF_SONET_OC192, 'OC-192/STM-64'],
[IFACE_FF_SONET_OC768, 'OC-768/STM-256'],
[IFACE_FF_SONET_OC1920, 'OC-1920/STM-640'],
[IFACE_FF_SONET_OC3840, 'OC-3840/STM-1234'],
]
],
[
'FibreChannel',
[
[IFACE_TYPE_1GFC_SFP, 'SFP (1GFC)'],
[IFACE_TYPE_2GFC_SFP, 'SFP (2GFC)'],
[IFACE_TYPE_4GFC_SFP, 'SFP (4GFC)'],
[IFACE_TYPE_8GFC_SFP_PLUS, 'SFP+ (8GFC)'],
[IFACE_TYPE_16GFC_SFP_PLUS, 'SFP+ (16GFC)'],
[IFACE_TYPE_32GFC_SFP28, 'SFP28 (32GFC)'],
[IFACE_TYPE_128GFC_QSFP28, 'QSFP28 (128GFC)'],
[IFACE_FF_1GFC_SFP, 'SFP (1GFC)'],
[IFACE_FF_2GFC_SFP, 'SFP (2GFC)'],
[IFACE_FF_4GFC_SFP, 'SFP (4GFC)'],
[IFACE_FF_8GFC_SFP_PLUS, 'SFP+ (8GFC)'],
[IFACE_FF_16GFC_SFP_PLUS, 'SFP+ (16GFC)'],
[IFACE_FF_32GFC_SFP28, 'SFP28 (32GFC)'],
[IFACE_FF_128GFC_QSFP28, 'QSFP28 (128GFC)'],
]
],
[
'Serial',
[
[IFACE_TYPE_T1, 'T1 (1.544 Mbps)'],
[IFACE_TYPE_E1, 'E1 (2.048 Mbps)'],
[IFACE_TYPE_T3, 'T3 (45 Mbps)'],
[IFACE_TYPE_E3, 'E3 (34 Mbps)'],
[IFACE_FF_T1, 'T1 (1.544 Mbps)'],
[IFACE_FF_E1, 'E1 (2.048 Mbps)'],
[IFACE_FF_T3, 'T3 (45 Mbps)'],
[IFACE_FF_E3, 'E3 (34 Mbps)'],
]
],
[
'Stacking',
[
[IFACE_TYPE_STACKWISE, 'Cisco StackWise'],
[IFACE_TYPE_STACKWISE_PLUS, 'Cisco StackWise Plus'],
[IFACE_TYPE_FLEXSTACK, 'Cisco FlexStack'],
[IFACE_TYPE_FLEXSTACK_PLUS, 'Cisco FlexStack Plus'],
[IFACE_TYPE_JUNIPER_VCP, 'Juniper VCP'],
[IFACE_TYPE_SUMMITSTACK, 'Extreme SummitStack'],
[IFACE_TYPE_SUMMITSTACK128, 'Extreme SummitStack-128'],
[IFACE_TYPE_SUMMITSTACK256, 'Extreme SummitStack-256'],
[IFACE_TYPE_SUMMITSTACK512, 'Extreme SummitStack-512'],
[IFACE_FF_STACKWISE, 'Cisco StackWise'],
[IFACE_FF_STACKWISE_PLUS, 'Cisco StackWise Plus'],
[IFACE_FF_FLEXSTACK, 'Cisco FlexStack'],
[IFACE_FF_FLEXSTACK_PLUS, 'Cisco FlexStack Plus'],
[IFACE_FF_JUNIPER_VCP, 'Juniper VCP'],
[IFACE_FF_SUMMITSTACK, 'Extreme SummitStack'],
[IFACE_FF_SUMMITSTACK128, 'Extreme SummitStack-128'],
[IFACE_FF_SUMMITSTACK256, 'Extreme SummitStack-256'],
[IFACE_FF_SUMMITSTACK512, 'Extreme SummitStack-512'],
]
],
[
'Other',
[
[IFACE_TYPE_OTHER, 'Other'],
[IFACE_FF_OTHER, 'Other'],
]
],
]
VIRTUAL_IFACE_TYPES = [
IFACE_TYPE_VIRTUAL,
IFACE_TYPE_LAG,
IFACE_FF_VIRTUAL,
IFACE_FF_LAG,
]
WIRELESS_IFACE_TYPES = [
IFACE_TYPE_80211A,
IFACE_TYPE_80211G,
IFACE_TYPE_80211N,
IFACE_TYPE_80211AC,
IFACE_TYPE_80211AD,
IFACE_FF_80211A,
IFACE_FF_80211G,
IFACE_FF_80211N,
IFACE_FF_80211AC,
IFACE_FF_80211AD,
]
NONCONNECTABLE_IFACE_TYPES = VIRTUAL_IFACE_TYPES + WIRELESS_IFACE_TYPES
@@ -275,23 +249,18 @@ IFACE_MODE_CHOICES = [
# Pass-through port types
PORT_TYPE_8P8C = 1000
PORT_TYPE_110_PUNCH = 1100
PORT_TYPE_ST = 2000
PORT_TYPE_SC = 2100
PORT_TYPE_SC_APC = 2110
PORT_TYPE_FC = 2200
PORT_TYPE_LC = 2300
PORT_TYPE_LC_APC = 2310
PORT_TYPE_MTRJ = 2400
PORT_TYPE_MPO = 2500
PORT_TYPE_LSH = 2600
PORT_TYPE_LSH_APC = 2610
PORT_TYPE_CHOICES = [
[
'Copper',
[
[PORT_TYPE_8P8C, '8P8C'],
[PORT_TYPE_110_PUNCH, '110 Punch'],
],
],
[
@@ -299,13 +268,10 @@ PORT_TYPE_CHOICES = [
[
[PORT_TYPE_FC, 'FC'],
[PORT_TYPE_LC, 'LC'],
[PORT_TYPE_LC_APC, 'LC/APC'],
[PORT_TYPE_LSH, 'LSH'],
[PORT_TYPE_LSH_APC, 'LSH/APC'],
[PORT_TYPE_MPO, 'MPO'],
[PORT_TYPE_MTRJ, 'MTRJ'],
[PORT_TYPE_SC, 'SC'],
[PORT_TYPE_SC_APC, 'SC/APC'],
[PORT_TYPE_ST, 'ST'],
]
]
@@ -318,7 +284,6 @@ DEVICE_STATUS_PLANNED = 2
DEVICE_STATUS_STAGED = 3
DEVICE_STATUS_FAILED = 4
DEVICE_STATUS_INVENTORY = 5
DEVICE_STATUS_DECOMMISSIONING = 6
DEVICE_STATUS_CHOICES = [
[DEVICE_STATUS_ACTIVE, 'Active'],
[DEVICE_STATUS_OFFLINE, 'Offline'],
@@ -326,7 +291,6 @@ DEVICE_STATUS_CHOICES = [
[DEVICE_STATUS_STAGED, 'Staged'],
[DEVICE_STATUS_FAILED, 'Failed'],
[DEVICE_STATUS_INVENTORY, 'Inventory'],
[DEVICE_STATUS_DECOMMISSIONING, 'Decommissioning'],
]
# Site statuses
@@ -347,7 +311,6 @@ STATUS_CLASSES = {
3: 'primary',
4: 'danger',
5: 'default',
6: 'warning',
}
# Console/power/interface connection statuses
@@ -370,17 +333,11 @@ CABLE_TYPE_CAT5E = 1510
CABLE_TYPE_CAT6 = 1600
CABLE_TYPE_CAT6A = 1610
CABLE_TYPE_CAT7 = 1700
CABLE_TYPE_DAC_ACTIVE = 1800
CABLE_TYPE_DAC_PASSIVE = 1810
CABLE_TYPE_MMF = 3000
CABLE_TYPE_MMF_OM1 = 3010
CABLE_TYPE_MMF_OM2 = 3020
CABLE_TYPE_MMF_OM3 = 3030
CABLE_TYPE_MMF_OM4 = 3040
CABLE_TYPE_SMF = 3500
CABLE_TYPE_SMF_OS1 = 3510
CABLE_TYPE_SMF_OS2 = 3520
CABLE_TYPE_AOC = 3800
CABLE_TYPE_POWER = 5000
CABLE_TYPE_CHOICES = (
(
@@ -391,21 +348,15 @@ CABLE_TYPE_CHOICES = (
(CABLE_TYPE_CAT6, 'CAT6'),
(CABLE_TYPE_CAT6A, 'CAT6a'),
(CABLE_TYPE_CAT7, 'CAT7'),
(CABLE_TYPE_DAC_ACTIVE, 'Direct Attach Copper (Active)'),
(CABLE_TYPE_DAC_PASSIVE, 'Direct Attach Copper (Passive)'),
),
),
(
'Fiber', (
(CABLE_TYPE_MMF, 'Multimode Fiber'),
(CABLE_TYPE_MMF_OM1, 'Multimode Fiber (OM1)'),
(CABLE_TYPE_MMF_OM2, 'Multimode Fiber (OM2)'),
(CABLE_TYPE_MMF_OM3, 'Multimode Fiber (OM3)'),
(CABLE_TYPE_MMF_OM4, 'Multimode Fiber (OM4)'),
(CABLE_TYPE_SMF, 'Singlemode Fiber'),
(CABLE_TYPE_SMF_OS1, 'Singlemode Fiber (OS1)'),
(CABLE_TYPE_SMF_OS2, 'Singlemode Fiber (OS2)'),
(CABLE_TYPE_AOC, 'Active Optical Cabling (AOC)'),
),
),
(CABLE_TYPE_POWER, 'Power'),
@@ -425,7 +376,7 @@ CABLE_TERMINATION_TYPE_CHOICES = {
COMPATIBLE_TERMINATION_TYPES = {
'consoleport': ['consoleserverport', 'frontport', 'rearport'],
'consoleserverport': ['consoleport', 'frontport', 'rearport'],
'powerport': ['poweroutlet', 'powerfeed'],
'powerport': ['poweroutlet'],
'poweroutlet': ['powerport'],
'interface': ['interface', 'circuittermination', 'frontport', 'rearport'],
'frontport': ['consoleport', 'consoleserverport', 'interface', 'frontport', 'rearport', 'circuittermination'],
@@ -448,41 +399,3 @@ RACK_DIMENSION_UNIT_CHOICES = (
(LENGTH_UNIT_MILLIMETER, 'Millimeters'),
(LENGTH_UNIT_INCH, 'Inches'),
)
# Power feeds
POWERFEED_TYPE_PRIMARY = 1
POWERFEED_TYPE_REDUNDANT = 2
POWERFEED_TYPE_CHOICES = (
(POWERFEED_TYPE_PRIMARY, 'Primary'),
(POWERFEED_TYPE_REDUNDANT, 'Redundant'),
)
POWERFEED_SUPPLY_AC = 1
POWERFEED_SUPPLY_DC = 2
POWERFEED_SUPPLY_CHOICES = (
(POWERFEED_SUPPLY_AC, 'AC'),
(POWERFEED_SUPPLY_DC, 'DC'),
)
POWERFEED_PHASE_SINGLE = 1
POWERFEED_PHASE_3PHASE = 3
POWERFEED_PHASE_CHOICES = (
(POWERFEED_PHASE_SINGLE, 'Single phase'),
(POWERFEED_PHASE_3PHASE, 'Three-phase'),
)
POWERFEED_STATUS_OFFLINE = 0
POWERFEED_STATUS_ACTIVE = 1
POWERFEED_STATUS_PLANNED = 2
POWERFEED_STATUS_FAILED = 4
POWERFEED_STATUS_CHOICES = (
(POWERFEED_STATUS_ACTIVE, 'Active'),
(POWERFEED_STATUS_OFFLINE, 'Offline'),
(POWERFEED_STATUS_PLANNED, 'Planned'),
(POWERFEED_STATUS_FAILED, 'Failed'),
)
POWERFEED_LEG_A = 1
POWERFEED_LEG_B = 2
POWERFEED_LEG_C = 3
POWERFEED_LEG_CHOICES = (
(POWERFEED_LEG_A, 'A'),
(POWERFEED_LEG_B, 'B'),
(POWERFEED_LEG_C, 'C'),
)

View File

@@ -1,5 +0,0 @@
class LoopDetected(Exception):
"""
A loop has been detected while tracing a cable path.
"""
pass

View File

@@ -31,7 +31,7 @@ class MACAddressField(models.Field):
try:
return EUI(value, version=48, dialect=mac_unix_expanded_uppercase)
except AddrFormatError as e:
raise ValidationError("Invalid MAC address format: {}".format(value))
raise ValidationError(e)
def db_type(self, connection):
return 'macaddr'

View File

@@ -1,6 +1,5 @@
import django_filters
from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist
from django.db.models import Q
from netaddr import EUI
@@ -9,21 +8,22 @@ from netaddr.core import AddrFormatError
from extras.filters import CustomFieldFilterSet
from tenancy.models import Tenant
from utilities.constants import COLOR_CHOICES
from utilities.filters import (
NameSlugSearchFilterSet, NullableCharFieldFilter, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter
)
from utilities.filters import NullableCharFieldFilter, NumericInFilter, TagFilter
from virtualization.models import Cluster
from .constants import *
from .models import (
Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate,
InventoryItem, Manufacturer, Platform, PowerFeed, PowerOutlet, PowerOutletTemplate, PowerPanel, PowerPort,
PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site,
VirtualChassis,
InventoryItem, Manufacturer, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack,
RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site, VirtualChassis,
)
class RegionFilter(NameSlugSearchFilterSet):
class RegionFilter(django_filters.FilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
)
parent_id = django_filters.ModelMultipleChoiceFilter(
queryset=Region.objects.all(),
label='Parent region (ID)',
@@ -39,8 +39,17 @@ class RegionFilter(NameSlugSearchFilterSet):
model = Region
fields = ['name', 'slug']
def search(self, queryset, name, value):
if not value.strip():
return queryset
qs_filter = (
Q(name__icontains=value) |
Q(slug__icontains=value)
)
return queryset.filter(qs_filter)
class SiteFilter(CustomFieldFilterSet):
class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
id__in = NumericInFilter(
field_name='id',
lookup_expr='in'
@@ -53,14 +62,13 @@ class SiteFilter(CustomFieldFilterSet):
choices=SITE_STATUS_CHOICES,
null_value=None
)
region_id = TreeNodeMultipleChoiceFilter(
region_id = django_filters.ModelMultipleChoiceFilter(
queryset=Region.objects.all(),
field_name='region__in',
label='Region (ID)',
)
region = TreeNodeMultipleChoiceFilter(
region = django_filters.ModelMultipleChoiceFilter(
field_name='region__slug',
queryset=Region.objects.all(),
field_name='region__in',
to_field_name='slug',
label='Region (slug)',
)
@@ -101,7 +109,11 @@ class SiteFilter(CustomFieldFilterSet):
return queryset.filter(qs_filter)
class RackGroupFilter(NameSlugSearchFilterSet):
class RackGroupFilter(django_filters.FilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
)
site_id = django_filters.ModelMultipleChoiceFilter(
queryset=Site.objects.all(),
label='Site (ID)',
@@ -117,15 +129,24 @@ class RackGroupFilter(NameSlugSearchFilterSet):
model = RackGroup
fields = ['site_id', 'name', 'slug']
def search(self, queryset, name, value):
if not value.strip():
return queryset
qs_filter = (
Q(name__icontains=value) |
Q(slug__icontains=value)
)
return queryset.filter(qs_filter)
class RackRoleFilter(NameSlugSearchFilterSet):
class RackRoleFilter(django_filters.FilterSet):
class Meta:
model = RackRole
fields = ['name', 'slug', 'color']
class RackFilter(CustomFieldFilterSet):
class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
id__in = NumericInFilter(
field_name='id',
lookup_expr='in'
@@ -272,7 +293,7 @@ class RackReservationFilter(django_filters.FilterSet):
)
class ManufacturerFilter(NameSlugSearchFilterSet):
class ManufacturerFilter(django_filters.FilterSet):
class Meta:
model = Manufacturer
@@ -362,7 +383,7 @@ class DeviceTypeFilter(CustomFieldFilterSet):
)
class DeviceTypeComponentFilterSet(NameSlugSearchFilterSet):
class DeviceTypeComponentFilterSet(django_filters.FilterSet):
devicetype_id = django_filters.ModelMultipleChoiceFilter(
queryset=DeviceType.objects.all(),
field_name='device_type_id',
@@ -402,7 +423,7 @@ class InterfaceTemplateFilter(DeviceTypeComponentFilterSet):
class Meta:
model = InterfaceTemplate
fields = ['name', 'type', 'mgmt_only']
fields = ['name', 'form_factor', 'mgmt_only']
class FrontPortTemplateFilter(DeviceTypeComponentFilterSet):
@@ -426,14 +447,14 @@ class DeviceBayTemplateFilter(DeviceTypeComponentFilterSet):
fields = ['name']
class DeviceRoleFilter(NameSlugSearchFilterSet):
class DeviceRoleFilter(django_filters.FilterSet):
class Meta:
model = DeviceRole
fields = ['name', 'slug', 'color', 'vm_role']
class PlatformFilter(NameSlugSearchFilterSet):
class PlatformFilter(django_filters.FilterSet):
manufacturer_id = django_filters.ModelMultipleChoiceFilter(
field_name='manufacturer',
queryset=Manufacturer.objects.all(),
@@ -508,15 +529,14 @@ class DeviceFilter(CustomFieldFilterSet):
)
name = NullableCharFieldFilter()
asset_tag = NullableCharFieldFilter()
region_id = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(),
field_name='site__region__in',
region_id = django_filters.NumberFilter(
method='filter_region',
field_name='pk',
label='Region (ID)',
)
region = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(),
field_name='site__region__in',
to_field_name='slug',
region = django_filters.CharFilter(
method='filter_region',
field_name='slug',
label='Region (slug)',
)
site_id = django_filters.ModelMultipleChoiceFilter(
@@ -539,10 +559,6 @@ class DeviceFilter(CustomFieldFilterSet):
queryset=Rack.objects.all(),
label='Rack (ID)',
)
position = django_filters.ChoiceFilter(
choices=DEVICE_POSITION_CHOICES,
null_label='Non-racked'
)
cluster_id = django_filters.ModelMultipleChoiceFilter(
queryset=Cluster.objects.all(),
label='VM cluster (ID)',
@@ -602,7 +618,7 @@ class DeviceFilter(CustomFieldFilterSet):
class Meta:
model = Device
fields = ['serial', 'face']
fields = ['serial', 'position']
def search(self, queryset, name, value):
if not value.strip():
@@ -615,6 +631,16 @@ class DeviceFilter(CustomFieldFilterSet):
Q(comments__icontains=value)
).distinct()
def filter_region(self, queryset, name, value):
try:
region = Region.objects.get(**{name: value})
except ObjectDoesNotExist:
return queryset.none()
return queryset.filter(
Q(site__region=region) |
Q(site__region__in=region.get_descendants())
)
def _mac_address(self, queryset, name, value):
value = value.strip()
if not value:
@@ -660,10 +686,6 @@ class DeviceFilter(CustomFieldFilterSet):
class DeviceComponentFilterSet(django_filters.FilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
)
device_id = django_filters.ModelChoiceFilter(
queryset=Device.objects.all(),
label='Device (ID)',
@@ -675,14 +697,6 @@ class DeviceComponentFilterSet(django_filters.FilterSet):
)
tag = TagFilter()
def search(self, queryset, name, value):
if not value.strip():
return queryset
return queryset.filter(
Q(name__icontains=value) |
Q(description__icontains=value)
)
class ConsolePortFilter(DeviceComponentFilterSet):
cabled = django_filters.BooleanFilter(
@@ -693,7 +707,7 @@ class ConsolePortFilter(DeviceComponentFilterSet):
class Meta:
model = ConsolePort
fields = ['name', 'description', 'connection_status']
fields = ['name', 'connection_status']
class ConsoleServerPortFilter(DeviceComponentFilterSet):
@@ -705,7 +719,7 @@ class ConsoleServerPortFilter(DeviceComponentFilterSet):
class Meta:
model = ConsoleServerPort
fields = ['name', 'description', 'connection_status']
fields = ['name', 'connection_status']
class PowerPortFilter(DeviceComponentFilterSet):
@@ -717,7 +731,7 @@ class PowerPortFilter(DeviceComponentFilterSet):
class Meta:
model = PowerPort
fields = ['name', 'description', 'connection_status']
fields = ['name', 'connection_status']
class PowerOutletFilter(DeviceComponentFilterSet):
@@ -729,17 +743,13 @@ class PowerOutletFilter(DeviceComponentFilterSet):
class Meta:
model = PowerOutlet
fields = ['name', 'description', 'connection_status']
fields = ['name', 'connection_status']
class InterfaceFilter(django_filters.FilterSet):
"""
Not using DeviceComponentFilterSet for Interfaces because we need to check for VirtualChassis membership.
"""
q = django_filters.CharFilter(
method='search',
label='Search',
)
device = django_filters.CharFilter(
method='filter_device',
field_name='name',
@@ -755,9 +765,9 @@ class InterfaceFilter(django_filters.FilterSet):
lookup_expr='isnull',
exclude=True
)
kind = django_filters.CharFilter(
method='filter_kind',
label='Kind of interface',
type = django_filters.CharFilter(
method='filter_type',
label='Interface type',
)
lag_id = django_filters.ModelMultipleChoiceFilter(
field_name='lag',
@@ -777,22 +787,14 @@ class InterfaceFilter(django_filters.FilterSet):
method='filter_vlan',
label='Assigned VID'
)
type = django_filters.MultipleChoiceFilter(
choices=IFACE_TYPE_CHOICES,
form_factor = django_filters.MultipleChoiceFilter(
choices=IFACE_FF_CHOICES,
null_value=None
)
class Meta:
model = Interface
fields = ['name', 'connection_status', 'type', 'enabled', 'mtu', 'mgmt_only', 'description']
def search(self, queryset, name, value):
if not value.strip():
return queryset
return queryset.filter(
Q(name__icontains=value) |
Q(description__icontains=value)
).distinct()
fields = ['name', 'connection_status', 'form_factor', 'enabled', 'mtu', 'mgmt_only']
def filter_device(self, queryset, name, value):
try:
@@ -820,12 +822,13 @@ class InterfaceFilter(django_filters.FilterSet):
Q(tagged_vlans__vid=value)
)
def filter_kind(self, queryset, name, value):
def filter_type(self, queryset, name, value):
value = value.strip().lower()
return {
'physical': queryset.exclude(type__in=NONCONNECTABLE_IFACE_TYPES),
'virtual': queryset.filter(type__in=VIRTUAL_IFACE_TYPES),
'wireless': queryset.filter(type__in=WIRELESS_IFACE_TYPES),
'physical': queryset.exclude(form_factor__in=NONCONNECTABLE_IFACE_TYPES),
'virtual': queryset.filter(form_factor__in=VIRTUAL_IFACE_TYPES),
'wireless': queryset.filter(form_factor__in=WIRELESS_IFACE_TYPES),
'lag': queryset.filter(form_factor=IFACE_FF_LAG),
}.get(value, queryset.none())
def _mac_address(self, queryset, name, value):
@@ -848,7 +851,7 @@ class FrontPortFilter(DeviceComponentFilterSet):
class Meta:
model = FrontPort
fields = ['name', 'type', 'description']
fields = ['name', 'type']
class RearPortFilter(DeviceComponentFilterSet):
@@ -860,14 +863,14 @@ class RearPortFilter(DeviceComponentFilterSet):
class Meta:
model = RearPort
fields = ['name', 'type', 'description']
fields = ['name', 'type']
class DeviceBayFilter(DeviceComponentFilterSet):
class Meta:
model = DeviceBay
fields = ['name', 'description']
fields = ['name']
class InventoryItemFilter(DeviceComponentFilterSet):
@@ -971,14 +974,6 @@ class CableFilter(django_filters.FilterSet):
color = django_filters.MultipleChoiceFilter(
choices=COLOR_CHOICES
)
device = django_filters.CharFilter(
method='filter_connected_device',
field_name='name'
)
device_id = django_filters.CharFilter(
method='filter_connected_device',
field_name='pk'
)
class Meta:
model = Cable
@@ -989,16 +984,6 @@ class CableFilter(django_filters.FilterSet):
return queryset
return queryset.filter(label__icontains=value)
def filter_connected_device(self, queryset, name, value):
if not value.strip():
return queryset
try:
device = Device.objects.get(**{name: value})
except ObjectDoesNotExist:
return queryset.none()
cable_pks = device.get_cables(pk_list=True)
return queryset.filter(pk__in=cable_pks)
class ConsoleConnectionFilter(django_filters.FilterSet):
site = django_filters.CharFilter(
@@ -1045,14 +1030,14 @@ class PowerConnectionFilter(django_filters.FilterSet):
def filter_site(self, queryset, name, value):
if not value.strip():
return queryset
return queryset.filter(_connected_poweroutlet__device__site__slug=value)
return queryset.filter(connected_endpoint__device__site__slug=value)
def filter_device(self, queryset, name, value):
if not value.strip():
return queryset
return queryset.filter(
Q(device__name__icontains=value) |
Q(_connected_poweroutlet__device__name__icontains=value)
Q(connected_endpoint__device__name__icontains=value)
)
@@ -1085,86 +1070,3 @@ class InterfaceConnectionFilter(django_filters.FilterSet):
Q(device__name__icontains=value) |
Q(_connected_interface__device__name__icontains=value)
)
class PowerPanelFilter(django_filters.FilterSet):
id__in = NumericInFilter(
field_name='id',
lookup_expr='in'
)
q = django_filters.CharFilter(
method='search',
label='Search',
)
site_id = django_filters.ModelMultipleChoiceFilter(
queryset=Site.objects.all(),
label='Site (ID)',
)
site = django_filters.ModelMultipleChoiceFilter(
field_name='site__slug',
queryset=Site.objects.all(),
to_field_name='slug',
label='Site name (slug)',
)
rack_group_id = django_filters.ModelMultipleChoiceFilter(
field_name='rack_group',
queryset=RackGroup.objects.all(),
label='Rack group (ID)',
)
class Meta:
model = PowerPanel
fields = ['name']
def search(self, queryset, name, value):
if not value.strip():
return queryset
qs_filter = (
Q(name__icontains=value)
)
return queryset.filter(qs_filter)
class PowerFeedFilter(CustomFieldFilterSet):
id__in = NumericInFilter(
field_name='id',
lookup_expr='in'
)
q = django_filters.CharFilter(
method='search',
label='Search',
)
site_id = django_filters.ModelMultipleChoiceFilter(
field_name='power_panel__site',
queryset=Site.objects.all(),
label='Site (ID)',
)
site = django_filters.ModelMultipleChoiceFilter(
field_name='power_panel__site__slug',
queryset=Site.objects.all(),
to_field_name='slug',
label='Site name (slug)',
)
power_panel_id = django_filters.ModelMultipleChoiceFilter(
queryset=PowerPanel.objects.all(),
label='Power panel (ID)',
)
rack_id = django_filters.ModelMultipleChoiceFilter(
field_name='rack',
queryset=Rack.objects.all(),
label='Rack (ID)',
)
tag = TagFilter()
class Meta:
model = PowerFeed
fields = ['name', 'status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'power_factor']
def search(self, queryset, name, value):
if not value.strip():
return queryset
qs_filter = (
Q(name__icontains=value) |
Q(comments__icontains=value)
)
return queryset.filter(qs_filter)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -14,6 +14,22 @@ CHANNEL_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^.*:(\d{{1,9}})(\.\d{{1,9}})?$')
VC_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^.*\.(\d{{1,9}})$') AS integer), 0)"
class DeviceComponentManager(Manager):
def get_queryset(self):
queryset = super().get_queryset()
table_name = self.model._meta.db_table
sql = r"CONCAT(REGEXP_REPLACE({}.name, '\d+$', ''), LPAD(SUBSTRING({}.name FROM '\d+$'), 8, '0'))"
# Pad any trailing digits to effect natural sorting
return queryset.extra(
select={
'name_padded': sql.format(table_name, table_name),
}
).order_by('name_padded')
class InterfaceQuerySet(QuerySet):
def connectable(self):
@@ -21,7 +37,7 @@ class InterfaceQuerySet(QuerySet):
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)
return self.exclude(form_factor__in=NONCONNECTABLE_IFACE_TYPES)
class InterfaceManager(Manager):
@@ -48,15 +64,11 @@ class InterfaceManager(Manager):
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'
'_slot', '_subslot', '_position', '_subposition', '_type', '_id', '_channel', '_vc', 'name',
]
fields = {

View File

@@ -1,38 +0,0 @@
# Generated by Django 2.1.5 on 2019-02-14 14:26
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dcim', '0068_rack_new_fields'),
]
operations = [
migrations.AlterField(
model_name='device',
name='asset_tag',
field=models.CharField(blank=True, max_length=50, null=True, unique=True),
),
migrations.AlterField(
model_name='device',
name='name',
field=models.CharField(blank=True, max_length=64, null=True, unique=True),
),
migrations.AlterField(
model_name='inventoryitem',
name='asset_tag',
field=models.CharField(blank=True, max_length=50, null=True, unique=True),
),
migrations.AlterField(
model_name='rack',
name='asset_tag',
field=models.CharField(blank=True, max_length=50, null=True, unique=True),
),
migrations.AlterField(
model_name='rack',
name='facility_id',
field=models.CharField(blank=True, max_length=50, null=True),
),
]

View File

@@ -1,85 +0,0 @@
# Generated by Django 2.1.4 on 2019-02-20 06:56
from django.db import migrations
import taggit.managers
class Migration(migrations.Migration):
dependencies = [
('dcim', '0069_deprecate_nullablecharfield'),
('extras', '0019_tag_taggeditem'),
]
operations = [
migrations.AlterField(
model_name='consoleport',
name='tags',
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
),
migrations.AlterField(
model_name='consoleserverport',
name='tags',
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
),
migrations.AlterField(
model_name='device',
name='tags',
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
),
migrations.AlterField(
model_name='devicebay',
name='tags',
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
),
migrations.AlterField(
model_name='devicetype',
name='tags',
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
),
migrations.AlterField(
model_name='frontport',
name='tags',
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
),
migrations.AlterField(
model_name='interface',
name='tags',
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
),
migrations.AlterField(
model_name='inventoryitem',
name='tags',
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
),
migrations.AlterField(
model_name='poweroutlet',
name='tags',
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
),
migrations.AlterField(
model_name='powerport',
name='tags',
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
),
migrations.AlterField(
model_name='rack',
name='tags',
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
),
migrations.AlterField(
model_name='rearport',
name='tags',
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
),
migrations.AlterField(
model_name='site',
name='tags',
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
),
migrations.AlterField(
model_name='virtualchassis',
name='tags',
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
),
]

View File

@@ -1,38 +0,0 @@
# Generated by Django 2.1.7 on 2019-02-20 18:50
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dcim', '0070_custom_tag_models'),
]
operations = [
migrations.AddField(
model_name='consoleport',
name='description',
field=models.CharField(blank=True, max_length=100),
),
migrations.AddField(
model_name='consoleserverport',
name='description',
field=models.CharField(blank=True, max_length=100),
),
migrations.AddField(
model_name='devicebay',
name='description',
field=models.CharField(blank=True, max_length=100),
),
migrations.AddField(
model_name='poweroutlet',
name='description',
field=models.CharField(blank=True, max_length=100),
),
migrations.AddField(
model_name='powerport',
name='description',
field=models.CharField(blank=True, max_length=100),
),
]

View File

@@ -1,133 +0,0 @@
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
import taggit.managers
class Migration(migrations.Migration):
dependencies = [
('extras', '0021_add_color_comments_changelog_to_tag'),
('dcim', '0071_device_components_add_description'),
]
operations = [
migrations.CreateModel(
name='PowerFeed',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
('created', models.DateField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)),
('name', models.CharField(max_length=50)),
('status', models.PositiveSmallIntegerField(default=1)),
('type', models.PositiveSmallIntegerField(default=1)),
('supply', models.PositiveSmallIntegerField(default=1)),
('phase', models.PositiveSmallIntegerField(default=1)),
('voltage', models.PositiveSmallIntegerField(default=120, validators=[django.core.validators.MinValueValidator(1)])),
('amperage', models.PositiveSmallIntegerField(default=20, validators=[django.core.validators.MinValueValidator(1)])),
('power_factor', models.PositiveSmallIntegerField(default=80, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)])),
('comments', models.TextField(blank=True)),
('cable', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.Cable')),
],
options={
'ordering': ['power_panel', 'name'],
},
),
migrations.CreateModel(
name='PowerPanel',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
('created', models.DateField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)),
('name', models.CharField(max_length=50)),
('rack_group', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='dcim.RackGroup')),
('site', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='dcim.Site')),
],
options={
'ordering': ['site', 'name'],
},
),
migrations.AddField(
model_name='powerfeed',
name='power_panel',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='powerfeeds', to='dcim.PowerPanel'),
),
migrations.AddField(
model_name='powerfeed',
name='rack',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='dcim.Rack'),
),
migrations.AddField(
model_name='powerfeed',
name='tags',
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
),
migrations.AddField(
model_name='powerfeed',
name='connected_endpoint',
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.PowerPort'),
),
migrations.AddField(
model_name='powerfeed',
name='connection_status',
field=models.NullBooleanField(),
),
migrations.RenameField(
model_name='powerport',
old_name='connected_endpoint',
new_name='_connected_poweroutlet',
),
migrations.AddField(
model_name='powerport',
name='_connected_powerfeed',
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.PowerFeed'),
),
migrations.AddField(
model_name='powerport',
name='allocated_draw',
field=models.PositiveSmallIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(1)]),
),
migrations.AddField(
model_name='powerport',
name='maximum_draw',
field=models.PositiveSmallIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(1)]),
),
migrations.AddField(
model_name='powerporttemplate',
name='allocated_draw',
field=models.PositiveSmallIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(1)]),
),
migrations.AddField(
model_name='powerporttemplate',
name='maximum_draw',
field=models.PositiveSmallIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(1)]),
),
migrations.AlterUniqueTogether(
name='powerpanel',
unique_together={('site', 'name')},
),
migrations.AlterUniqueTogether(
name='powerfeed',
unique_together={('power_panel', 'name')},
),
migrations.AddField(
model_name='poweroutlet',
name='feed_leg',
field=models.PositiveSmallIntegerField(blank=True, null=True),
),
migrations.AddField(
model_name='poweroutlet',
name='power_port',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='poweroutlets', to='dcim.PowerPort'),
),
migrations.AddField(
model_name='poweroutlettemplate',
name='feed_leg',
field=models.PositiveSmallIntegerField(blank=True, null=True),
),
migrations.AddField(
model_name='poweroutlettemplate',
name='power_port',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='poweroutlet_templates', to='dcim.PowerPortTemplate'),
),
]

View File

@@ -1,23 +0,0 @@
# Generated by Django 2.1.7 on 2019-04-12 17:27
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('dcim', '0072_powerfeeds'),
]
operations = [
migrations.RenameField(
model_name='interface',
old_name='form_factor',
new_name='type',
),
migrations.RenameField(
model_name='interfacetemplate',
old_name='form_factor',
new_name='type',
),
]

File diff suppressed because it is too large Load Diff

View File

@@ -6,9 +6,8 @@ from utilities.tables import BaseTable, BooleanColumn, ColorColumn, ToggleColumn
from .models import (
Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate,
InventoryItem, Manufacturer, Platform, PowerFeed, PowerOutlet, PowerOutletTemplate, PowerPanel, PowerPort,
PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site,
VirtualChassis,
InventoryItem, Manufacturer, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack,
RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site, VirtualChassis,
)
REGION_LINK = """
@@ -30,8 +29,7 @@ SITE_REGION_LINK = """
"""
COLOR_LABEL = """
{% load helpers %}
<label class="label" style="color: {{ record.color|fgcolor }}; background-color: #{{ record.color }}">{{ record }}</label>
<label class="label" style="background-color: #{{ record.color }}">{{ record }}</label>
"""
DEVICE_LINK = """
@@ -45,7 +43,7 @@ REGION_ACTIONS = """
<i class="fa fa-history"></i>
</a>
{% if perms.dcim.change_region %}
<a href="{% url 'dcim:region_edit' pk=record.pk %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
<a href="{% url 'dcim:region_edit' pk=record.pk %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
{% endif %}
"""
@@ -57,7 +55,7 @@ RACKGROUP_ACTIONS = """
<i class="fa fa-eye"></i>
</a>
{% if perms.dcim.change_rackgroup %}
<a href="{% url 'dcim:rackgroup_edit' pk=record.pk %}?return_url={{ request.path }}" class="btn btn-xs btn-warning" title="Edit">
<a href="{% url 'dcim:rackgroup_edit' pk=record.pk %}" class="btn btn-xs btn-warning" title="Edit">
<i class="glyphicon glyphicon-pencil"></i>
</a>
{% endif %}
@@ -68,7 +66,7 @@ RACKROLE_ACTIONS = """
<i class="fa fa-history"></i>
</a>
{% if perms.dcim.change_rackrole %}
<a href="{% url 'dcim:rackrole_edit' pk=record.pk %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
<a href="{% url 'dcim:rackrole_edit' pk=record.pk %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
{% endif %}
"""
@@ -89,7 +87,7 @@ RACKRESERVATION_ACTIONS = """
<i class="fa fa-history"></i>
</a>
{% if perms.dcim.change_rackreservation %}
<a href="{% url 'dcim:rackreservation_edit' pk=record.pk %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
<a href="{% url 'dcim:rackreservation_edit' pk=record.pk %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
{% endif %}
"""
@@ -98,7 +96,7 @@ MANUFACTURER_ACTIONS = """
<i class="fa fa-history"></i>
</a>
{% if perms.dcim.change_manufacturer %}
<a href="{% url 'dcim:manufacturer_edit' slug=record.slug %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
<a href="{% url 'dcim:manufacturer_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
{% endif %}
"""
@@ -107,7 +105,7 @@ DEVICEROLE_ACTIONS = """
<i class="fa fa-history"></i>
</a>
{% if perms.dcim.change_devicerole %}
<a href="{% url 'dcim:devicerole_edit' slug=record.slug %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
<a href="{% url 'dcim:devicerole_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
{% endif %}
"""
@@ -132,23 +130,18 @@ PLATFORM_ACTIONS = """
<i class="fa fa-history"></i>
</a>
{% if perms.dcim.change_platform %}
<a href="{% url 'dcim:platform_edit' slug=record.slug %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
<a href="{% url 'dcim:platform_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
{% endif %}
"""
DEVICE_ROLE = """
{% load helpers %}
<label class="label" style="color: {{ record.device_role.color|fgcolor }}; background-color: #{{ record.device_role.color }}">{{ value }}</label>
<label class="label" style="background-color: #{{ record.device_role.color }}">{{ value }}</label>
"""
STATUS_LABEL = """
<span class="label label-{{ record.get_status_class }}">{{ record.get_status_display }}</span>
"""
TYPE_LABEL = """
<span class="label label-{{ record.get_type_class }}">{{ record.get_type_display }}</span>
"""
DEVICE_PRIMARY_IP = """
{{ record.primary_ip6.address.ip|default:"" }}
{% if record.primary_ip6 and record.primary_ip4 %}<br />{% endif %}
@@ -173,7 +166,7 @@ VIRTUALCHASSIS_ACTIONS = """
<i class="fa fa-history"></i>
</a>
{% if perms.dcim.change_virtualchassis %}
<a href="{% url 'dcim:virtualchassis_edit' pk=record.pk %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
<a href="{% url 'dcim:virtualchassis_edit' pk=record.pk %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
{% endif %}
"""
@@ -189,10 +182,6 @@ CABLE_LENGTH = """
{% if record.length %}{{ record.length }} {{ record.get_length_unit_display }}{% else %}&mdash;{% endif %}
"""
POWERPANEL_POWERFEED_COUNT = """
<a href="{% url 'dcim:powerfeed_list' %}?power_panel_id={{ record.pk }}">{{ value }}</a>
"""
#
# Regions
@@ -205,7 +194,7 @@ class RegionTable(BaseTable):
slug = tables.Column(verbose_name='Slug')
actions = tables.TemplateColumn(
template_code=REGION_ACTIONS,
attrs={'td': {'class': 'text-right noprint'}},
attrs={'td': {'class': 'text-right'}},
verbose_name=''
)
@@ -248,7 +237,7 @@ class RackGroupTable(BaseTable):
slug = tables.Column()
actions = tables.TemplateColumn(
template_code=RACKGROUP_ACTIONS,
attrs={'td': {'class': 'text-right noprint'}},
attrs={'td': {'class': 'text-right'}},
verbose_name=''
)
@@ -267,7 +256,7 @@ class RackRoleTable(BaseTable):
rack_count = tables.Column(verbose_name='Racks')
color = tables.TemplateColumn(COLOR_LABEL, verbose_name='Color')
slug = tables.Column(verbose_name='Slug')
actions = tables.TemplateColumn(template_code=RACKROLE_ACTIONS, attrs={'td': {'class': 'text-right noprint'}},
actions = tables.TemplateColumn(template_code=RACKROLE_ACTIONS, attrs={'td': {'class': 'text-right'}},
verbose_name='')
class Meta(BaseTable.Meta):
@@ -318,7 +307,7 @@ class RackReservationTable(BaseTable):
rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')])
unit_list = tables.Column(orderable=False, verbose_name='Units')
actions = tables.TemplateColumn(
template_code=RACKRESERVATION_ACTIONS, attrs={'td': {'class': 'text-right noprint'}}, verbose_name=''
template_code=RACKRESERVATION_ACTIONS, attrs={'td': {'class': 'text-right'}}, verbose_name=''
)
class Meta(BaseTable.Meta):
@@ -336,7 +325,7 @@ class ManufacturerTable(BaseTable):
devicetype_count = tables.Column(verbose_name='Device Types')
platform_count = tables.Column(verbose_name='Platforms')
slug = tables.Column(verbose_name='Slug')
actions = tables.TemplateColumn(template_code=MANUFACTURER_ACTIONS, attrs={'td': {'class': 'text-right noprint'}},
actions = tables.TemplateColumn(template_code=MANUFACTURER_ACTIONS, attrs={'td': {'class': 'text-right'}},
verbose_name='')
class Meta(BaseTable.Meta):
@@ -419,7 +408,7 @@ class InterfaceTemplateTable(BaseTable):
class Meta(BaseTable.Meta):
model = InterfaceTemplate
fields = ('pk', 'name', 'mgmt_only', 'type')
fields = ('pk', 'name', 'mgmt_only', 'form_factor')
empty_text = "None"
@@ -472,7 +461,7 @@ class DeviceRoleTable(BaseTable):
slug = tables.Column(verbose_name='Slug')
actions = tables.TemplateColumn(
template_code=DEVICEROLE_ACTIONS,
attrs={'td': {'class': 'text-right noprint'}},
attrs={'td': {'class': 'text-right'}},
verbose_name=''
)
@@ -501,7 +490,7 @@ class PlatformTable(BaseTable):
)
actions = tables.TemplateColumn(
template_code=PLATFORM_ACTIONS,
attrs={'td': {'class': 'text-right noprint'}},
attrs={'td': {'class': 'text-right'}},
verbose_name=''
)
@@ -527,7 +516,7 @@ class DeviceTable(BaseTable):
device_role = tables.TemplateColumn(DEVICE_ROLE, verbose_name='Role')
device_type = tables.LinkColumn(
'dcim:devicetype', args=[Accessor('device_type.pk')], verbose_name='Type',
text=lambda record: record.device_type.display_name
text=lambda record: record.device_type.full_name
)
class Meta(BaseTable.Meta):
@@ -597,7 +586,7 @@ class InterfaceTable(BaseTable):
class Meta(BaseTable.Meta):
model = Interface
fields = ('name', 'type', 'lag', 'enabled', 'mgmt_only', 'description')
fields = ('name', 'form_factor', 'lag', 'enabled', 'mgmt_only', 'description')
class FrontPortTable(BaseTable):
@@ -656,9 +645,6 @@ class CableTable(BaseTable):
orderable=False,
verbose_name=''
)
status = tables.TemplateColumn(
template_code=STATUS_LABEL
)
length = tables.TemplateColumn(
template_code=CABLE_LENGTH,
order_by='_abs_length'
@@ -707,8 +693,7 @@ class PowerConnectionTable(BaseTable):
args=[Accessor('connected_endpoint.device.pk')],
verbose_name='PDU'
)
outlet = tables.Column(
accessor=Accessor('_connected_poweroutlet'),
connected_endpoint = tables.Column(
verbose_name='Outlet'
)
device = tables.LinkColumn(
@@ -721,7 +706,7 @@ class PowerConnectionTable(BaseTable):
class Meta(BaseTable.Meta):
model = PowerPort
fields = ('pdu', 'outlet', 'device', 'name', 'connection_status')
fields = ('pdu', 'connected_endpoint', 'device', 'name', 'connection_status')
class InterfaceConnectionTable(BaseTable):
@@ -743,18 +728,18 @@ class InterfaceConnectionTable(BaseTable):
)
device_b = tables.LinkColumn(
viewname='dcim:device',
accessor=Accessor('_connected_interface.device'),
args=[Accessor('_connected_interface.device.pk')],
accessor=Accessor('connected_endpoint.device'),
args=[Accessor('connected_endpoint.device.pk')],
verbose_name='Device B'
)
interface_b = tables.LinkColumn(
viewname='dcim:interface',
accessor=Accessor('_connected_interface'),
args=[Accessor('_connected_interface.pk')],
accessor=Accessor('connected_endpoint.name'),
args=[Accessor('connected_endpoint.pk')],
verbose_name='Interface B'
)
description_b = tables.Column(
accessor=Accessor('_connected_interface.description'),
accessor=Accessor('connected_endpoint.description'),
verbose_name='Description'
)
@@ -789,58 +774,10 @@ class VirtualChassisTable(BaseTable):
member_count = tables.Column(verbose_name='Members')
actions = tables.TemplateColumn(
template_code=VIRTUALCHASSIS_ACTIONS,
attrs={'td': {'class': 'text-right noprint'}},
attrs={'td': {'class': 'text-right'}},
verbose_name=''
)
class Meta(BaseTable.Meta):
model = VirtualChassis
fields = ('pk', 'master', 'domain', 'member_count', 'actions')
#
# Power panels
#
class PowerPanelTable(BaseTable):
pk = ToggleColumn()
name = tables.LinkColumn()
site = tables.LinkColumn(
viewname='dcim:site',
args=[Accessor('site.slug')]
)
powerfeed_count = tables.TemplateColumn(
template_code=POWERPANEL_POWERFEED_COUNT,
verbose_name='Feeds'
)
class Meta(BaseTable.Meta):
model = PowerPanel
fields = ('pk', 'name', 'site', 'rack_group', 'powerfeed_count')
#
# Power feeds
#
class PowerFeedTable(BaseTable):
pk = ToggleColumn()
name = tables.LinkColumn()
power_panel = tables.LinkColumn(
viewname='dcim:powerpanel',
args=[Accessor('power_panel.pk')],
)
rack = tables.LinkColumn(
viewname='dcim:rack',
args=[Accessor('rack.pk')]
)
status = tables.TemplateColumn(
template_code=STATUS_LABEL
)
type = tables.TemplateColumn(
template_code=TYPE_LABEL
)
class Meta(BaseTable.Meta):
model = PowerFeed
fields = ('pk', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase')

View File

@@ -7,8 +7,8 @@ from dcim.constants import *
from dcim.models import (
Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, Interface, InterfaceTemplate, Manufacturer,
InventoryItem, Platform, PowerFeed, PowerPort, PowerPortTemplate, PowerOutlet, PowerOutletTemplate, PowerPanel,
Rack, RackGroup, RackReservation, RackRole, RearPort, Region, Site, VirtualChassis,
InventoryItem, Platform, PowerPort, PowerPortTemplate, PowerOutlet, PowerOutletTemplate, Rack, RackGroup,
RackReservation, RackRole, RearPort, Region, Site, VirtualChassis,
)
from ipam.models import IPAddress, VLAN
from extras.models import Graph, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE
@@ -47,7 +47,7 @@ class RegionTest(APITestCase):
self.assertEqual(
sorted(response.data['results'][0]),
['id', 'name', 'site_count', 'slug', 'url']
['id', 'name', 'slug', 'url']
)
def test_create_region(self):
@@ -285,7 +285,7 @@ class RackGroupTest(APITestCase):
self.assertEqual(
sorted(response.data['results'][0]),
['id', 'name', 'rack_count', 'slug', 'url']
['id', 'name', 'slug', 'url']
)
def test_create_rackgroup(self):
@@ -393,7 +393,7 @@ class RackRoleTest(APITestCase):
self.assertEqual(
sorted(response.data['results'][0]),
['id', 'name', 'rack_count', 'slug', 'url']
['id', 'name', 'slug', 'url']
)
def test_create_rackrole(self):
@@ -520,7 +520,7 @@ class RackTest(APITestCase):
self.assertEqual(
sorted(response.data['results'][0]),
['device_count', 'display_name', 'id', 'name', 'url']
['display_name', 'id', 'name', 'url']
)
def test_create_rack(self):
@@ -746,7 +746,7 @@ class ManufacturerTest(APITestCase):
self.assertEqual(
sorted(response.data['results'][0]),
['devicetype_count', 'id', 'name', 'slug', 'url']
['id', 'name', 'slug', 'url']
)
def test_create_manufacturer(self):
@@ -855,7 +855,7 @@ class DeviceTypeTest(APITestCase):
self.assertEqual(
sorted(response.data['results'][0]),
['device_count', 'display_name', 'id', 'manufacturer', 'model', 'slug', 'url']
['id', 'manufacturer', 'model', 'slug', 'url']
)
def test_create_devicetype(self):
@@ -1569,7 +1569,7 @@ class DeviceRoleTest(APITestCase):
self.assertEqual(
sorted(response.data['results'][0]),
['device_count', 'id', 'name', 'slug', 'url', 'virtualmachine_count']
['id', 'name', 'slug', 'url']
)
def test_create_devicerole(self):
@@ -1677,7 +1677,7 @@ class PlatformTest(APITestCase):
self.assertEqual(
sorted(response.data['results'][0]),
['device_count', 'id', 'name', 'slug', 'url', 'virtualmachine_count']
['id', 'name', 'slug', 'url']
)
def test_create_platform(self):
@@ -1791,16 +1791,6 @@ class DeviceTest(APITestCase):
site=self.site1,
cluster=self.cluster1
)
self.device_with_context_data = Device.objects.create(
device_type=self.devicetype1,
device_role=self.devicerole1,
name='Device with context data',
site=self.site1,
local_context_data={
'A': 1,
'B': 2
}
)
def test_get_device(self):
@@ -1816,7 +1806,7 @@ class DeviceTest(APITestCase):
url = reverse('dcim-api:device-list')
response = self.client.get(url, **self.header)
self.assertEqual(response.data['count'], 4)
self.assertEqual(response.data['count'], 3)
def test_list_devices_brief(self):
@@ -1842,7 +1832,7 @@ class DeviceTest(APITestCase):
response = self.client.post(url, data, format='json', **self.header)
self.assertHttpStatus(response, status.HTTP_201_CREATED)
self.assertEqual(Device.objects.count(), 5)
self.assertEqual(Device.objects.count(), 4)
device4 = Device.objects.get(pk=response.data['id'])
self.assertEqual(device4.device_type_id, data['device_type'])
self.assertEqual(device4.device_role_id, data['device_role'])
@@ -1877,7 +1867,7 @@ class DeviceTest(APITestCase):
response = self.client.post(url, data, format='json', **self.header)
self.assertHttpStatus(response, status.HTTP_201_CREATED)
self.assertEqual(Device.objects.count(), 7)
self.assertEqual(Device.objects.count(), 6)
self.assertEqual(response.data[0]['name'], data[0]['name'])
self.assertEqual(response.data[1]['name'], data[1]['name'])
self.assertEqual(response.data[2]['name'], data[2]['name'])
@@ -1901,7 +1891,7 @@ class DeviceTest(APITestCase):
response = self.client.put(url, data, format='json', **self.header)
self.assertHttpStatus(response, status.HTTP_200_OK)
self.assertEqual(Device.objects.count(), 4)
self.assertEqual(Device.objects.count(), 3)
device1 = Device.objects.get(pk=response.data['id'])
self.assertEqual(device1.device_type_id, data['device_type'])
self.assertEqual(device1.device_role_id, data['device_role'])
@@ -1916,21 +1906,7 @@ class DeviceTest(APITestCase):
response = self.client.delete(url, **self.header)
self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
self.assertEqual(Device.objects.count(), 3)
def test_config_context_included_by_default_in_list_view(self):
url = reverse('dcim-api:device-list') + '?slug=device-with-context-data'
response = self.client.get(url, **self.header)
self.assertEqual(response.data['results'][0].get('config_context', {}).get('A'), 1)
def test_config_context_excluded(self):
url = reverse('dcim-api:device-list') + '?exclude=config_context'
response = self.client.get(url, **self.header)
self.assertFalse('config_context' in response.data['results'][0])
self.assertEqual(Device.objects.count(), 2)
class ConsolePortTest(APITestCase):
@@ -2553,7 +2529,7 @@ class InterfaceTest(APITestCase):
def test_update_interface(self):
lag_interface = Interface.objects.create(
device=self.device, name='Test LAG Interface', type=IFACE_TYPE_LAG
device=self.device, name='Test LAG Interface', form_factor=IFACE_FF_LAG
)
data = {
@@ -2841,7 +2817,7 @@ class CableTest(APITestCase):
)
for device in [self.device1, self.device2]:
for i in range(0, 10):
Interface(device=device, type=IFACE_TYPE_1GE_FIXED, name='eth{}'.format(i)).save()
Interface(device=device, form_factor=IFACE_FF_1GE_FIXED, name='eth{}'.format(i)).save()
self.cable1 = Cable(
termination_a=self.device1.interfaces.get(name='eth0'),
@@ -3410,23 +3386,23 @@ class VirtualChassisTest(APITestCase):
device_type=device_type, device_role=device_role, name='StackSwitch9', site=site
)
for i in range(0, 13):
Interface.objects.create(device=self.device1, name='1/{}'.format(i), type=IFACE_TYPE_1GE_FIXED)
Interface.objects.create(device=self.device1, name='1/{}'.format(i), form_factor=IFACE_FF_1GE_FIXED)
for i in range(0, 13):
Interface.objects.create(device=self.device2, name='2/{}'.format(i), type=IFACE_TYPE_1GE_FIXED)
Interface.objects.create(device=self.device2, name='2/{}'.format(i), form_factor=IFACE_FF_1GE_FIXED)
for i in range(0, 13):
Interface.objects.create(device=self.device3, name='3/{}'.format(i), type=IFACE_TYPE_1GE_FIXED)
Interface.objects.create(device=self.device3, name='3/{}'.format(i), form_factor=IFACE_FF_1GE_FIXED)
for i in range(0, 13):
Interface.objects.create(device=self.device4, name='1/{}'.format(i), type=IFACE_TYPE_1GE_FIXED)
Interface.objects.create(device=self.device4, name='1/{}'.format(i), form_factor=IFACE_FF_1GE_FIXED)
for i in range(0, 13):
Interface.objects.create(device=self.device5, name='2/{}'.format(i), type=IFACE_TYPE_1GE_FIXED)
Interface.objects.create(device=self.device5, name='2/{}'.format(i), form_factor=IFACE_FF_1GE_FIXED)
for i in range(0, 13):
Interface.objects.create(device=self.device6, name='3/{}'.format(i), type=IFACE_TYPE_1GE_FIXED)
Interface.objects.create(device=self.device6, name='3/{}'.format(i), form_factor=IFACE_FF_1GE_FIXED)
for i in range(0, 13):
Interface.objects.create(device=self.device7, name='1/{}'.format(i), type=IFACE_TYPE_1GE_FIXED)
Interface.objects.create(device=self.device7, name='1/{}'.format(i), form_factor=IFACE_FF_1GE_FIXED)
for i in range(0, 13):
Interface.objects.create(device=self.device8, name='2/{}'.format(i), type=IFACE_TYPE_1GE_FIXED)
Interface.objects.create(device=self.device8, name='2/{}'.format(i), form_factor=IFACE_FF_1GE_FIXED)
for i in range(0, 13):
Interface.objects.create(device=self.device9, name='3/{}'.format(i), type=IFACE_TYPE_1GE_FIXED)
Interface.objects.create(device=self.device9, name='3/{}'.format(i), form_factor=IFACE_FF_1GE_FIXED)
# Create two VirtualChassis with three members each
self.vc1 = VirtualChassis.objects.create(master=self.device1, domain='test-domain-1')
@@ -3457,7 +3433,7 @@ class VirtualChassisTest(APITestCase):
self.assertEqual(
sorted(response.data['results'][0]),
['id', 'master', 'member_count', 'url']
['id', 'master', 'url']
)
def test_create_virtualchassis(self):
@@ -3532,260 +3508,3 @@ class VirtualChassisTest(APITestCase):
self.assertTrue(
Device.objects.filter(pk=d.pk, virtual_chassis=None, vc_position=None, vc_priority=None)
)
class PowerPanelTest(APITestCase):
def setUp(self):
super().setUp()
self.site1 = Site.objects.create(name='Test Site 1', slug='test-site-1')
self.rackgroup1 = RackGroup.objects.create(site=self.site1, name='Test Rack Group 1', slug='test-rack-group-1')
self.rackgroup2 = RackGroup.objects.create(site=self.site1, name='Test Rack Group 2', slug='test-rack-group-2')
self.rackgroup3 = RackGroup.objects.create(site=self.site1, name='Test Rack Group 3', slug='test-rack-group-3')
self.powerpanel1 = PowerPanel.objects.create(
site=self.site1, rack_group=self.rackgroup1, name='Test Power Panel 1'
)
self.powerpanel2 = PowerPanel.objects.create(
site=self.site1, rack_group=self.rackgroup2, name='Test Power Panel 2'
)
self.powerpanel3 = PowerPanel.objects.create(
site=self.site1, rack_group=self.rackgroup3, name='Test Power Panel 3'
)
def test_get_powerpanel(self):
url = reverse('dcim-api:powerpanel-detail', kwargs={'pk': self.powerpanel1.pk})
response = self.client.get(url, **self.header)
self.assertEqual(response.data['name'], self.powerpanel1.name)
def test_list_powerpanels(self):
url = reverse('dcim-api:powerpanel-list')
response = self.client.get(url, **self.header)
self.assertEqual(response.data['count'], 3)
def test_list_powerpanels_brief(self):
url = reverse('dcim-api:powerpanel-list')
response = self.client.get('{}?brief=1'.format(url), **self.header)
self.assertEqual(
sorted(response.data['results'][0]),
['id', 'name', 'powerfeed_count', 'url']
)
def test_create_powerpanel(self):
data = {
'name': 'Test Power Panel 4',
'site': self.site1.pk,
'rack_group': self.rackgroup1.pk,
}
url = reverse('dcim-api:powerpanel-list')
response = self.client.post(url, data, format='json', **self.header)
self.assertHttpStatus(response, status.HTTP_201_CREATED)
self.assertEqual(PowerPanel.objects.count(), 4)
powerpanel4 = PowerPanel.objects.get(pk=response.data['id'])
self.assertEqual(powerpanel4.name, data['name'])
self.assertEqual(powerpanel4.site_id, data['site'])
self.assertEqual(powerpanel4.rack_group_id, data['rack_group'])
def test_create_powerpanel_bulk(self):
data = [
{
'name': 'Test Power Panel 4',
'site': self.site1.pk,
'rack_group': self.rackgroup1.pk,
},
{
'name': 'Test Power Panel 5',
'site': self.site1.pk,
'rack_group': self.rackgroup2.pk,
},
{
'name': 'Test Power Panel 6',
'site': self.site1.pk,
'rack_group': self.rackgroup3.pk,
},
]
url = reverse('dcim-api:powerpanel-list')
response = self.client.post(url, data, format='json', **self.header)
self.assertHttpStatus(response, status.HTTP_201_CREATED)
self.assertEqual(PowerPanel.objects.count(), 6)
self.assertEqual(response.data[0]['name'], data[0]['name'])
self.assertEqual(response.data[1]['name'], data[1]['name'])
self.assertEqual(response.data[2]['name'], data[2]['name'])
def test_update_powerpanel(self):
data = {
'name': 'Test Power Panel X',
'rack_group': self.rackgroup2.pk,
}
url = reverse('dcim-api:powerpanel-detail', kwargs={'pk': self.powerpanel1.pk})
response = self.client.patch(url, data, format='json', **self.header)
self.assertHttpStatus(response, status.HTTP_200_OK)
self.assertEqual(PowerPanel.objects.count(), 3)
powerpanel1 = PowerPanel.objects.get(pk=response.data['id'])
self.assertEqual(powerpanel1.name, data['name'])
self.assertEqual(powerpanel1.rack_group_id, data['rack_group'])
def test_delete_powerpanel(self):
url = reverse('dcim-api:powerpanel-detail', kwargs={'pk': self.powerpanel1.pk})
response = self.client.delete(url, **self.header)
self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
self.assertEqual(PowerPanel.objects.count(), 2)
class PowerFeedTest(APITestCase):
def setUp(self):
super().setUp()
self.site1 = Site.objects.create(name='Test Site 1', slug='test-site-1')
self.rackgroup1 = RackGroup.objects.create(site=self.site1, name='Test Rack Group 1', slug='test-rack-group-1')
self.rackrole1 = RackRole.objects.create(name='Test Rack Role 1', slug='test-rack-role-1', color='ff0000')
self.rack1 = Rack.objects.create(
site=self.site1, group=self.rackgroup1, role=self.rackrole1, name='Test Rack 1', u_height=42,
)
self.rack2 = Rack.objects.create(
site=self.site1, group=self.rackgroup1, role=self.rackrole1, name='Test Rack 2', u_height=42,
)
self.rack3 = Rack.objects.create(
site=self.site1, group=self.rackgroup1, role=self.rackrole1, name='Test Rack 3', u_height=42,
)
self.rack4 = Rack.objects.create(
site=self.site1, group=self.rackgroup1, role=self.rackrole1, name='Test Rack 4', u_height=42,
)
self.powerpanel1 = PowerPanel.objects.create(
site=self.site1, rack_group=self.rackgroup1, name='Test Power Panel 1'
)
self.powerpanel2 = PowerPanel.objects.create(
site=self.site1, rack_group=self.rackgroup1, name='Test Power Panel 2'
)
self.powerfeed1 = PowerFeed.objects.create(
power_panel=self.powerpanel1, rack=self.rack1, name='Test Power Feed 1A', type=POWERFEED_TYPE_PRIMARY
)
self.powerfeed2 = PowerFeed.objects.create(
power_panel=self.powerpanel2, rack=self.rack1, name='Test Power Feed 1B', type=POWERFEED_TYPE_REDUNDANT
)
self.powerfeed3 = PowerFeed.objects.create(
power_panel=self.powerpanel1, rack=self.rack2, name='Test Power Feed 2A', type=POWERFEED_TYPE_PRIMARY
)
self.powerfeed4 = PowerFeed.objects.create(
power_panel=self.powerpanel2, rack=self.rack2, name='Test Power Feed 2B', type=POWERFEED_TYPE_REDUNDANT
)
self.powerfeed5 = PowerFeed.objects.create(
power_panel=self.powerpanel1, rack=self.rack3, name='Test Power Feed 3A', type=POWERFEED_TYPE_PRIMARY
)
self.powerfeed6 = PowerFeed.objects.create(
power_panel=self.powerpanel2, rack=self.rack3, name='Test Power Feed 3B', type=POWERFEED_TYPE_REDUNDANT
)
def test_get_powerfeed(self):
url = reverse('dcim-api:powerfeed-detail', kwargs={'pk': self.powerfeed1.pk})
response = self.client.get(url, **self.header)
self.assertEqual(response.data['name'], self.powerfeed1.name)
def test_list_powerfeeds(self):
url = reverse('dcim-api:powerfeed-list')
response = self.client.get(url, **self.header)
self.assertEqual(response.data['count'], 6)
def test_list_powerfeeds_brief(self):
url = reverse('dcim-api:powerfeed-list')
response = self.client.get('{}?brief=1'.format(url), **self.header)
self.assertEqual(
sorted(response.data['results'][0]),
['id', 'name', 'url']
)
def test_create_powerfeed(self):
data = {
'name': 'Test Power Feed 4A',
'power_panel': self.powerpanel1.pk,
'rack': self.rack4.pk,
'type': POWERFEED_TYPE_PRIMARY,
}
url = reverse('dcim-api:powerfeed-list')
response = self.client.post(url, data, format='json', **self.header)
self.assertHttpStatus(response, status.HTTP_201_CREATED)
self.assertEqual(PowerFeed.objects.count(), 7)
powerfeed4 = PowerFeed.objects.get(pk=response.data['id'])
self.assertEqual(powerfeed4.name, data['name'])
self.assertEqual(powerfeed4.power_panel_id, data['power_panel'])
self.assertEqual(powerfeed4.rack_id, data['rack'])
def test_create_powerfeed_bulk(self):
data = [
{
'name': 'Test Power Feed 4A',
'power_panel': self.powerpanel1.pk,
'rack': self.rack4.pk,
'type': POWERFEED_TYPE_PRIMARY,
},
{
'name': 'Test Power Feed 4B',
'power_panel': self.powerpanel1.pk,
'rack': self.rack4.pk,
'type': POWERFEED_TYPE_REDUNDANT,
},
]
url = reverse('dcim-api:powerfeed-list')
response = self.client.post(url, data, format='json', **self.header)
self.assertHttpStatus(response, status.HTTP_201_CREATED)
self.assertEqual(PowerFeed.objects.count(), 8)
self.assertEqual(response.data[0]['name'], data[0]['name'])
self.assertEqual(response.data[1]['name'], data[1]['name'])
def test_update_powerfeed(self):
data = {
'name': 'Test Power Feed X',
'rack': self.rack4.pk,
'type': POWERFEED_TYPE_REDUNDANT,
}
url = reverse('dcim-api:powerfeed-detail', kwargs={'pk': self.powerfeed1.pk})
response = self.client.patch(url, data, format='json', **self.header)
self.assertHttpStatus(response, status.HTTP_200_OK)
self.assertEqual(PowerFeed.objects.count(), 6)
powerfeed1 = PowerFeed.objects.get(pk=response.data['id'])
self.assertEqual(powerfeed1.name, data['name'])
self.assertEqual(powerfeed1.rack_id, data['rack'])
self.assertEqual(powerfeed1.type, data['type'])
def test_delete_powerfeed(self):
url = reverse('dcim-api:powerfeed-detail', kwargs={'pk': self.powerfeed1.pk})
response = self.client.delete(url, **self.header)
self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
self.assertEqual(PowerFeed.objects.count(), 5)

View File

@@ -249,7 +249,7 @@ class CableTestCase(TestCase):
"""
A cable connection cannot include a virtual interface
"""
virtual_interface = Interface(device=self.device1, name="V1", type=0)
virtual_interface = Interface(device=self.device1, name="V1", form_factor=0)
cable = Cable(termination_a=self.interface2, termination_b=virtual_interface)
with self.assertRaises(ValidationError):
cable.clean()

View File

@@ -1,456 +0,0 @@
import urllib.parse
from django.test import Client, TestCase
from django.urls import reverse
from dcim.constants import CABLE_TYPE_CAT6, IFACE_TYPE_1GE_FIXED
from dcim.models import (
Cable, Device, DeviceRole, DeviceType, Interface, InventoryItem, Manufacturer, Platform, Rack, RackGroup,
RackReservation, RackRole, Site, Region, VirtualChassis,
)
from utilities.testing import create_test_user
class RegionTestCase(TestCase):
def setUp(self):
user = create_test_user(permissions=['dcim.view_region'])
self.client = Client()
self.client.force_login(user)
# Create three Regions
for i in range(1, 4):
Region(name='Region {}'.format(i), slug='region-{}'.format(i)).save()
def test_region_list(self):
url = reverse('dcim:region_list')
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
class SiteTestCase(TestCase):
def setUp(self):
user = create_test_user(permissions=['dcim.view_site'])
self.client = Client()
self.client.force_login(user)
region = Region(name='Region 1', slug='region-1')
region.save()
Site.objects.bulk_create([
Site(name='Site 1', slug='site-1', region=region),
Site(name='Site 2', slug='site-2', region=region),
Site(name='Site 3', slug='site-3', region=region),
])
def test_site_list(self):
url = reverse('dcim:site_list')
params = {
"region": Region.objects.first().slug,
}
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
self.assertEqual(response.status_code, 200)
def test_site(self):
site = Site.objects.first()
response = self.client.get(site.get_absolute_url())
self.assertEqual(response.status_code, 200)
class RackGroupTestCase(TestCase):
def setUp(self):
user = create_test_user(permissions=['dcim.view_rackgroup'])
self.client = Client()
self.client.force_login(user)
site = Site(name='Site 1', slug='site-1')
site.save()
RackGroup.objects.bulk_create([
RackGroup(name='Rack Group 1', slug='rack-group-1', site=site),
RackGroup(name='Rack Group 2', slug='rack-group-2', site=site),
RackGroup(name='Rack Group 3', slug='rack-group-3', site=site),
])
def test_rackgroup_list(self):
url = reverse('dcim:rackgroup_list')
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
class RackRoleTestCase(TestCase):
def setUp(self):
user = create_test_user(permissions=['dcim.view_rackrole'])
self.client = Client()
self.client.force_login(user)
RackRole.objects.bulk_create([
RackRole(name='Rack Role 1', slug='rack-role-1'),
RackRole(name='Rack Role 2', slug='rack-role-2'),
RackRole(name='Rack Role 3', slug='rack-role-3'),
])
def test_rackrole_list(self):
url = reverse('dcim:rackrole_list')
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
class RackReservationTestCase(TestCase):
def setUp(self):
user = create_test_user(permissions=['dcim.view_rackreservation'])
self.client = Client()
self.client.force_login(user)
site = Site(name='Site 1', slug='site-1')
site.save()
rack = Rack(name='Rack 1', site=site)
rack.save()
RackReservation.objects.bulk_create([
RackReservation(rack=rack, user=user, units=[1, 2, 3], description='Reservation 1'),
RackReservation(rack=rack, user=user, units=[4, 5, 6], description='Reservation 2'),
RackReservation(rack=rack, user=user, units=[7, 8, 9], description='Reservation 3'),
])
def test_rackreservation_list(self):
url = reverse('dcim:rackreservation_list')
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
class RackTestCase(TestCase):
def setUp(self):
user = create_test_user(permissions=['dcim.view_rack'])
self.client = Client()
self.client.force_login(user)
site = Site(name='Site 1', slug='site-1')
site.save()
Rack.objects.bulk_create([
Rack(name='Rack 1', site=site),
Rack(name='Rack 2', site=site),
Rack(name='Rack 3', site=site),
])
def test_rack_list(self):
url = reverse('dcim:rack_list')
params = {
"site": Site.objects.first().slug,
}
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
self.assertEqual(response.status_code, 200)
def test_rack(self):
rack = Rack.objects.first()
response = self.client.get(rack.get_absolute_url())
self.assertEqual(response.status_code, 200)
class ManufacturerTypeTestCase(TestCase):
def setUp(self):
user = create_test_user(permissions=['dcim.view_manufacturer'])
self.client = Client()
self.client.force_login(user)
Manufacturer.objects.bulk_create([
Manufacturer(name='Manufacturer 1', slug='manufacturer-1'),
Manufacturer(name='Manufacturer 2', slug='manufacturer-2'),
Manufacturer(name='Manufacturer 3', slug='manufacturer-3'),
])
def test_manufacturer_list(self):
url = reverse('dcim:manufacturer_list')
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
class DeviceTypeTestCase(TestCase):
def setUp(self):
user = create_test_user(permissions=['dcim.view_devicetype'])
self.client = Client()
self.client.force_login(user)
manufacturer = Manufacturer(name='Manufacturer 1', slug='manufacturer-1')
manufacturer.save()
DeviceType.objects.bulk_create([
DeviceType(model='Device Type 1', slug='device-type-1', manufacturer=manufacturer),
DeviceType(model='Device Type 2', slug='device-type-2', manufacturer=manufacturer),
DeviceType(model='Device Type 3', slug='device-type-3', manufacturer=manufacturer),
])
def test_devicetype_list(self):
url = reverse('dcim:devicetype_list')
params = {
"manufacturer": Manufacturer.objects.first().slug,
}
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
self.assertEqual(response.status_code, 200)
def test_devicetype(self):
devicetype = DeviceType.objects.first()
response = self.client.get(devicetype.get_absolute_url())
self.assertEqual(response.status_code, 200)
class DeviceRoleTestCase(TestCase):
def setUp(self):
user = create_test_user(permissions=['dcim.view_devicerole'])
self.client = Client()
self.client.force_login(user)
DeviceRole.objects.bulk_create([
DeviceRole(name='Device Role 1', slug='device-role-1'),
DeviceRole(name='Device Role 2', slug='device-role-2'),
DeviceRole(name='Device Role 3', slug='device-role-3'),
])
def test_devicerole_list(self):
url = reverse('dcim:devicerole_list')
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
class PlatformTestCase(TestCase):
def setUp(self):
user = create_test_user(permissions=['dcim.view_platform'])
self.client = Client()
self.client.force_login(user)
Platform.objects.bulk_create([
Platform(name='Platform 1', slug='platform-1'),
Platform(name='Platform 2', slug='platform-2'),
Platform(name='Platform 3', slug='platform-3'),
])
def test_platform_list(self):
url = reverse('dcim:platform_list')
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
class DeviceTestCase(TestCase):
def setUp(self):
user = create_test_user(permissions=['dcim.view_device'])
self.client = Client()
self.client.force_login(user)
site = Site(name='Site 1', slug='site-1')
site.save()
manufacturer = Manufacturer(name='Manufacturer 1', slug='manufacturer-1')
manufacturer.save()
devicetype = DeviceType(model='Device Type 1', manufacturer=manufacturer)
devicetype.save()
devicerole = DeviceRole(name='Device Role 1', slug='device-role-1')
devicerole.save()
Device.objects.bulk_create([
Device(name='Device 1', site=site, device_type=devicetype, device_role=devicerole),
Device(name='Device 2', site=site, device_type=devicetype, device_role=devicerole),
Device(name='Device 3', site=site, device_type=devicetype, device_role=devicerole),
])
def test_device_list(self):
url = reverse('dcim:device_list')
params = {
"device_type_id": DeviceType.objects.first().pk,
"role": DeviceRole.objects.first().slug,
}
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
self.assertEqual(response.status_code, 200)
def test_device(self):
device = Device.objects.first()
response = self.client.get(device.get_absolute_url())
self.assertEqual(response.status_code, 200)
class InventoryItemTestCase(TestCase):
def setUp(self):
user = create_test_user(permissions=['dcim.view_inventoryitem'])
self.client = Client()
self.client.force_login(user)
site = Site(name='Site 1', slug='site-1')
site.save()
manufacturer = Manufacturer(name='Manufacturer 1', slug='manufacturer-1')
manufacturer.save()
devicetype = DeviceType(model='Device Type 1', manufacturer=manufacturer)
devicetype.save()
devicerole = DeviceRole(name='Device Role 1', slug='device-role-1')
devicerole.save()
device = Device(name='Device 1', site=site, device_type=devicetype, device_role=devicerole)
device.save()
InventoryItem.objects.bulk_create([
InventoryItem(device=device, name='Inventory Item 1'),
InventoryItem(device=device, name='Inventory Item 2'),
InventoryItem(device=device, name='Inventory Item 3'),
])
def test_inventoryitem_list(self):
url = reverse('dcim:inventoryitem_list')
params = {
"device_id": Device.objects.first().pk,
}
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
self.assertEqual(response.status_code, 200)
class CableTestCase(TestCase):
def setUp(self):
user = create_test_user(permissions=['dcim.view_cable'])
self.client = Client()
self.client.force_login(user)
site = Site(name='Site 1', slug='site-1')
site.save()
manufacturer = Manufacturer(name='Manufacturer 1', slug='manufacturer-1')
manufacturer.save()
devicetype = DeviceType(model='Device Type 1', manufacturer=manufacturer)
devicetype.save()
devicerole = DeviceRole(name='Device Role 1', slug='device-role-1')
devicerole.save()
device1 = Device(name='Device 1', site=site, device_type=devicetype, device_role=devicerole)
device1.save()
device2 = Device(name='Device 2', site=site, device_type=devicetype, device_role=devicerole)
device2.save()
iface1 = Interface(device=device1, name='Interface 1', type=IFACE_TYPE_1GE_FIXED)
iface1.save()
iface2 = Interface(device=device1, name='Interface 2', type=IFACE_TYPE_1GE_FIXED)
iface2.save()
iface3 = Interface(device=device1, name='Interface 3', type=IFACE_TYPE_1GE_FIXED)
iface3.save()
iface4 = Interface(device=device2, name='Interface 1', type=IFACE_TYPE_1GE_FIXED)
iface4.save()
iface5 = Interface(device=device2, name='Interface 2', type=IFACE_TYPE_1GE_FIXED)
iface5.save()
iface6 = Interface(device=device2, name='Interface 3', type=IFACE_TYPE_1GE_FIXED)
iface6.save()
Cable(termination_a=iface1, termination_b=iface4, type=CABLE_TYPE_CAT6).save()
Cable(termination_a=iface2, termination_b=iface5, type=CABLE_TYPE_CAT6).save()
Cable(termination_a=iface3, termination_b=iface6, type=CABLE_TYPE_CAT6).save()
def test_cable_list(self):
url = reverse('dcim:cable_list')
params = {
"type": CABLE_TYPE_CAT6,
}
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
self.assertEqual(response.status_code, 200)
def test_cable(self):
cable = Cable.objects.first()
response = self.client.get(cable.get_absolute_url())
self.assertEqual(response.status_code, 200)
class VirtualChassisTestCase(TestCase):
def setUp(self):
user = create_test_user(permissions=['dcim.view_virtualchassis'])
self.client = Client()
self.client.force_login(user)
site = Site.objects.create(name='Site 1', slug='site-1')
manufacturer = Manufacturer.objects.create(name='Manufacturer', slug='manufacturer-1')
device_type = DeviceType.objects.create(
manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'
)
device_role = DeviceRole.objects.create(
name='Device Role', slug='device-role-1'
)
# Create 9 member Devices
device1 = Device.objects.create(
device_type=device_type, device_role=device_role, name='Device 1', site=site
)
device2 = Device.objects.create(
device_type=device_type, device_role=device_role, name='Device 2', site=site
)
device3 = Device.objects.create(
device_type=device_type, device_role=device_role, name='Device 3', site=site
)
device4 = Device.objects.create(
device_type=device_type, device_role=device_role, name='Device 4', site=site
)
device5 = Device.objects.create(
device_type=device_type, device_role=device_role, name='Device 5', site=site
)
device6 = Device.objects.create(
device_type=device_type, device_role=device_role, name='Device 6', site=site
)
# Create three VirtualChassis with two members each
vc1 = VirtualChassis.objects.create(master=device1, domain='test-domain-1')
Device.objects.filter(pk=device2.pk).update(virtual_chassis=vc1, vc_position=2)
vc2 = VirtualChassis.objects.create(master=device3, domain='test-domain-2')
Device.objects.filter(pk=device4.pk).update(virtual_chassis=vc2, vc_position=2)
vc3 = VirtualChassis.objects.create(master=device5, domain='test-domain-3')
Device.objects.filter(pk=device6.pk).update(virtual_chassis=vc3, vc_position=2)
def test_virtualchassis_list(self):
url = reverse('dcim:virtualchassis_list')
response = self.client.get(url)
self.assertEqual(response.status_code, 200)

View File

@@ -6,8 +6,7 @@ from secrets.views import secret_add
from . import views
from .models import (
Cable, ConsolePort, ConsoleServerPort, Device, DeviceRole, DeviceType, FrontPort, Interface, Manufacturer, Platform,
PowerFeed, PowerPanel, PowerPort, PowerOutlet, Rack, RackGroup, RackReservation, RackRole, RearPort, Region, Site,
VirtualChassis,
PowerPort, PowerOutlet, Rack, RackGroup, RackReservation, RackRole, RearPort, Region, Site, VirtualChassis,
)
app_name = 'dcim'
@@ -162,7 +161,7 @@ urlpatterns = [
url(r'^devices/console-ports/add/$', views.DeviceBulkAddConsolePortView.as_view(), name='device_bulk_add_consoleport'),
url(r'^devices/(?P<pk>\d+)/console-ports/add/$', views.ConsolePortCreateView.as_view(), name='consoleport_add'),
url(r'^devices/(?P<pk>\d+)/console-ports/delete/$', views.ConsolePortBulkDeleteView.as_view(), name='consoleport_bulk_delete'),
url(r'^console-ports/(?P<termination_a_id>\d+)/connect/(?P<termination_b_type>[\w-]+)/$', views.CableCreateView.as_view(), name='consoleport_connect', kwargs={'termination_a_type': ConsolePort}),
url(r'^console-ports/(?P<termination_a_id>\d+)/connect/$', views.CableCreateView.as_view(), name='consoleport_connect', kwargs={'termination_a_type': ConsolePort}),
url(r'^console-ports/(?P<pk>\d+)/edit/$', views.ConsolePortEditView.as_view(), name='consoleport_edit'),
url(r'^console-ports/(?P<pk>\d+)/delete/$', views.ConsolePortDeleteView.as_view(), name='consoleport_delete'),
url(r'^console-ports/(?P<pk>\d+)/trace/$', views.CableTraceView.as_view(), name='consoleport_trace', kwargs={'model': ConsolePort}),
@@ -171,7 +170,7 @@ urlpatterns = [
url(r'^devices/console-server-ports/add/$', views.DeviceBulkAddConsoleServerPortView.as_view(), name='device_bulk_add_consoleserverport'),
url(r'^devices/(?P<pk>\d+)/console-server-ports/add/$', views.ConsoleServerPortCreateView.as_view(), name='consoleserverport_add'),
url(r'^devices/(?P<pk>\d+)/console-server-ports/delete/$', views.ConsoleServerPortBulkDeleteView.as_view(), name='consoleserverport_bulk_delete'),
url(r'^console-server-ports/(?P<termination_a_id>\d+)/connect/(?P<termination_b_type>[\w-]+)/$', views.CableCreateView.as_view(), name='consoleserverport_connect', kwargs={'termination_a_type': ConsoleServerPort}),
url(r'^console-server-ports/(?P<termination_a_id>\d+)/connect/$', views.CableCreateView.as_view(), name='consoleserverport_connect', kwargs={'termination_a_type': ConsoleServerPort}),
url(r'^console-server-ports/(?P<pk>\d+)/edit/$', views.ConsoleServerPortEditView.as_view(), name='consoleserverport_edit'),
url(r'^console-server-ports/(?P<pk>\d+)/delete/$', views.ConsoleServerPortDeleteView.as_view(), name='consoleserverport_delete'),
url(r'^console-server-ports/(?P<pk>\d+)/trace/$', views.CableTraceView.as_view(), name='consoleserverport_trace', kwargs={'model': ConsoleServerPort}),
@@ -182,7 +181,7 @@ urlpatterns = [
url(r'^devices/power-ports/add/$', views.DeviceBulkAddPowerPortView.as_view(), name='device_bulk_add_powerport'),
url(r'^devices/(?P<pk>\d+)/power-ports/add/$', views.PowerPortCreateView.as_view(), name='powerport_add'),
url(r'^devices/(?P<pk>\d+)/power-ports/delete/$', views.PowerPortBulkDeleteView.as_view(), name='powerport_bulk_delete'),
url(r'^power-ports/(?P<termination_a_id>\d+)/connect/(?P<termination_b_type>[\w-]+)/$', views.CableCreateView.as_view(), name='powerport_connect', kwargs={'termination_a_type': PowerPort}),
url(r'^power-ports/(?P<termination_a_id>\d+)/connect/$', views.CableCreateView.as_view(), name='powerport_connect', kwargs={'termination_a_type': PowerPort}),
url(r'^power-ports/(?P<pk>\d+)/edit/$', views.PowerPortEditView.as_view(), name='powerport_edit'),
url(r'^power-ports/(?P<pk>\d+)/delete/$', views.PowerPortDeleteView.as_view(), name='powerport_delete'),
url(r'^power-ports/(?P<pk>\d+)/trace/$', views.CableTraceView.as_view(), name='powerport_trace', kwargs={'model': PowerPort}),
@@ -191,7 +190,7 @@ urlpatterns = [
url(r'^devices/power-outlets/add/$', views.DeviceBulkAddPowerOutletView.as_view(), name='device_bulk_add_poweroutlet'),
url(r'^devices/(?P<pk>\d+)/power-outlets/add/$', views.PowerOutletCreateView.as_view(), name='poweroutlet_add'),
url(r'^devices/(?P<pk>\d+)/power-outlets/delete/$', views.PowerOutletBulkDeleteView.as_view(), name='poweroutlet_bulk_delete'),
url(r'^power-outlets/(?P<termination_a_id>\d+)/connect/(?P<termination_b_type>[\w-]+)/$', views.CableCreateView.as_view(), name='poweroutlet_connect', kwargs={'termination_a_type': PowerOutlet}),
url(r'^power-outlets/(?P<termination_a_id>\d+)/connect/$', views.CableCreateView.as_view(), name='poweroutlet_connect', kwargs={'termination_a_type': PowerOutlet}),
url(r'^power-outlets/(?P<pk>\d+)/edit/$', views.PowerOutletEditView.as_view(), name='poweroutlet_edit'),
url(r'^power-outlets/(?P<pk>\d+)/delete/$', views.PowerOutletDeleteView.as_view(), name='poweroutlet_delete'),
url(r'^power-outlets/(?P<pk>\d+)/trace/$', views.CableTraceView.as_view(), name='poweroutlet_trace', kwargs={'model': PowerOutlet}),
@@ -203,7 +202,7 @@ urlpatterns = [
url(r'^devices/(?P<pk>\d+)/interfaces/add/$', views.InterfaceCreateView.as_view(), name='interface_add'),
url(r'^devices/(?P<pk>\d+)/interfaces/edit/$', views.InterfaceBulkEditView.as_view(), name='interface_bulk_edit'),
url(r'^devices/(?P<pk>\d+)/interfaces/delete/$', views.InterfaceBulkDeleteView.as_view(), name='interface_bulk_delete'),
url(r'^interfaces/(?P<termination_a_id>\d+)/connect/(?P<termination_b_type>[\w-]+)/$', views.CableCreateView.as_view(), name='interface_connect', kwargs={'termination_a_type': Interface}),
url(r'^interfaces/(?P<termination_a_id>\d+)/connect/$', views.CableCreateView.as_view(), name='interface_connect', kwargs={'termination_a_type': Interface}),
url(r'^interfaces/(?P<pk>\d+)/$', views.InterfaceView.as_view(), name='interface'),
url(r'^interfaces/(?P<pk>\d+)/edit/$', views.InterfaceEditView.as_view(), name='interface_edit'),
url(r'^interfaces/(?P<pk>\d+)/assign-vlans/$', views.InterfaceAssignVLANsView.as_view(), name='interface_assign_vlans'),
@@ -216,9 +215,8 @@ urlpatterns = [
# Front ports
# url(r'^devices/front-ports/add/$', views.DeviceBulkAddFrontPortView.as_view(), name='device_bulk_add_frontport'),
url(r'^devices/(?P<pk>\d+)/front-ports/add/$', views.FrontPortCreateView.as_view(), name='frontport_add'),
url(r'^devices/(?P<pk>\d+)/front-ports/edit/$', views.FrontPortBulkEditView.as_view(), name='frontport_bulk_edit'),
url(r'^devices/(?P<pk>\d+)/front-ports/delete/$', views.FrontPortBulkDeleteView.as_view(), name='frontport_bulk_delete'),
url(r'^front-ports/(?P<termination_a_id>\d+)/connect/(?P<termination_b_type>[\w-]+)/$', views.CableCreateView.as_view(), name='frontport_connect', kwargs={'termination_a_type': FrontPort}),
url(r'^front-ports/(?P<termination_a_id>\d+)/connect/$', views.CableCreateView.as_view(), name='frontport_connect', kwargs={'termination_a_type': FrontPort}),
url(r'^front-ports/(?P<pk>\d+)/edit/$', views.FrontPortEditView.as_view(), name='frontport_edit'),
url(r'^front-ports/(?P<pk>\d+)/delete/$', views.FrontPortDeleteView.as_view(), name='frontport_delete'),
url(r'^front-ports/(?P<pk>\d+)/trace/$', views.CableTraceView.as_view(), name='frontport_trace', kwargs={'model': FrontPort}),
@@ -228,9 +226,8 @@ urlpatterns = [
# Rear ports
# url(r'^devices/rear-ports/add/$', views.DeviceBulkAddRearPortView.as_view(), name='device_bulk_add_rearport'),
url(r'^devices/(?P<pk>\d+)/rear-ports/add/$', views.RearPortCreateView.as_view(), name='rearport_add'),
url(r'^devices/(?P<pk>\d+)/rear-ports/edit/$', views.RearPortBulkEditView.as_view(), name='rearport_bulk_edit'),
url(r'^devices/(?P<pk>\d+)/rear-ports/delete/$', views.RearPortBulkDeleteView.as_view(), name='rearport_bulk_delete'),
url(r'^rear-ports/(?P<termination_a_id>\d+)/connect/(?P<termination_b_type>[\w-]+)/$', views.CableCreateView.as_view(), name='rearport_connect', kwargs={'termination_a_type': RearPort}),
url(r'^rear-ports/(?P<termination_a_id>\d+)/connect/$', views.CableCreateView.as_view(), name='rearport_connect', kwargs={'termination_a_type': RearPort}),
url(r'^rear-ports/(?P<pk>\d+)/edit/$', views.RearPortEditView.as_view(), name='rearport_edit'),
url(r'^rear-ports/(?P<pk>\d+)/delete/$', views.RearPortDeleteView.as_view(), name='rearport_delete'),
url(r'^rear-ports/(?P<pk>\d+)/trace/$', views.CableTraceView.as_view(), name='rearport_trace', kwargs={'model': RearPort}),
@@ -280,25 +277,4 @@ urlpatterns = [
url(r'^virtual-chassis/(?P<pk>\d+)/add-member/$', views.VirtualChassisAddMemberView.as_view(), name='virtualchassis_add_member'),
url(r'^virtual-chassis-members/(?P<pk>\d+)/delete/$', views.VirtualChassisRemoveMemberView.as_view(), name='virtualchassis_remove_member'),
# Power panels
url(r'^power-panels/$', views.PowerPanelListView.as_view(), name='powerpanel_list'),
url(r'^power-panels/add/$', views.PowerPanelCreateView.as_view(), name='powerpanel_add'),
url(r'^power-panels/import/$', views.PowerPanelBulkImportView.as_view(), name='powerpanel_import'),
url(r'^power-panels/delete/$', views.PowerPanelBulkDeleteView.as_view(), name='powerpanel_bulk_delete'),
url(r'^power-panels/(?P<pk>\d+)/$', views.PowerPanelView.as_view(), name='powerpanel'),
url(r'^power-panels/(?P<pk>\d+)/edit/$', views.PowerPanelEditView.as_view(), name='powerpanel_edit'),
url(r'^power-panels/(?P<pk>\d+)/delete/$', views.PowerPanelDeleteView.as_view(), name='powerpanel_delete'),
url(r'^power-panels/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='powerpanel_changelog', kwargs={'model': PowerPanel}),
# Power feeds
url(r'^power-feeds/$', views.PowerFeedListView.as_view(), name='powerfeed_list'),
url(r'^power-feeds/add/$', views.PowerFeedEditView.as_view(), name='powerfeed_add'),
url(r'^power-feeds/import/$', views.PowerFeedBulkImportView.as_view(), name='powerfeed_import'),
url(r'^power-feeds/edit/$', views.PowerFeedBulkEditView.as_view(), name='powerfeed_bulk_edit'),
url(r'^power-feeds/delete/$', views.PowerFeedBulkDeleteView.as_view(), name='powerfeed_bulk_delete'),
url(r'^power-feeds/(?P<pk>\d+)/$', views.PowerFeedView.as_view(), name='powerfeed'),
url(r'^power-feeds/(?P<pk>\d+)/edit/$', views.PowerFeedEditView.as_view(), name='powerfeed_edit'),
url(r'^power-feeds/(?P<pk>\d+)/delete/$', views.PowerFeedDeleteView.as_view(), name='powerfeed_delete'),
url(r'^power-feeds/(?P<pk>\d+)/changelog/$', ObjectChangeLogView.as_view(), name='powerfeed_changelog', kwargs={'model': PowerFeed}),
]

View File

@@ -1,9 +1,5 @@
import re
from django.conf import settings
from django.contrib import messages
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.contrib.contenttypes.models import ContentType
from django.core.paginator import EmptyPage, PageNotAnInteger
from django.db import transaction
from django.db.models import Count, F
@@ -11,7 +7,6 @@ from django.forms import modelformset_factory
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.utils.html import escape
from django.utils.http import is_safe_url
from django.utils.safestring import mark_safe
from django.views.generic import View
@@ -32,9 +27,8 @@ from . import filters, forms, tables
from .models import (
Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate,
InventoryItem, Manufacturer, Platform, PowerFeed, PowerOutlet, PowerOutletTemplate, PowerPanel, PowerPort,
PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site,
VirtualChassis,
InventoryItem, Manufacturer, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack,
RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site, VirtualChassis,
)
@@ -56,16 +50,7 @@ class BulkRenameView(GetReturnURLMixin, View):
if form.is_valid():
for obj in selected_objects:
find = form.cleaned_data['find']
replace = form.cleaned_data['replace']
if form.cleaned_data['use_regex']:
try:
obj.new_name = re.sub(find, replace, obj.name)
# Catch regex group reference errors
except re.error:
obj.new_name = obj.name
else:
obj.new_name = obj.name.replace(find, replace)
obj.new_name = obj.name.replace(form.cleaned_data['find'], form.cleaned_data['replace'])
if '_apply' in request.POST:
for obj in selected_objects:
@@ -138,15 +123,8 @@ class BulkDisconnectView(GetReturnURLMixin, View):
# Regions
#
class RegionListView(PermissionRequiredMixin, ObjectListView):
permission_required = 'dcim.view_region'
queryset = Region.objects.add_related_count(
Region.objects.all(),
Site,
'region',
'site_count',
cumulative=True
)
class RegionListView(ObjectListView):
queryset = Region.objects.annotate(site_count=Count('sites'))
filter = filters.RegionFilter
filter_form = forms.RegionFilterForm
table = tables.RegionTable
@@ -173,7 +151,7 @@ class RegionBulkImportView(PermissionRequiredMixin, BulkImportView):
class RegionBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_region'
queryset = Region.objects.all()
queryset = Region.objects.annotate(site_count=Count('sites'))
filter = filters.RegionFilter
table = tables.RegionTable
default_return_url = 'dcim:region_list'
@@ -183,8 +161,7 @@ class RegionBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
# Sites
#
class SiteListView(PermissionRequiredMixin, ObjectListView):
permission_required = 'dcim.view_site'
class SiteListView(ObjectListView):
queryset = Site.objects.select_related('region', 'tenant')
filter = filters.SiteFilter
filter_form = forms.SiteFilterForm
@@ -192,8 +169,7 @@ class SiteListView(PermissionRequiredMixin, ObjectListView):
template_name = 'dcim/site_list.html'
class SiteView(PermissionRequiredMixin, View):
permission_required = 'dcim.view_site'
class SiteView(View):
def get(self, request, slug):
@@ -257,8 +233,7 @@ class SiteBulkEditView(PermissionRequiredMixin, BulkEditView):
# Rack groups
#
class RackGroupListView(PermissionRequiredMixin, ObjectListView):
permission_required = 'dcim.view_rackgroup'
class RackGroupListView(ObjectListView):
queryset = RackGroup.objects.select_related('site').annotate(rack_count=Count('racks'))
filter = filters.RackGroupFilter
filter_form = forms.RackGroupFilterForm
@@ -296,8 +271,7 @@ class RackGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
# Rack roles
#
class RackRoleListView(PermissionRequiredMixin, ObjectListView):
permission_required = 'dcim.view_rackrole'
class RackRoleListView(ObjectListView):
queryset = RackRole.objects.annotate(rack_count=Count('racks'))
table = tables.RackRoleTable
template_name = 'dcim/rackrole_list.html'
@@ -332,8 +306,7 @@ class RackRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
# Racks
#
class RackListView(PermissionRequiredMixin, ObjectListView):
permission_required = 'dcim.view_rack'
class RackListView(ObjectListView):
queryset = Rack.objects.select_related(
'site', 'group', 'tenant', 'role'
).prefetch_related(
@@ -347,11 +320,10 @@ class RackListView(PermissionRequiredMixin, ObjectListView):
template_name = 'dcim/rack_list.html'
class RackElevationListView(PermissionRequiredMixin, View):
class RackElevationListView(View):
"""
Display a set of rack elevations side-by-side.
"""
permission_required = 'dcim.view_rack'
def get(self, request):
@@ -364,9 +336,8 @@ class RackElevationListView(PermissionRequiredMixin, View):
total_count = racks.count()
# Pagination
per_page = request.GET.get('per_page', settings.PAGINATE_COUNT)
paginator = EnhancedPaginator(racks, 25)
page_number = request.GET.get('page', 1)
paginator = EnhancedPaginator(racks, per_page)
try:
page = paginator.page(page_number)
except PageNotAnInteger:
@@ -389,8 +360,7 @@ class RackElevationListView(PermissionRequiredMixin, View):
})
class RackView(PermissionRequiredMixin, View):
permission_required = 'dcim.view_rack'
class RackView(View):
def get(self, request, pk):
@@ -402,12 +372,10 @@ class RackView(PermissionRequiredMixin, View):
prev_rack = Rack.objects.filter(site=rack.site, name__lt=rack.name).order_by('-name').first()
reservations = RackReservation.objects.filter(rack=rack)
power_feeds = PowerFeed.objects.filter(rack=rack).select_related('power_panel')
return render(request, 'dcim/rack.html', {
'rack': rack,
'reservations': reservations,
'power_feeds': power_feeds,
'nonracked_devices': nonracked_devices,
'next_rack': next_rack,
'prev_rack': prev_rack,
@@ -462,8 +430,7 @@ class RackBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
# Rack reservations
#
class RackReservationListView(PermissionRequiredMixin, ObjectListView):
permission_required = 'dcim.view_rackreservation'
class RackReservationListView(ObjectListView):
queryset = RackReservation.objects.all()
filter = filters.RackReservationFilter
filter_form = forms.RackReservationFilterForm
@@ -519,8 +486,7 @@ class RackReservationBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
# Manufacturers
#
class ManufacturerListView(PermissionRequiredMixin, ObjectListView):
permission_required = 'dcim.view_manufacturer'
class ManufacturerListView(ObjectListView):
queryset = Manufacturer.objects.annotate(
devicetype_count=Count('device_types', distinct=True),
platform_count=Count('platforms', distinct=True),
@@ -558,8 +524,7 @@ class ManufacturerBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
# Device types
#
class DeviceTypeListView(PermissionRequiredMixin, ObjectListView):
permission_required = 'dcim.view_devicetype'
class DeviceTypeListView(ObjectListView):
queryset = DeviceType.objects.select_related('manufacturer').annotate(instance_count=Count('instances'))
filter = filters.DeviceTypeFilter
filter_form = forms.DeviceTypeFilterForm
@@ -567,8 +532,7 @@ class DeviceTypeListView(PermissionRequiredMixin, ObjectListView):
template_name = 'dcim/devicetype_list.html'
class DeviceTypeView(PermissionRequiredMixin, View):
permission_required = 'dcim.view_devicetype'
class DeviceTypeView(View):
def get(self, request, pk):
@@ -824,8 +788,7 @@ class DeviceBayTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
# Device roles
#
class DeviceRoleListView(PermissionRequiredMixin, ObjectListView):
permission_required = 'dcim.view_devicerole'
class DeviceRoleListView(ObjectListView):
queryset = DeviceRole.objects.all()
table = tables.DeviceRoleTable
template_name = 'dcim/devicerole_list.html'
@@ -860,8 +823,7 @@ class DeviceRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
# Platforms
#
class PlatformListView(PermissionRequiredMixin, ObjectListView):
permission_required = 'dcim.view_platform'
class PlatformListView(ObjectListView):
queryset = Platform.objects.all()
table = tables.PlatformTable
template_name = 'dcim/platform_list.html'
@@ -896,8 +858,7 @@ class PlatformBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
# Devices
#
class DeviceListView(PermissionRequiredMixin, ObjectListView):
permission_required = 'dcim.view_device'
class DeviceListView(ObjectListView):
queryset = Device.objects.select_related(
'device_type__manufacturer', 'device_role', 'tenant', 'site', 'rack', 'primary_ip4', 'primary_ip6'
)
@@ -907,8 +868,7 @@ class DeviceListView(PermissionRequiredMixin, ObjectListView):
template_name = 'dcim/device_list.html'
class DeviceView(PermissionRequiredMixin, View):
permission_required = 'dcim.view_device'
class DeviceView(View):
def get(self, request, pk):
@@ -931,7 +891,7 @@ class DeviceView(PermissionRequiredMixin, View):
consoleserverports = device.consoleserverports.select_related('connected_endpoint__device', 'cable')
# Power ports
power_ports = device.powerports.select_related('_connected_poweroutlet__device', 'cable')
power_ports = device.powerports.select_related('connected_endpoint__device', 'cable')
# Power outlets
poweroutlets = device.poweroutlets.select_related('connected_endpoint__device', 'cable')
@@ -988,8 +948,7 @@ class DeviceView(PermissionRequiredMixin, View):
})
class DeviceInventoryView(PermissionRequiredMixin, View):
permission_required = 'dcim.view_device'
class DeviceInventoryView(View):
def get(self, request, pk):
@@ -1010,7 +969,7 @@ class DeviceInventoryView(PermissionRequiredMixin, View):
class DeviceStatusView(PermissionRequiredMixin, View):
permission_required = ('dcim.view_device', 'dcim.napalm_read')
permission_required = 'dcim.napalm_read'
def get(self, request, pk):
@@ -1023,7 +982,7 @@ class DeviceStatusView(PermissionRequiredMixin, View):
class DeviceLLDPNeighborsView(PermissionRequiredMixin, View):
permission_required = ('dcim.view_device', 'dcim.napalm_read')
permission_required = 'dcim.napalm_read'
def get(self, request, pk):
@@ -1040,7 +999,7 @@ class DeviceLLDPNeighborsView(PermissionRequiredMixin, View):
class DeviceConfigView(PermissionRequiredMixin, View):
permission_required = ('dcim.view_device', 'dcim.napalm_read')
permission_required = 'dcim.napalm_read'
def get(self, request, pk):
@@ -1052,8 +1011,7 @@ class DeviceConfigView(PermissionRequiredMixin, View):
})
class DeviceConfigContextView(PermissionRequiredMixin, ObjectConfigContextView):
permission_required = 'dcim.view_device'
class DeviceConfigContextView(ObjectConfigContextView):
object_class = Device
base_template = 'dcim/device.html'
@@ -1276,8 +1234,7 @@ class PowerOutletBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
# Interfaces
#
class InterfaceView(PermissionRequiredMixin, View):
permission_required = 'dcim.view_interface'
class InterfaceView(View):
def get(self, request, pk):
@@ -1392,14 +1349,6 @@ class FrontPortDeleteView(PermissionRequiredMixin, ObjectDeleteView):
model = FrontPort
class FrontPortBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.change_frontport'
queryset = FrontPort.objects.all()
parent_model = Device
table = tables.FrontPortTable
form = forms.FrontPortBulkEditForm
class FrontPortBulkRenameView(PermissionRequiredMixin, BulkRenameView):
permission_required = 'dcim.change_frontport'
queryset = FrontPort.objects.all()
@@ -1444,14 +1393,6 @@ class RearPortDeleteView(PermissionRequiredMixin, ObjectDeleteView):
model = RearPort
class RearPortBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.change_rearport'
queryset = RearPort.objects.all()
parent_model = Device
table = tables.RearPortTable
form = forms.RearPortBulkEditForm
class RearPortBulkRenameView(PermissionRequiredMixin, BulkRenameView):
permission_required = 'dcim.change_rearport'
queryset = RearPort.objects.all()
@@ -1589,7 +1530,6 @@ class DeviceBulkAddConsolePortView(PermissionRequiredMixin, BulkComponentCreateV
form = forms.DeviceBulkAddComponentForm
model = ConsolePort
model_form = forms.ConsolePortForm
filter = filters.DeviceFilter
table = tables.DeviceTable
default_return_url = 'dcim:device_list'
@@ -1601,7 +1541,6 @@ class DeviceBulkAddConsoleServerPortView(PermissionRequiredMixin, BulkComponentC
form = forms.DeviceBulkAddComponentForm
model = ConsoleServerPort
model_form = forms.ConsoleServerPortForm
filter = filters.DeviceFilter
table = tables.DeviceTable
default_return_url = 'dcim:device_list'
@@ -1613,7 +1552,6 @@ class DeviceBulkAddPowerPortView(PermissionRequiredMixin, BulkComponentCreateVie
form = forms.DeviceBulkAddComponentForm
model = PowerPort
model_form = forms.PowerPortForm
filter = filters.DeviceFilter
table = tables.DeviceTable
default_return_url = 'dcim:device_list'
@@ -1625,7 +1563,6 @@ class DeviceBulkAddPowerOutletView(PermissionRequiredMixin, BulkComponentCreateV
form = forms.DeviceBulkAddComponentForm
model = PowerOutlet
model_form = forms.PowerOutletForm
filter = filters.DeviceFilter
table = tables.DeviceTable
default_return_url = 'dcim:device_list'
@@ -1637,7 +1574,6 @@ class DeviceBulkAddInterfaceView(PermissionRequiredMixin, BulkComponentCreateVie
form = forms.DeviceBulkAddInterfaceForm
model = Interface
model_form = forms.InterfaceForm
filter = filters.DeviceFilter
table = tables.DeviceTable
default_return_url = 'dcim:device_list'
@@ -1649,7 +1585,6 @@ class DeviceBulkAddDeviceBayView(PermissionRequiredMixin, BulkComponentCreateVie
form = forms.DeviceBulkAddComponentForm
model = DeviceBay
model_form = forms.DeviceBayForm
filter = filters.DeviceFilter
table = tables.DeviceTable
default_return_url = 'dcim:device_list'
@@ -1658,8 +1593,7 @@ class DeviceBulkAddDeviceBayView(PermissionRequiredMixin, BulkComponentCreateVie
# Cables
#
class CableListView(PermissionRequiredMixin, ObjectListView):
permission_required = 'dcim.view_cable'
class CableListView(ObjectListView):
queryset = Cable.objects.prefetch_related(
'termination_a', 'termination_b'
)
@@ -1669,8 +1603,7 @@ class CableListView(PermissionRequiredMixin, ObjectListView):
template_name = 'dcim/cable_list.html'
class CableView(PermissionRequiredMixin, View):
permission_required = 'dcim.view_cable'
class CableView(View):
def get(self, request, pk):
@@ -1681,11 +1614,10 @@ class CableView(PermissionRequiredMixin, View):
})
class CableTraceView(PermissionRequiredMixin, View):
class CableTraceView(View):
"""
Trace a cable path beginning from the given termination.
"""
permission_required = 'dcim.view_cable'
def get(self, request, model, pk):
@@ -1697,79 +1629,20 @@ class CableTraceView(PermissionRequiredMixin, View):
})
class CableCreateView(PermissionRequiredMixin, GetReturnURLMixin, View):
class CableCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.add_cable'
model = Cable
model_form = forms.CableCreateForm
template_name = 'dcim/cable_connect.html'
def dispatch(self, request, *args, **kwargs):
def alter_obj(self, obj, request, url_args, url_kwargs):
termination_a_type = kwargs.get('termination_a_type')
termination_a_id = kwargs.get('termination_a_id')
# Retrieve endpoint A based on the given type and PK
termination_a_type = url_kwargs.get('termination_a_type')
termination_a_id = url_kwargs.get('termination_a_id')
obj.termination_a = termination_a_type.objects.get(pk=termination_a_id)
termination_b_type_name = kwargs.get('termination_b_type')
self.termination_b_type = ContentType.objects.get(model=termination_b_type_name.replace('-', ''))
self.obj = Cable(
termination_a=termination_a_type.objects.get(pk=termination_a_id),
termination_b_type=self.termination_b_type
)
self.form_class = {
'console-port': forms.ConnectCableToConsolePortForm,
'console-server-port': forms.ConnectCableToConsoleServerPortForm,
'power-port': forms.ConnectCableToPowerPortForm,
'power-outlet': forms.ConnectCableToPowerOutletForm,
'interface': forms.ConnectCableToInterfaceForm,
'front-port': forms.ConnectCableToFrontPortForm,
'power-feed': forms.ConnectCableToPowerFeedForm,
'circuit-termination': forms.ConnectCableToCircuitTerminationForm,
}[termination_b_type_name]
return super().dispatch(request, *args, **kwargs)
def get(self, request, *args, **kwargs):
# Parse initial data manually to avoid setting field values as lists
initial_data = {k: request.GET[k] for k in request.GET}
form = self.form_class(instance=self.obj, initial=initial_data)
return render(request, self.template_name, {
'obj': self.obj,
'obj_type': Cable._meta.verbose_name,
'termination_b_type': self.termination_b_type.name,
'form': form,
'return_url': self.get_return_url(request, self.obj),
})
def post(self, request, *args, **kwargs):
form = self.form_class(request.POST, request.FILES, instance=self.obj)
if form.is_valid():
obj = form.save()
msg = 'Created cable <a href="{}">{}</a>'.format(
obj.get_absolute_url(),
escape(obj)
)
messages.success(request, mark_safe(msg))
if '_addanother' in request.POST:
return redirect(request.get_full_path())
return_url = form.cleaned_data.get('return_url')
if return_url is not None and is_safe_url(url=return_url, allowed_hosts=request.get_host()):
return redirect(return_url)
else:
return redirect(self.get_return_url(request, obj))
return render(request, self.template_name, {
'obj': self.obj,
'obj_type': Cable._meta.verbose_name,
'termination_b_type': self.termination_b_type.name,
'form': form,
'return_url': self.get_return_url(request, self.obj),
})
return obj
class CableEditView(PermissionRequiredMixin, ObjectEditView):
@@ -1814,8 +1687,7 @@ class CableBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
# Connections
#
class ConsoleConnectionsListView(PermissionRequiredMixin, ObjectListView):
permission_required = ('dcim.view_consoleport', 'dcim.view_consoleserverport')
class ConsoleConnectionsListView(ObjectListView):
queryset = ConsolePort.objects.select_related(
'device', 'connected_endpoint__device'
).filter(
@@ -1845,14 +1717,13 @@ class ConsoleConnectionsListView(PermissionRequiredMixin, ObjectListView):
return csv_data
class PowerConnectionsListView(PermissionRequiredMixin, ObjectListView):
permission_required = ('dcim.view_powerport', 'dcim.view_poweroutlet')
class PowerConnectionsListView(ObjectListView):
queryset = PowerPort.objects.select_related(
'device', '_connected_poweroutlet__device'
'device', 'connected_endpoint__device'
).filter(
_connected_poweroutlet__isnull=False
connected_endpoint__isnull=False
).order_by(
'cable', '_connected_poweroutlet__device__name', '_connected_poweroutlet__name'
'cable', 'connected_endpoint__device__name', 'connected_endpoint__name'
)
filter = filters.PowerConnectionFilter
filter_form = forms.PowerConnectionFilterForm
@@ -1876,8 +1747,7 @@ class PowerConnectionsListView(PermissionRequiredMixin, ObjectListView):
return csv_data
class InterfaceConnectionsListView(PermissionRequiredMixin, ObjectListView):
permission_required = 'dcim.interface'
class InterfaceConnectionsListView(ObjectListView):
queryset = Interface.objects.select_related(
'device', 'cable', '_connected_interface__device'
).filter(
@@ -1895,20 +1765,14 @@ class InterfaceConnectionsListView(PermissionRequiredMixin, ObjectListView):
def queryset_to_csv(self):
csv_data = [
# Headers
','.join([
'device_a', 'interface_a', 'interface_a_description',
'device_b', 'interface_b', 'interface_b_description',
'connection_status'
])
','.join(['device_a', 'interface_a', 'device_b', 'interface_b', 'connection_status'])
]
for obj in self.queryset:
csv = csv_format([
obj.connected_endpoint.device.identifier if obj.connected_endpoint else None,
obj.connected_endpoint.name if obj.connected_endpoint else None,
obj.connected_endpoint.description if obj.connected_endpoint else None,
obj.device.identifier,
obj.name,
obj.description,
obj.get_connection_status_display(),
])
csv_data.append(csv)
@@ -1919,8 +1783,7 @@ class InterfaceConnectionsListView(PermissionRequiredMixin, ObjectListView):
# Inventory items
#
class InventoryItemListView(PermissionRequiredMixin, ObjectListView):
permission_required = 'dcim.view_inventoryitem'
class InventoryItemListView(ObjectListView):
queryset = InventoryItem.objects.select_related('device', 'manufacturer')
filter = filters.InventoryItemFilter
filter_form = forms.InventoryItemFilterForm
@@ -1975,8 +1838,7 @@ class InventoryItemBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
# Virtual chassis
#
class VirtualChassisListView(PermissionRequiredMixin, ObjectListView):
permission_required = 'dcim.view_virtualchassis'
class VirtualChassisListView(ObjectListView):
queryset = VirtualChassis.objects.select_related('master').annotate(member_count=Count('members'))
table = tables.VirtualChassisTable
filter = filters.VirtualChassisFilter
@@ -2205,143 +2067,3 @@ class VirtualChassisRemoveMemberView(PermissionRequiredMixin, GetReturnURLMixin,
'form': form,
'return_url': self.get_return_url(request, device),
})
#
# Power panels
#
class PowerPanelListView(PermissionRequiredMixin, ObjectListView):
permission_required = 'dcim.view_powerpanel'
queryset = PowerPanel.objects.select_related(
'site', 'rack_group'
).annotate(
powerfeed_count=Count('powerfeeds')
)
filter = filters.PowerPanelFilter
filter_form = forms.PowerPanelFilterForm
table = tables.PowerPanelTable
template_name = 'dcim/powerpanel_list.html'
class PowerPanelView(PermissionRequiredMixin, View):
permission_required = 'dcim.view_powerpanel'
def get(self, request, pk):
powerpanel = get_object_or_404(PowerPanel.objects.select_related('site', 'rack_group'), pk=pk)
powerfeed_table = tables.PowerFeedTable(
data=PowerFeed.objects.filter(power_panel=powerpanel).select_related('rack'),
orderable=False
)
powerfeed_table.exclude = ['power_panel']
return render(request, 'dcim/powerpanel.html', {
'powerpanel': powerpanel,
'powerfeed_table': powerfeed_table,
})
class PowerPanelCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.add_powerpanel'
model = PowerPanel
model_form = forms.PowerPanelForm
default_return_url = 'dcim:powerpanel_list'
class PowerPanelEditView(PowerPanelCreateView):
permission_required = 'dcim.change_powerpanel'
class PowerPanelDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'dcim.delete_powerpanel'
model = PowerPanel
default_return_url = 'dcim:powerpanel_list'
class PowerPanelBulkImportView(PermissionRequiredMixin, BulkImportView):
permission_required = 'dcim.add_powerpanel'
model_form = forms.PowerPanelCSVForm
table = tables.PowerPanelTable
default_return_url = 'dcim:powerpanel_list'
class PowerPanelBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_powerpanel'
queryset = PowerPanel.objects.select_related(
'site', 'rack_group'
).annotate(
rack_count=Count('powerfeeds')
)
filter = filters.PowerPanelFilter
table = tables.PowerPanelTable
default_return_url = 'dcim:powerpanel_list'
#
# Power feeds
#
class PowerFeedListView(PermissionRequiredMixin, ObjectListView):
permission_required = 'dcim.view_powerfeed'
queryset = PowerFeed.objects.select_related(
'power_panel', 'rack'
)
filter = filters.PowerFeedFilter
filter_form = forms.PowerFeedFilterForm
table = tables.PowerFeedTable
template_name = 'dcim/powerfeed_list.html'
class PowerFeedView(PermissionRequiredMixin, View):
permission_required = 'dcim.view_powerfeed'
def get(self, request, pk):
powerfeed = get_object_or_404(PowerFeed.objects.select_related('power_panel', 'rack'), pk=pk)
return render(request, 'dcim/powerfeed.html', {
'powerfeed': powerfeed,
})
class PowerFeedCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.add_powerfeed'
model = PowerFeed
model_form = forms.PowerFeedForm
template_name = 'dcim/powerfeed_edit.html'
default_return_url = 'dcim:powerfeed_list'
class PowerFeedEditView(PowerFeedCreateView):
permission_required = 'dcim.change_powerfeed'
class PowerFeedDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'dcim.delete_powerfeed'
model = PowerFeed
default_return_url = 'dcim:powerfeed_list'
class PowerFeedBulkImportView(PermissionRequiredMixin, BulkImportView):
permission_required = 'dcim.add_powerfeed'
model_form = forms.PowerFeedCSVForm
table = tables.PowerFeedTable
default_return_url = 'dcim:powerfeed_list'
class PowerFeedBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.change_powerfeed'
queryset = PowerFeed.objects.select_related('power_panel', 'rack')
filter = filters.PowerFeedFilter
table = tables.PowerFeedTable
form = forms.PowerFeedBulkEditForm
default_return_url = 'dcim:powerfeed_list'
class PowerFeedBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_powerfeed'
queryset = PowerFeed.objects.select_related('power_panel', 'rack')
filter = filters.PowerFeedFilter
table = tables.PowerFeedTable
default_return_url = 'dcim:powerfeed_list'

View File

@@ -3,7 +3,7 @@ from django.contrib import admin
from netbox.admin import admin_site
from utilities.forms import LaxURLField
from .models import CustomField, CustomFieldChoice, CustomLink, Graph, ExportTemplate, TopologyMap, Webhook
from .models import CustomField, CustomFieldChoice, Graph, ExportTemplate, TopologyMap, Webhook
def order_content_types(field):
@@ -77,34 +77,6 @@ class CustomFieldAdmin(admin.ModelAdmin):
return ', '.join([ct.name for ct in obj.obj_type.all()])
#
# Custom links
#
class CustomLinkForm(forms.ModelForm):
class Meta:
model = CustomLink
exclude = []
help_texts = {
'text': 'Jinja2 template code for the link text. Reference the object as <code>{{ obj }}</code>.',
'url': 'Jinja2 template code for the link URL. Reference the object as <code>{{ obj }}</code>.',
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Format ContentType choices
order_content_types(self.fields['content_type'])
self.fields['content_type'].choices.insert(0, ('', '---------'))
@admin.register(CustomLink, site=admin_site)
class CustomLinkAdmin(admin.ModelAdmin):
list_display = ['name', 'content_type', 'group_name', 'weight']
form = CustomLinkForm
#
# Graphs
#

View File

@@ -1,7 +1,6 @@
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist
from drf_yasg.utils import swagger_serializer_method
from rest_framework import serializers
from taggit.models import Tag
from dcim.api.nested_serializers import (
NestedDeviceSerializer, NestedDeviceRoleSerializer, NestedPlatformSerializer, NestedRackSerializer,
@@ -11,14 +10,12 @@ from dcim.models import Device, DeviceRole, Platform, Rack, Region, Site
from extras.constants import *
from extras.models import (
ConfigContext, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, TopologyMap,
Tag
)
from tenancy.api.nested_serializers import NestedTenantSerializer, NestedTenantGroupSerializer
from tenancy.models import Tenant, TenantGroup
from users.api.nested_serializers import NestedUserSerializer
from utilities.api import (
ChoiceField, ContentTypeField, get_serializer_for_model, SerializerNotFound, SerializedPKRelatedField,
ValidatedModelSerializer,
ChoiceField, ContentTypeField, get_serializer_for_model, SerializedPKRelatedField, ValidatedModelSerializer,
)
from .nested_serializers import *
@@ -56,17 +53,10 @@ class RenderedGraphSerializer(serializers.ModelSerializer):
#
class ExportTemplateSerializer(ValidatedModelSerializer):
template_language = ChoiceField(
choices=TEMPLATE_LANGUAGE_CHOICES,
default=TEMPLATE_LANGUAGE_JINJA2
)
class Meta:
model = ExportTemplate
fields = [
'id', 'content_type', 'name', 'description', 'template_language', 'template_code', 'mime_type',
'file_extension',
]
fields = ['id', 'content_type', 'name', 'description', 'template_code', 'mime_type', 'file_extension']
#
@@ -90,7 +80,7 @@ class TagSerializer(ValidatedModelSerializer):
class Meta:
model = Tag
fields = ['id', 'name', 'slug', 'color', 'comments', 'tagged_items']
fields = ['id', 'name', 'slug', 'tagged_items']
#
@@ -98,9 +88,7 @@ class TagSerializer(ValidatedModelSerializer):
#
class ImageAttachmentSerializer(ValidatedModelSerializer):
content_type = ContentTypeField(
queryset=ContentType.objects.all()
)
content_type = ContentTypeField()
parent = serializers.SerializerMethodField(read_only=True)
class Meta:
@@ -124,7 +112,6 @@ class ImageAttachmentSerializer(ValidatedModelSerializer):
return data
@swagger_serializer_method(serializer_or_field=serializers.DictField)
def get_parent(self, obj):
# Static mapping of models to their nested serializers
@@ -218,42 +205,25 @@ class ReportDetailSerializer(ReportSerializer):
#
class ObjectChangeSerializer(serializers.ModelSerializer):
user = NestedUserSerializer(
read_only=True
)
action = ChoiceField(
choices=OBJECTCHANGE_ACTION_CHOICES,
read_only=True
)
changed_object_type = ContentTypeField(
read_only=True
)
changed_object = serializers.SerializerMethodField(
read_only=True
)
user = NestedUserSerializer(read_only=True)
content_type = ContentTypeField(read_only=True)
changed_object = serializers.SerializerMethodField(read_only=True)
class Meta:
model = ObjectChange
fields = [
'id', 'time', 'user', 'user_name', 'request_id', 'action', 'changed_object_type', 'changed_object',
'object_data',
'id', 'time', 'user', 'user_name', 'request_id', 'action', 'content_type', 'changed_object', 'object_data',
]
@swagger_serializer_method(serializer_or_field=serializers.DictField)
def get_changed_object(self, obj):
"""
Serialize a nested representation of the changed object.
"""
if obj.changed_object is None:
return None
try:
serializer = get_serializer_for_model(obj.changed_object, prefix='Nested')
except SerializerNotFound:
serializer = get_serializer_for_model(obj.changed_object, prefix='Nested')
if serializer is None:
return obj.object_repr
context = {
'request': self.context['request']
}
context = {'request': self.context['request']}
data = serializer(obj.changed_object, context=context).data
return data

View File

@@ -17,9 +17,6 @@ router.APIRootView = ExtrasRootView
# Field choices
router.register(r'_choices', views.ExtrasFieldChoicesViewSet, basename='field-choice')
# Custom field choices
router.register(r'_custom_field_choices', views.CustomFieldChoicesViewSet, base_name='custom-field-choice')
# Graphs
router.register(r'graphs', views.GraphViewSet)

View File

@@ -1,5 +1,3 @@
from collections import OrderedDict
from django.contrib.contenttypes.models import ContentType
from django.db.models import Count
from django.http import Http404, HttpResponse
@@ -8,11 +6,11 @@ from rest_framework.decorators import action
from rest_framework.exceptions import PermissionDenied
from rest_framework.response import Response
from rest_framework.viewsets import ReadOnlyModelViewSet, ViewSet
from taggit.models import Tag
from extras import filters
from extras.models import (
ConfigContext, CustomFieldChoice, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, TopologyMap,
Tag,
ConfigContext, CustomField, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, TopologyMap,
)
from extras.reports import get_report, get_reports
from utilities.api import FieldChoicesViewSet, IsAuthenticatedOrLoginNotRequired, ModelViewSet
@@ -25,42 +23,11 @@ from . import serializers
class ExtrasFieldChoicesViewSet(FieldChoicesViewSet):
fields = (
(ExportTemplate, ['template_language']),
(CustomField, ['type']),
(Graph, ['type']),
(ObjectChange, ['action']),
)
#
# Custom field choices
#
class CustomFieldChoicesViewSet(ViewSet):
"""
"""
permission_classes = [IsAuthenticatedOrLoginNotRequired]
def __init__(self, *args, **kwargs):
super(CustomFieldChoicesViewSet, self).__init__(*args, **kwargs)
self._fields = OrderedDict()
for cfc in CustomFieldChoice.objects.all():
self._fields.setdefault(cfc.field.name, {})
self._fields[cfc.field.name][cfc.value] = cfc.pk
def list(self, request):
return Response(self._fields)
def retrieve(self, request, pk):
if pk not in self._fields:
raise Http404
return Response(self._fields[pk])
def get_view_name(self):
return "Custom Field choices"
#
# Custom fields
#
@@ -132,9 +99,10 @@ class TopologyMapViewSet(ModelViewSet):
try:
data = tmap.render(img_format=img_format)
except Exception as e:
except Exception:
return HttpResponse(
"There was an error generating the requested graph: %s" % e
"There was an error generating the requested graph. Ensure that the GraphViz executables have been "
"installed correctly."
)
response = HttpResponse(data, content_type='image/{}'.format(img_format))
@@ -148,9 +116,7 @@ class TopologyMapViewSet(ModelViewSet):
#
class TagViewSet(ModelViewSet):
queryset = Tag.objects.annotate(
tagged_items=Count('extras_taggeditem_items', distinct=True)
)
queryset = Tag.objects.annotate(tagged_items=Count('taggit_taggeditem_items'))
serializer_class = serializers.TagSerializer
filterset_class = filters.TagFilter

View File

@@ -7,9 +7,6 @@ class ExtrasConfig(AppConfig):
name = "extras"
def ready(self):
import extras.signals
# Check that we can connect to the configured Redis database if webhooks are enabled.
if settings.WEBHOOKS_ENABLED:
try:
@@ -25,7 +22,6 @@ class ExtrasConfig(AppConfig):
port=settings.REDIS_PORT,
db=settings.REDIS_DATABASE,
password=settings.REDIS_PASSWORD or None,
ssl=settings.REDIS_SSL,
)
rs.ping()
except redis.exceptions.ConnectionError:

View File

@@ -1,24 +1,13 @@
# Models which support custom fields
CUSTOMFIELD_MODELS = [
'circuits.circuit',
'circuits.provider',
'dcim.device',
'dcim.devicetype',
'dcim.powerfeed',
'dcim.rack',
'dcim.site',
'ipam.aggregate',
'ipam.ipaddress',
'ipam.prefix',
'ipam.service',
'ipam.vlan',
'ipam.vrf',
'secrets.secret',
'tenancy.tenant',
'virtualization.cluster',
'virtualization.virtualmachine',
]
CUSTOMFIELD_MODELS = (
'provider', 'circuit', # Circuits
'site', 'rack', 'devicetype', 'device', # DCIM
'aggregate', 'prefix', 'ipaddress', 'vlan', 'vrf', 'service', # IPAM
'secret', # Secrets
'tenant', # Tenancy
'cluster', 'virtualmachine', # Virtualization
)
# Custom field types
CF_TYPE_TEXT = 100
@@ -46,46 +35,6 @@ CF_FILTER_CHOICES = (
(CF_FILTER_EXACT, 'Exact'),
)
# Custom links
CUSTOMLINK_MODELS = [
'circuits.circuit',
'circuits.provider',
'dcim.cable',
'dcim.device',
'dcim.devicetype',
'dcim.powerpanel',
'dcim.powerfeed',
'dcim.rack',
'dcim.site',
'ipam.aggregate',
'ipam.ipaddress',
'ipam.prefix',
'ipam.service',
'ipam.vlan',
'ipam.vrf',
'secrets.secret',
'tenancy.tenant',
'virtualization.cluster',
'virtualization.virtualmachine',
]
BUTTON_CLASS_DEFAULT = 'default'
BUTTON_CLASS_PRIMARY = 'primary'
BUTTON_CLASS_SUCCESS = 'success'
BUTTON_CLASS_INFO = 'info'
BUTTON_CLASS_WARNING = 'warning'
BUTTON_CLASS_DANGER = 'danger'
BUTTON_CLASS_LINK = 'link'
BUTTON_CLASS_CHOICES = (
(BUTTON_CLASS_DEFAULT, 'Default'),
(BUTTON_CLASS_PRIMARY, 'Primary (blue)'),
(BUTTON_CLASS_SUCCESS, 'Success (green)'),
(BUTTON_CLASS_INFO, 'Info (aqua)'),
(BUTTON_CLASS_WARNING, 'Warning (orange)'),
(BUTTON_CLASS_DANGER, 'Danger (red)'),
(BUTTON_CLASS_LINK, 'None (link)'),
)
# Graph types
GRAPH_TYPE_INTERFACE = 100
GRAPH_TYPE_PROVIDER = 200
@@ -98,42 +47,15 @@ GRAPH_TYPE_CHOICES = (
# Models which support export templates
EXPORTTEMPLATE_MODELS = [
'circuits.circuit',
'circuits.provider',
'dcim.cable',
'dcim.consoleport',
'dcim.device',
'dcim.devicetype',
'dcim.interface',
'dcim.manufacturer',
'dcim.powerpanel',
'dcim.powerport',
'dcim.powerfeed',
'dcim.rack',
'dcim.rackgroup',
'dcim.region',
'dcim.site',
'dcim.virtualchassis',
'ipam.aggregate',
'ipam.ipaddress',
'ipam.prefix',
'ipam.service',
'ipam.vlan',
'ipam.vrf',
'secrets.secret',
'tenancy.tenant',
'virtualization.cluster',
'virtualization.virtualmachine',
'provider', 'circuit', # Circuits
'site', 'region', 'rack', 'rackgroup', 'manufacturer', 'devicetype', 'device', # DCIM
'consoleport', 'powerport', 'interface', 'cable', 'virtualchassis', # DCIM
'aggregate', 'prefix', 'ipaddress', 'vlan', 'vrf', 'service', # IPAM
'secret', # Secrets
'tenant', # Tenancy
'cluster', 'virtualmachine', # Virtualization
]
# ExportTemplate language choices
TEMPLATE_LANGUAGE_DJANGO = 10
TEMPLATE_LANGUAGE_JINJA2 = 20
TEMPLATE_LANGUAGE_CHOICES = (
(TEMPLATE_LANGUAGE_DJANGO, 'Django'),
(TEMPLATE_LANGUAGE_JINJA2, 'Jinja2'),
)
# Topology map types
TOPOLOGYMAP_TYPE_NETWORK = 1
TOPOLOGYMAP_TYPE_CONSOLE = 2
@@ -195,36 +117,13 @@ WEBHOOK_CT_CHOICES = (
)
# Models which support registered webhooks
WEBHOOK_MODELS = [
'circuits.circuit',
'circuits.provider',
'dcim.cable',
'dcim.consoleport',
'dcim.consoleserverport',
'dcim.device',
'dcim.devicebay',
'dcim.devicetype',
'dcim.interface',
'dcim.inventoryitem',
'dcim.frontport',
'dcim.manufacturer',
'dcim.poweroutlet',
'dcim.powerpanel',
'dcim.powerport',
'dcim.powerfeed',
'dcim.rack',
'dcim.rearport',
'dcim.region',
'dcim.site',
'dcim.virtualchassis',
'ipam.aggregate',
'ipam.ipaddress',
'ipam.prefix',
'ipam.service',
'ipam.vlan',
'ipam.vrf',
'secrets.secret',
'tenancy.tenant',
'virtualization.cluster',
'virtualization.virtualmachine',
]
WEBHOOK_MODELS = (
'provider', 'circuit', # Circuits
'site', 'rack', 'devicetype', 'device', 'virtualchassis', # DCIM
'consoleport', 'consoleserverport', 'powerport', 'poweroutlet',
'interface', 'devicebay', 'inventoryitem',
'aggregate', 'prefix', 'ipaddress', 'vlan', 'vrf', 'service', # IPAM
'secret', # Secrets
'tenant', # Tenancy
'cluster', 'virtualmachine', # Virtualization
)

View File

@@ -1,11 +1,12 @@
import django_filters
from django.contrib.contenttypes.models import ContentType
from django.db.models import Q
from taggit.models import Tag
from dcim.models import DeviceRole, Platform, Region, Site
from tenancy.models import Tenant, TenantGroup
from .constants import CF_FILTER_DISABLED, CF_FILTER_EXACT, CF_TYPE_BOOLEAN, CF_TYPE_SELECT
from .models import ConfigContext, CustomField, Graph, ExportTemplate, ObjectChange, Tag, TopologyMap
from .models import ConfigContext, CustomField, Graph, ExportTemplate, ObjectChange, TopologyMap
class CustomFieldFilter(django_filters.Filter):
@@ -81,7 +82,7 @@ class ExportTemplateFilter(django_filters.FilterSet):
class Meta:
model = ExportTemplate
fields = ['content_type', 'name', 'template_language']
fields = ['content_type', 'name']
class TagFilter(django_filters.FilterSet):

View File

@@ -4,19 +4,21 @@ from django import forms
from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist
from mptt.forms import TreeNodeMultipleChoiceField
from taggit.forms import TagField
from taggit.models import Tag
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, CommentField,
ContentTypeSelect, FilterChoiceField, LaxURLField, JSONField, SlugField,
add_blank_choice, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ContentTypeSelect, FilterChoiceField,
FilterTreeNodeMultipleChoiceField, LaxURLField, JSONField, SlugField,
)
from .constants import (
CF_FILTER_DISABLED, CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT, CF_TYPE_URL,
OBJECTCHANGE_ACTION_CHOICES,
)
from .models import ConfigContext, CustomField, CustomFieldValue, ImageAttachment, ObjectChange, Tag
from .models import ConfigContext, CustomField, CustomFieldValue, ImageAttachment, ObjectChange
#
@@ -188,12 +190,11 @@ class CustomFieldFilterForm(forms.Form):
class TagForm(BootstrapMixin, forms.ModelForm):
slug = SlugField()
comments = CommentField()
class Meta:
model = Tag
fields = [
'name', 'slug', 'color', 'comments'
'name', 'slug',
]
@@ -220,6 +221,10 @@ class TagFilterForm(BootstrapMixin, forms.Form):
#
class ConfigContextForm(BootstrapMixin, forms.ModelForm):
regions = TreeNodeMultipleChoiceField(
queryset=Region.objects.all(),
required=False
)
data = JSONField()
class Meta:
@@ -228,26 +233,6 @@ class ConfigContextForm(BootstrapMixin, forms.ModelForm):
'name', 'weight', 'description', 'is_active', 'regions', 'sites', 'roles', 'platforms', 'tenant_groups',
'tenants', 'data',
]
widgets = {
'regions': APISelectMultiple(
api_url="/api/dcim/regions/"
),
'sites': APISelectMultiple(
api_url="/api/dcim/sites/"
),
'roles': APISelectMultiple(
api_url="/api/dcim/device-roles/"
),
'platforms': APISelectMultiple(
api_url="/api/dcim/platforms/"
),
'tenant_groups': APISelectMultiple(
api_url="/api/tenancy/tenant-groups/"
),
'tenants': APISelectMultiple(
api_url="/api/tenancy/tenants/"
)
}
class ConfigContextBulkEditForm(BootstrapMixin, BulkEditForm):
@@ -279,53 +264,29 @@ class ConfigContextFilterForm(BootstrapMixin, forms.Form):
required=False,
label='Search'
)
region = FilterChoiceField(
region = FilterTreeNodeMultipleChoiceField(
queryset=Region.objects.all(),
to_field_name='slug',
widget=APISelectMultiple(
api_url="/api/dcim/regions/",
value_field="slug",
)
to_field_name='slug'
)
site = FilterChoiceField(
queryset=Site.objects.all(),
to_field_name='slug',
widget=APISelectMultiple(
api_url="/api/dcim/sites/",
value_field="slug",
)
to_field_name='slug'
)
role = FilterChoiceField(
queryset=DeviceRole.objects.all(),
to_field_name='slug',
widget=APISelectMultiple(
api_url="/api/dcim/device-roles/",
value_field="slug",
)
to_field_name='slug'
)
platform = FilterChoiceField(
queryset=Platform.objects.all(),
to_field_name='slug',
widget=APISelectMultiple(
api_url="/api/dcim/platforms/",
value_field="slug",
)
to_field_name='slug'
)
tenant_group = FilterChoiceField(
queryset=TenantGroup.objects.all(),
to_field_name='slug',
widget=APISelectMultiple(
api_url="/api/tenancy/tenant-groups/",
value_field="slug",
)
to_field_name='slug'
)
tenant = FilterChoiceField(
queryset=Tenant.objects.all(),
to_field_name='slug',
widget=APISelectMultiple(
api_url="/api/tenancy/tenants/",
value_field="slug",
)
to_field_name='slug'
)

View File

@@ -6,6 +6,7 @@ from django import get_version
from django.apps import apps
from django.conf import settings
from django.core.management.base import BaseCommand
from django.db.models import Model
APPS = ['circuits', 'dcim', 'extras', 'ipam', 'secrets', 'tenancy', 'users', 'virtualization']
@@ -37,10 +38,16 @@ class Command(BaseCommand):
for app in APPS:
self.django_models[app] = []
# Load models from each app
for model in apps.get_app_config(app).get_models():
namespace[model.__name__] = model
self.django_models[app].append(model.__name__)
# Models
app_models = sys.modules['{}.models'.format(app)]
for name in dir(app_models):
model = getattr(app_models, name)
try:
if issubclass(model, Model) and model._meta.app_label == app:
namespace[name] = model
self.django_models[app].append(name)
except TypeError:
pass
# Constants
try:

View File

@@ -7,7 +7,6 @@ from django.conf import settings
from django.db.models.signals import post_delete, post_save
from django.utils import timezone
from django.utils.functional import curry
from django_prometheus.models import model_deletes, model_inserts, model_updates
from extras.webhooks import enqueue_webhooks
from .constants import (
@@ -30,28 +29,19 @@ def cache_changed_object(instance, **kwargs):
def _record_object_deleted(request, instance, **kwargs):
# Force resolution of request.user in case it's still a SimpleLazyObject. This seems to happen
# occasionally during tests, but haven't been able to determine why.
assert request.user.is_authenticated
# Record that the object was deleted
# Record that the object was deleted.
if hasattr(instance, 'log_change'):
instance.log_change(request.user, request.id, OBJECTCHANGE_ACTION_DELETE)
# Enqueue webhooks
enqueue_webhooks(instance, request.user, request.id, OBJECTCHANGE_ACTION_DELETE)
# Increment metric counters
model_deletes.labels(instance._meta.model_name).inc()
enqueue_webhooks(instance, OBJECTCHANGE_ACTION_DELETE)
class ObjectChangeMiddleware(object):
"""
This middleware performs three functions in response to an object being created, updated, or deleted:
This middleware performs two functions in response to an object being created, updated, or deleted:
1. Create an ObjectChange to reflect the modification to the object in the changelog.
2. Enqueue any relevant webhooks.
3. Increment metric counter for the event type
The post_save and pre_delete signals are employed to catch object modifications, however changes are recorded a bit
differently for each. Objects being saved are cached into thread-local storage for action *after* the response has
@@ -89,13 +79,7 @@ class ObjectChangeMiddleware(object):
obj.log_change(request.user, request.id, action)
# Enqueue webhooks
enqueue_webhooks(obj, request.user, request.id, action)
# Increment metric counters
if action == OBJECTCHANGE_ACTION_CREATE:
model_inserts.labels(obj._meta.model_name).inc()
elif action == OBJECTCHANGE_ACTION_UPDATE:
model_updates.labels(obj._meta.model_name).inc()
enqueue_webhooks(obj, action)
# Housekeeping: 1% chance of clearing out expired ObjectChanges
if _thread_locals.changed_objects and settings.CHANGELOG_RETENTION and random.randint(1, 100) == 1:

View File

@@ -1,18 +0,0 @@
# Generated by Django 2.1.7 on 2019-03-05 18:07
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('extras', '0016_exporttemplate_add_cable'),
]
operations = [
migrations.AlterField(
model_name='exporttemplate',
name='mime_type',
field=models.CharField(blank=True, max_length=50),
),
]

View File

@@ -1,27 +0,0 @@
# Generated by Django 2.1.7 on 2019-04-08 14:49
from django.db import migrations, models
def set_template_language(apps, schema_editor):
"""
Set the language for all existing ExportTemplates to Django (Jinja2 is the default for new ExportTemplates).
"""
ExportTemplate = apps.get_model('extras', 'ExportTemplate')
ExportTemplate.objects.update(template_language=10)
class Migration(migrations.Migration):
dependencies = [
('extras', '0017_exporttemplate_mime_type_length'),
]
operations = [
migrations.AddField(
model_name='exporttemplate',
name='template_language',
field=models.PositiveSmallIntegerField(default=20),
),
migrations.RunPython(set_template_language),
]

View File

@@ -1,43 +0,0 @@
# Generated by Django 2.1.4 on 2019-02-20 06:56
from django.db import migrations, models
import django.db.models.deletion
import utilities.fields
class Migration(migrations.Migration):
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
('extras', '0018_exporttemplate_add_jinja2'),
]
operations = [
migrations.CreateModel(
name='Tag',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
('name', models.CharField(max_length=100, unique=True)),
('slug', models.SlugField(max_length=100, unique=True)),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='TaggedItem',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
('object_id', models.IntegerField(db_index=True)),
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='extras_taggeditem_tagged_items', to='contenttypes.ContentType')),
('tag', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='extras_taggeditem_items', to='extras.Tag')),
],
options={
'abstract': False,
},
),
migrations.AlterIndexTogether(
name='taggeditem',
index_together={('content_type', 'object_id')},
),
]

View File

@@ -1,65 +0,0 @@
# Generated by Django 2.1.4 on 2019-02-20 06:56
from django.db import migrations, models
import django.db.models.deletion
import utilities.fields
def copy_tags(apps, schema_editor):
"""
Copy data from taggit_tag to extras_tag
"""
TaggitTag = apps.get_model('taggit', 'Tag')
ExtrasTag = apps.get_model('extras', 'Tag')
tags_values = TaggitTag.objects.all().values('id', 'name', 'slug')
tags = [ExtrasTag(**tag) for tag in tags_values]
ExtrasTag.objects.bulk_create(tags)
def copy_taggeditems(apps, schema_editor):
"""
Copy data from taggit_taggeditem to extras_taggeditem
"""
TaggitTaggedItem = apps.get_model('taggit', 'TaggedItem')
ExtrasTaggedItem = apps.get_model('extras', 'TaggedItem')
tagged_items_values = TaggitTaggedItem.objects.all().values('id', 'object_id', 'content_type_id', 'tag_id')
tagged_items = [ExtrasTaggedItem(**tagged_item) for tagged_item in tagged_items_values]
ExtrasTaggedItem.objects.bulk_create(tagged_items)
def delete_taggit_taggeditems(apps, schema_editor):
"""
Delete all TaggedItem instances from taggit_taggeditem
"""
TaggitTaggedItem = apps.get_model('taggit', 'TaggedItem')
TaggitTaggedItem.objects.all().delete()
def delete_taggit_tags(apps, schema_editor):
"""
Delete all Tag instances from taggit_tag
"""
TaggitTag = apps.get_model('taggit', 'Tag')
TaggitTag.objects.all().delete()
class Migration(migrations.Migration):
dependencies = [
('extras', '0019_tag_taggeditem'),
('circuits', '0015_custom_tag_models'),
('dcim', '0070_custom_tag_models'),
('ipam', '0025_custom_tag_models'),
('secrets', '0006_custom_tag_models'),
('tenancy', '0006_custom_tag_models'),
('virtualization', '0009_custom_tag_models'),
]
operations = [
migrations.RunPython(copy_tags),
migrations.RunPython(copy_taggeditems),
migrations.RunPython(delete_taggit_taggeditems),
migrations.RunPython(delete_taggit_tags),
]

View File

@@ -1,34 +0,0 @@
# Generated by Django 2.1.4 on 2019-02-20 07:38
from django.db import migrations, models
import utilities.fields
class Migration(migrations.Migration):
dependencies = [
('extras', '0020_tag_data'),
]
operations = [
migrations.AddField(
model_name='tag',
name='color',
field=utilities.fields.ColorField(max_length=6, default='9e9e9e'),
),
migrations.AddField(
model_name='tag',
name='comments',
field=models.TextField(blank=True, default=''),
),
migrations.AddField(
model_name='tag',
name='created',
field=models.DateField(auto_now_add=True, null=True),
),
migrations.AddField(
model_name='tag',
name='last_updated',
field=models.DateTimeField(auto_now=True, null=True),
),
]

View File

@@ -1,48 +0,0 @@
from django.db import migrations, models
import django.db.models.deletion
import extras.models
class Migration(migrations.Migration):
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
('extras', '0021_add_color_comments_changelog_to_tag'),
]
operations = [
migrations.CreateModel(
name='CustomLink',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
('name', models.CharField(max_length=100, unique=True)),
('text', models.CharField(max_length=500)),
('url', models.CharField(max_length=500)),
('weight', models.PositiveSmallIntegerField(default=100)),
('group_name', models.CharField(blank=True, max_length=50)),
('button_class', models.CharField(default='default', max_length=30)),
('new_window', models.BooleanField()),
('content_type', models.ForeignKey(limit_choices_to=extras.models.get_custom_link_models, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')),
],
options={
'ordering': ['group_name', 'weight', 'name'],
},
),
# Update limit_choices_to for CustomFields, ExportTemplates, and Webhooks
migrations.AlterField(
model_name='customfield',
name='obj_type',
field=models.ManyToManyField(limit_choices_to=extras.models.get_custom_field_models, related_name='custom_fields', to='contenttypes.ContentType'),
),
migrations.AlterField(
model_name='exporttemplate',
name='content_type',
field=models.ForeignKey(limit_choices_to=extras.models.get_export_template_models, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType'),
),
migrations.AlterField(
model_name='webhook',
name='obj_type',
field=models.ManyToManyField(limit_choices_to=extras.models.get_webhook_models, related_name='webhooks', to='contenttypes.ContentType'),
),
]

View File

@@ -1,6 +1,7 @@
from collections import OrderedDict
from datetime import date
import graphviz
from django.contrib.auth.models import User
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
@@ -11,13 +12,9 @@ from django.db.models import F, Q
from django.http import HttpResponse
from django.template import Template, Context
from django.urls import reverse
import graphviz
from jinja2 import Environment
from taggit.models import TagBase, GenericTaggedItemBase
from dcim.constants import CONNECTION_STATUS_CONNECTED
from utilities.fields import ColorField
from utilities.utils import deepmerge, foreground_color, model_names_to_filter_dict
from utilities.utils import deepmerge, foreground_color
from .constants import *
from .querysets import ConfigContextQuerySet
@@ -26,10 +23,6 @@ from .querysets import ConfigContextQuerySet
# Webhooks
#
def get_webhook_models():
return model_names_to_filter_dict(WEBHOOK_MODELS)
class Webhook(models.Model):
"""
A Webhook defines a request that will be sent to a remote application when an object is created, updated, and/or
@@ -41,7 +34,7 @@ class Webhook(models.Model):
to=ContentType,
related_name='webhooks',
verbose_name='Object types',
limit_choices_to=get_webhook_models,
limit_choices_to={'model__in': WEBHOOK_MODELS},
help_text="The object(s) to which this Webhook applies."
)
name = models.CharField(
@@ -138,16 +131,12 @@ class CustomFieldModel(models.Model):
return OrderedDict([(field, None) for field in fields])
def get_custom_field_models():
return model_names_to_filter_dict(CUSTOMFIELD_MODELS)
class CustomField(models.Model):
obj_type = models.ManyToManyField(
to=ContentType,
related_name='custom_fields',
verbose_name='Object(s)',
limit_choices_to=get_custom_field_models,
limit_choices_to={'model__in': CUSTOMFIELD_MODELS},
help_text='The object(s) to which this field applies.'
)
type = models.PositiveSmallIntegerField(
@@ -308,62 +297,6 @@ class CustomFieldChoice(models.Model):
CustomFieldValue.objects.filter(field__type=CF_TYPE_SELECT, serialized_value=str(pk)).delete()
#
# Custom links
#
def get_custom_link_models():
return model_names_to_filter_dict(CUSTOMLINK_MODELS)
class CustomLink(models.Model):
"""
A custom link to an external representation of a NetBox object. The link text and URL fields accept Jinja2 template
code to be rendered with an object as context.
"""
content_type = models.ForeignKey(
to=ContentType,
on_delete=models.CASCADE,
limit_choices_to=get_custom_link_models
)
name = models.CharField(
max_length=100,
unique=True
)
text = models.CharField(
max_length=500,
help_text="Jinja2 template code for link text"
)
url = models.CharField(
max_length=500,
verbose_name='URL',
help_text="Jinja2 template code for link URL"
)
weight = models.PositiveSmallIntegerField(
default=100
)
group_name = models.CharField(
max_length=50,
blank=True,
help_text="Links with the same group will appear as a dropdown menu"
)
button_class = models.CharField(
max_length=30,
choices=BUTTON_CLASS_CHOICES,
default=BUTTON_CLASS_DEFAULT,
help_text="The class of the first link in a group will be used for the dropdown button"
)
new_window = models.BooleanField(
help_text="Force link to open in a new window"
)
class Meta:
ordering = ['group_name', 'weight', 'name']
def __str__(self):
return self.name
#
# Graphs
#
@@ -409,15 +342,11 @@ class Graph(models.Model):
# Export templates
#
def get_export_template_models():
return model_names_to_filter_dict(EXPORTTEMPLATE_MODELS)
class ExportTemplate(models.Model):
content_type = models.ForeignKey(
to=ContentType,
on_delete=models.CASCADE,
limit_choices_to=get_export_template_models
limit_choices_to={'model__in': EXPORTTEMPLATE_MODELS}
)
name = models.CharField(
max_length=100
@@ -426,13 +355,9 @@ class ExportTemplate(models.Model):
max_length=200,
blank=True
)
template_language = models.PositiveSmallIntegerField(
choices=TEMPLATE_LANGUAGE_CHOICES,
default=TEMPLATE_LANGUAGE_JINJA2
)
template_code = models.TextField()
mime_type = models.CharField(
max_length=50,
max_length=15,
blank=True
)
file_extension = models.CharField(
@@ -449,36 +374,16 @@ class ExportTemplate(models.Model):
def __str__(self):
return '{}: {}'.format(self.content_type, self.name)
def render(self, queryset):
"""
Render the contents of the template.
"""
context = {
'queryset': queryset
}
if self.template_language == TEMPLATE_LANGUAGE_DJANGO:
template = Template(self.template_code)
output = template.render(Context(context))
elif self.template_language == TEMPLATE_LANGUAGE_JINJA2:
template = Environment().from_string(source=self.template_code)
output = template.render(**context)
else:
return None
# Replace CRLF-style line terminators
output = output.replace('\r\n', '\n')
return output
def render_to_response(self, queryset):
"""
Render the template to an HTTP response, delivered as a named file attachment
"""
output = self.render(queryset)
template = Template(self.template_code)
mime_type = 'text/plain' if not self.mime_type else self.mime_type
output = template.render(Context({'queryset': queryset}))
# Replace CRLF-style line terminators
output = output.replace('\r\n', '\n')
# Build the response
response = HttpResponse(output, content_type=mime_type)
@@ -634,7 +539,7 @@ class TopologyMap(models.Model):
from dcim.models import PowerPort
# Add all power connections to the graph
for pp in PowerPort.objects.filter(device__in=devices, _connected_poweroutlet__device__in=devices):
for pp in PowerPort.objects.filter(device__in=devices, connected_endpoint__device__in=devices):
style = 'solid' if pp.connection_status == CONNECTION_STATUS_CONNECTED else 'dashed'
self.graph.edge(pp.connected_endpoint.device.name, pp.device.name, style=style)
@@ -815,7 +720,7 @@ class ConfigContextModel(models.Model):
data = deepmerge(data, context.data)
# If the object has local config context data defined, merge it last
if self.local_context_data:
if self.local_context_data is not None:
data = deepmerge(data, self.local_context_data)
return data
@@ -955,37 +860,3 @@ class ObjectChange(models.Model):
self.object_repr,
self.object_data,
)
#
# Tags
#
# TODO: figure out a way around this circular import for ObjectChange
from utilities.models import ChangeLoggedModel # noqa: E402
class Tag(TagBase, ChangeLoggedModel):
color = ColorField(
default='9e9e9e'
)
comments = models.TextField(
blank=True,
default=''
)
def get_absolute_url(self):
return reverse('extras:tag', args=[self.slug])
class TaggedItem(GenericTaggedItemBase):
tag = models.ForeignKey(
to=Tag,
related_name="%(app_label)s_%(class)s_items",
on_delete=models.CASCADE
)
class Meta:
index_together = (
("content_type", "object_id")
)

View File

@@ -1,22 +0,0 @@
from cacheops.signals import cache_invalidated, cache_read
from prometheus_client import Counter
cacheops_cache_hit = Counter('cacheops_cache_hit', 'Number of cache hits')
cacheops_cache_miss = Counter('cacheops_cache_miss', 'Number of cache misses')
cacheops_cache_invalidated = Counter('cacheops_cache_invalidated', 'Number of cache invalidations')
def cache_read_collector(sender, func, hit, **kwargs):
if hit:
cacheops_cache_hit.inc()
else:
cacheops_cache_miss.inc()
def cache_invalidated_collector(sender, obj_dict, **kwargs):
cacheops_cache_invalidated.inc()
cache_read.connect(cache_read_collector)
cache_invalidated.connect(cache_invalidated_collector)

View File

@@ -1,13 +1,11 @@
import django_tables2 as tables
from django_tables2.utils import Accessor
from taggit.models import Tag, TaggedItem
from utilities.tables import BaseTable, BooleanColumn, ColorColumn, ToggleColumn
from .models import ConfigContext, ObjectChange, Tag, TaggedItem
from utilities.tables import BaseTable, BooleanColumn, ToggleColumn
from .models import ConfigContext, ObjectChange
TAG_ACTIONS = """
<a href="{% url 'extras:tag_changelog' slug=record.slug %}" class="btn btn-default btn-xs" title="Changelog">
<i class="fa fa-history"></i>
</a>
{% if perms.taggit.change_tag %}
<a href="{% url 'extras:tag_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
{% endif %}
@@ -70,14 +68,13 @@ class TagTable(BaseTable):
)
actions = tables.TemplateColumn(
template_code=TAG_ACTIONS,
attrs={'td': {'class': 'text-right noprint'}},
attrs={'td': {'class': 'text-right'}},
verbose_name=''
)
color = ColorColumn()
class Meta(BaseTable.Meta):
model = Tag
fields = ('pk', 'name', 'items', 'slug', 'color', 'actions')
fields = ('pk', 'name', 'items', 'slug', 'actions')
class TaggedItemTable(BaseTable):

View File

@@ -1,68 +0,0 @@
from collections import OrderedDict
from django import template
from django.contrib.contenttypes.models import ContentType
from django.utils.safestring import mark_safe
from jinja2 import Environment
from extras.models import CustomLink
register = template.Library()
LINK_BUTTON = '<a href="{}"{} class="btn btn-sm btn-{}">{}</a>\n'
GROUP_BUTTON = '<div class="btn-group">\n' \
'<button type="button" class="btn btn-sm btn-{} dropdown-toggle" data-toggle="dropdown">\n' \
'{} <span class="caret"></span>\n' \
'</button>\n' \
'<ul class="dropdown-menu pull-right">\n'
GROUP_LINK = '<li><a href="{}"{}>{}</a></li>\n'
@register.simple_tag()
def custom_links(obj):
"""
Render all applicable links for the given object.
"""
content_type = ContentType.objects.get_for_model(obj)
custom_links = CustomLink.objects.filter(content_type=content_type)
if not custom_links:
return ''
context = {
'obj': obj,
}
template_code = ''
group_names = OrderedDict()
# Organize custom links by group
for cl in custom_links:
if cl.group_name and cl.group_name in group_names:
group_names[cl.group_name].append(cl)
elif cl.group_name:
group_names[cl.group_name] = [cl]
# Add non-grouped links
for cl in custom_links:
if not cl.group_name:
link_target = ' target="_blank"' if cl.new_window else ''
template_code += LINK_BUTTON.format(
cl.url, link_target, cl.button_class, cl.text
)
# Add grouped links to template
for group, links in group_names.items():
template_code += GROUP_BUTTON.format(
links[0].button_class, group
)
for cl in links:
link_target = ' target="_blank"' if cl.new_window else ''
template_code += GROUP_LINK.format(
cl.url, link_target, cl.text
)
template_code += '</ul>\n</div>\n'
# Render template
rendered = Environment().from_string(source=template_code).render(**context)
return mark_safe(rendered)

View File

@@ -1,10 +1,11 @@
from django.contrib.contenttypes.models import ContentType
from django.urls import reverse
from rest_framework import status
from taggit.models import Tag
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Platform, Region, Site
from extras.constants import GRAPH_TYPE_SITE
from extras.models import ConfigContext, Graph, ExportTemplate, Tag
from extras.models import ConfigContext, Graph, ExportTemplate
from tenancy.models import Tenant, TenantGroup
from utilities.testing import APITestCase

View File

@@ -6,10 +6,9 @@ from django.urls import reverse
from rest_framework import status
from dcim.models import Site
from extras.constants import CF_TYPE_TEXT, CF_TYPE_INTEGER, CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_SELECT, CF_TYPE_URL, CF_TYPE_SELECT
from extras.constants import CF_TYPE_TEXT, CF_TYPE_INTEGER, CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_SELECT, CF_TYPE_URL
from extras.models import CustomField, CustomFieldValue, CustomFieldChoice
from utilities.testing import APITestCase
from virtualization.models import VirtualMachine
class CustomFieldTest(TestCase):
@@ -300,33 +299,3 @@ class CustomFieldAPITest(APITestCase):
self.assertEqual(response.data['custom_fields'].get('magic_choice'), data['custom_fields']['magic_choice'])
cfv = self.site.custom_field_values.get(field=self.cf_select)
self.assertEqual(cfv.value.pk, data['custom_fields']['magic_choice'])
class CustomFieldChoiceAPITest(APITestCase):
def setUp(self):
super().setUp()
vm_content_type = ContentType.objects.get_for_model(VirtualMachine)
self.cf_1 = CustomField.objects.create(name="cf_1", type=CF_TYPE_SELECT)
self.cf_2 = CustomField.objects.create(name="cf_2", type=CF_TYPE_SELECT)
self.cf_choice_1 = CustomFieldChoice.objects.create(field=self.cf_1, value="cf_field_1", weight=100)
self.cf_choice_2 = CustomFieldChoice.objects.create(field=self.cf_1, value="cf_field_2", weight=50)
self.cf_choice_3 = CustomFieldChoice.objects.create(field=self.cf_2, value="cf_field_3", weight=10)
def test_list_cfc(self):
url = reverse('extras-api:custom-field-choice-list')
response = self.client.get(url, **self.header)
self.assertEqual(len(response.data), 2)
self.assertEqual(len(response.data[self.cf_1.name]), 2)
self.assertEqual(len(response.data[self.cf_2.name]), 1)
self.assertTrue(self.cf_choice_1.value in response.data[self.cf_1.name])
self.assertTrue(self.cf_choice_2.value in response.data[self.cf_1.name])
self.assertTrue(self.cf_choice_3.value in response.data[self.cf_2.name])
self.assertEqual(self.cf_choice_1.pk, response.data[self.cf_1.name][self.cf_choice_1.value])
self.assertEqual(self.cf_choice_2.pk, response.data[self.cf_1.name][self.cf_choice_2.value])
self.assertEqual(self.cf_choice_3.pk, response.data[self.cf_2.name][self.cf_choice_3.value])

View File

@@ -1,104 +0,0 @@
import urllib.parse
import uuid
from django.contrib.auth.models import User
from django.test import Client, TestCase
from django.urls import reverse
from dcim.models import Site
from extras.models import ConfigContext, ObjectChange, Tag
from utilities.testing import create_test_user
class TagTestCase(TestCase):
def setUp(self):
self.client = Client()
Tag.objects.bulk_create([
Tag(name='Tag 1', slug='tag-1'),
Tag(name='Tag 2', slug='tag-2'),
Tag(name='Tag 3', slug='tag-3'),
])
def test_tag_list(self):
url = reverse('extras:tag_list')
params = {
"q": "tag",
}
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
self.assertEqual(response.status_code, 200)
class ConfigContextTestCase(TestCase):
def setUp(self):
user = create_test_user(permissions=['extras.view_configcontext'])
self.client = Client()
self.client.force_login(user)
site = Site(name='Site 1', slug='site-1')
site.save()
# Create three ConfigContexts
for i in range(1, 4):
configcontext = ConfigContext(
name='Config Context {}'.format(i),
data='{{"foo": {}}}'.format(i)
)
configcontext.save()
configcontext.sites.add(site)
def test_configcontext_list(self):
url = reverse('extras:configcontext_list')
params = {
"q": "foo",
}
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
self.assertEqual(response.status_code, 200)
def test_configcontext(self):
configcontext = ConfigContext.objects.first()
response = self.client.get(configcontext.get_absolute_url())
self.assertEqual(response.status_code, 200)
class ObjectChangeTestCase(TestCase):
def setUp(self):
user = create_test_user(permissions=['extras.view_objectchange'])
self.client = Client()
self.client.force_login(user)
site = Site(name='Site 1', slug='site-1')
site.save()
# Create three ObjectChanges
for i in range(1, 4):
site.log_change(
user=user,
request_id=uuid.uuid4(),
action=2
)
def test_objectchange_list(self):
url = reverse('extras:objectchange_list')
params = {
"user": User.objects.first(),
}
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
self.assertEqual(response.status_code, 200)
def test_objectchange(self):
objectchange = ObjectChange.objects.first()
response = self.client.get(objectchange.get_absolute_url())
self.assertEqual(response.status_code, 200)

View File

@@ -1,28 +1,25 @@
from django.conf.urls import url
from extras import views
from extras.models import Tag
app_name = 'extras'
urlpatterns = [
# Tags
url(r'^tags/$', views.TagListView.as_view(), name='tag_list'),
url(r'^tags/delete/$', views.TagBulkDeleteView.as_view(), name='tag_bulk_delete'),
url(r'^tags/(?P<slug>[\w-]+)/$', views.TagView.as_view(), name='tag'),
url(r'^tags/(?P<slug>[\w-]+)/edit/$', views.TagEditView.as_view(), name='tag_edit'),
url(r'^tags/(?P<slug>[\w-]+)/delete/$', views.TagDeleteView.as_view(), name='tag_delete'),
url(r'^tags/(?P<slug>[\w-]+)/changelog/$', views.ObjectChangeLogView.as_view(), name='tag_changelog', kwargs={'model': Tag}),
url(r'^tags/delete/$', views.TagBulkDeleteView.as_view(), name='tag_bulk_delete'),
# Config contexts
url(r'^config-contexts/$', views.ConfigContextListView.as_view(), name='configcontext_list'),
url(r'^config-contexts/add/$', views.ConfigContextCreateView.as_view(), name='configcontext_add'),
url(r'^config-contexts/edit/$', views.ConfigContextBulkEditView.as_view(), name='configcontext_bulk_edit'),
url(r'^config-contexts/delete/$', views.ConfigContextBulkDeleteView.as_view(), name='configcontext_bulk_delete'),
url(r'^config-contexts/(?P<pk>\d+)/$', views.ConfigContextView.as_view(), name='configcontext'),
url(r'^config-contexts/(?P<pk>\d+)/edit/$', views.ConfigContextEditView.as_view(), name='configcontext_edit'),
url(r'^config-contexts/(?P<pk>\d+)/delete/$', views.ConfigContextDeleteView.as_view(), name='configcontext_delete'),
url(r'^config-contexts/delete/$', views.ConfigContextBulkDeleteView.as_view(), name='configcontext_bulk_delete'),
# Image attachments
url(r'^image-attachments/(?P<pk>\d+)/edit/$', views.ImageAttachmentEditView.as_view(), name='imageattachment_edit'),

View File

@@ -9,6 +9,7 @@ from django.shortcuts import get_object_or_404, redirect, render
from django.utils.safestring import mark_safe
from django.views.generic import View
from django_tables2 import RequestConfig
from taggit.models import Tag, TaggedItem
from utilities.forms import ConfirmationForm
from utilities.paginator import EnhancedPaginator
@@ -18,7 +19,7 @@ from .forms import (
ConfigContextForm, ConfigContextBulkEditForm, ConfigContextFilterForm, ImageAttachmentForm, ObjectChangeFilterForm,
TagFilterForm, TagForm,
)
from .models import ConfigContext, ImageAttachment, ObjectChange, ReportResult, Tag, TaggedItem
from .models import ConfigContext, ImageAttachment, ObjectChange, ReportResult
from .reports import get_report, get_reports
from .tables import ConfigContextTable, ObjectChangeTable, TagTable, TaggedItemTable
@@ -29,7 +30,7 @@ from .tables import ConfigContextTable, ObjectChangeTable, TagTable, TaggedItemT
class TagListView(ObjectListView):
queryset = Tag.objects.annotate(
items=Count('extras_taggeditem_items', distinct=True)
items=Count('taggit_taggeditem_items')
).order_by(
'name'
)
@@ -68,23 +69,22 @@ class TagView(View):
class TagEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'extras.change_tag'
permission_required = 'taggit.change_tag'
model = Tag
model_form = TagForm
default_return_url = 'extras:tag_list'
template_name = 'extras/tag_edit.html'
class TagDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'extras.delete_tag'
permission_required = 'taggit.delete_tag'
model = Tag
default_return_url = 'extras:tag_list'
class TagBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'extras.delete_tag'
permission_required = 'circuits.delete_circuittype'
queryset = Tag.objects.annotate(
items=Count('extras_taggeditem_items')
items=Count('taggit_taggeditem_items')
).order_by(
'name'
)
@@ -96,8 +96,7 @@ class TagBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
# Config contexts
#
class ConfigContextListView(PermissionRequiredMixin, ObjectListView):
permission_required = 'extras.view_configcontext'
class ConfigContextListView(ObjectListView):
queryset = ConfigContext.objects.all()
filter = filters.ConfigContextFilter
filter_form = ConfigContextFilterForm
@@ -105,8 +104,7 @@ class ConfigContextListView(PermissionRequiredMixin, ObjectListView):
template_name = 'extras/configcontext_list.html'
class ConfigContextView(PermissionRequiredMixin, View):
permission_required = 'extras.view_configcontext'
class ConfigContextView(View):
def get(self, request, pk):
@@ -175,8 +173,7 @@ class ObjectConfigContextView(View):
# Change logging
#
class ObjectChangeListView(PermissionRequiredMixin, ObjectListView):
permission_required = 'extras.view_objectchange'
class ObjectChangeListView(ObjectListView):
queryset = ObjectChange.objects.select_related('user', 'changed_object_type')
filter = filters.ObjectChangeFilter
filter_form = ObjectChangeFilterForm
@@ -184,8 +181,7 @@ class ObjectChangeListView(PermissionRequiredMixin, ObjectListView):
template_name = 'extras/objectchange_list.html'
class ObjectChangeView(PermissionRequiredMixin, View):
permission_required = 'extras.view_objectchange'
class ObjectChangeView(View):
def get(self, request, pk):
@@ -276,11 +272,10 @@ class ImageAttachmentDeleteView(PermissionRequiredMixin, ObjectDeleteView):
# Reports
#
class ReportListView(PermissionRequiredMixin, View):
class ReportListView(View):
"""
Retrieve all of the available reports from disk and the recorded ReportResult (if any) for each.
"""
permission_required = 'extras.view_reportresult'
def get(self, request):
@@ -300,11 +295,10 @@ class ReportListView(PermissionRequiredMixin, View):
})
class ReportView(PermissionRequiredMixin, View):
class ReportView(View):
"""
Display a single Report and its associated ReportResult (if any).
"""
permission_required = 'extras.view_reportresult'
def get(self, request, name):

View File

@@ -9,12 +9,12 @@ from utilities.api import get_serializer_for_model
from .constants import WEBHOOK_MODELS
def enqueue_webhooks(instance, user, request_id, action):
def enqueue_webhooks(instance, action):
"""
Find Webhook(s) assigned to this instance + action and enqueue them
to be processed
"""
if not settings.WEBHOOKS_ENABLED or instance._meta.label.lower() not in WEBHOOK_MODELS:
if not settings.WEBHOOKS_ENABLED or instance._meta.model_name not in WEBHOOK_MODELS:
return
# Retrieve any applicable Webhooks
@@ -47,7 +47,5 @@ def enqueue_webhooks(instance, user, request_id, action):
serializer.data,
instance._meta.model_name,
action,
str(datetime.datetime.now()),
user.username,
request_id
str(datetime.datetime.now())
)

View File

@@ -10,7 +10,7 @@ from extras.constants import WEBHOOK_CT_JSON, WEBHOOK_CT_X_WWW_FORM_ENCODED, OBJ
@job('default')
def process_webhook(webhook, data, model_name, event, timestamp, username, request_id):
def process_webhook(webhook, data, model_name, event, timestamp):
"""
Make a POST request to the defined Webhook
"""
@@ -18,8 +18,6 @@ def process_webhook(webhook, data, model_name, event, timestamp, username, reque
'event': dict(OBJECTCHANGE_ACTION_CHOICES)[event].lower(),
'timestamp': timestamp,
'model': model_name,
'username': username,
'request_id': request_id,
'data': data
}
headers = {

View File

@@ -21,11 +21,10 @@ __all__ = [
class NestedVRFSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vrf-detail')
prefix_count = serializers.IntegerField(read_only=True)
class Meta:
model = VRF
fields = ['id', 'url', 'name', 'rd', 'prefix_count']
fields = ['id', 'url', 'name', 'rd']
#
@@ -34,11 +33,10 @@ class NestedVRFSerializer(WritableNestedSerializer):
class NestedRIRSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:rir-detail')
aggregate_count = serializers.IntegerField(read_only=True)
class Meta:
model = RIR
fields = ['id', 'url', 'name', 'slug', 'aggregate_count']
fields = ['id', 'url', 'name', 'slug']
class NestedAggregateSerializer(WritableNestedSerializer):
@@ -55,21 +53,18 @@ class NestedAggregateSerializer(WritableNestedSerializer):
class NestedRoleSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:role-detail')
prefix_count = serializers.IntegerField(read_only=True)
vlan_count = serializers.IntegerField(read_only=True)
class Meta:
model = Role
fields = ['id', 'url', 'name', 'slug', 'prefix_count', 'vlan_count']
fields = ['id', 'url', 'name', 'slug']
class NestedVLANGroupSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlangroup-detail')
vlan_count = serializers.IntegerField(read_only=True)
class Meta:
model = VLANGroup
fields = ['id', 'url', 'name', 'slug', 'vlan_count']
fields = ['id', 'url', 'name', 'slug']
class NestedVLANSerializer(WritableNestedSerializer):

View File

@@ -25,14 +25,12 @@ from .nested_serializers import *
class VRFSerializer(TaggitSerializer, CustomFieldModelSerializer):
tenant = NestedTenantSerializer(required=False, allow_null=True)
tags = TagListSerializerField(required=False)
ipaddress_count = serializers.IntegerField(read_only=True)
prefix_count = serializers.IntegerField(read_only=True)
class Meta:
model = VRF
fields = [
'id', 'name', 'rd', 'tenant', 'enforce_unique', 'description', 'tags', 'display_name', 'custom_fields',
'created', 'last_updated', 'ipaddress_count', 'prefix_count',
'created', 'last_updated',
]
@@ -41,11 +39,10 @@ class VRFSerializer(TaggitSerializer, CustomFieldModelSerializer):
#
class RIRSerializer(ValidatedModelSerializer):
aggregate_count = serializers.IntegerField(read_only=True)
class Meta:
model = RIR
fields = ['id', 'name', 'slug', 'is_private', 'aggregate_count']
fields = ['id', 'name', 'slug', 'is_private']
class AggregateSerializer(TaggitSerializer, CustomFieldModelSerializer):
@@ -66,21 +63,18 @@ class AggregateSerializer(TaggitSerializer, CustomFieldModelSerializer):
#
class RoleSerializer(ValidatedModelSerializer):
prefix_count = serializers.IntegerField(read_only=True)
vlan_count = serializers.IntegerField(read_only=True)
class Meta:
model = Role
fields = ['id', 'name', 'slug', 'weight', 'prefix_count', 'vlan_count']
fields = ['id', 'name', 'slug', 'weight']
class VLANGroupSerializer(ValidatedModelSerializer):
site = NestedSiteSerializer(required=False, allow_null=True)
vlan_count = serializers.IntegerField(read_only=True)
class Meta:
model = VLANGroup
fields = ['id', 'name', 'slug', 'site', 'vlan_count']
fields = ['id', 'name', 'slug', 'site']
validators = []
def validate(self, data):
@@ -105,13 +99,12 @@ class VLANSerializer(TaggitSerializer, CustomFieldModelSerializer):
status = ChoiceField(choices=VLAN_STATUS_CHOICES, required=False)
role = NestedRoleSerializer(required=False, allow_null=True)
tags = TagListSerializerField(required=False)
prefix_count = serializers.IntegerField(read_only=True)
class Meta:
model = VLAN
fields = [
'id', 'site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description', 'tags', 'display_name',
'custom_fields', 'created', 'last_updated', 'prefix_count',
'custom_fields', 'created', 'last_updated',
]
validators = []
@@ -135,7 +128,6 @@ class VLANSerializer(TaggitSerializer, CustomFieldModelSerializer):
#
class PrefixSerializer(TaggitSerializer, CustomFieldModelSerializer):
family = ChoiceField(choices=AF_CHOICES, read_only=True)
site = NestedSiteSerializer(required=False, allow_null=True)
vrf = NestedVRFSerializer(required=False, allow_null=True)
tenant = NestedTenantSerializer(required=False, allow_null=True)
@@ -197,7 +189,6 @@ class IPAddressInterfaceSerializer(WritableNestedSerializer):
class IPAddressSerializer(TaggitSerializer, CustomFieldModelSerializer):
family = ChoiceField(choices=AF_CHOICES, read_only=True)
vrf = NestedVRFSerializer(required=False, allow_null=True)
tenant = NestedTenantSerializer(required=False, allow_null=True)
status = ChoiceField(choices=IPADDRESS_STATUS_CHOICES, required=False)
@@ -210,8 +201,8 @@ class IPAddressSerializer(TaggitSerializer, CustomFieldModelSerializer):
class Meta:
model = IPAddress
fields = [
'id', 'family', 'address', 'vrf', 'tenant', 'status', 'role', 'interface', 'nat_inside',
'nat_outside', 'dns_name', 'description', 'tags', 'custom_fields', 'created', 'last_updated',
'id', 'family', 'address', 'vrf', 'tenant', 'status', 'role', 'interface', 'description', 'nat_inside',
'nat_outside', 'tags', 'custom_fields', 'created', 'last_updated',
]
read_only_fields = ['family']

View File

@@ -1,5 +1,4 @@
from django.conf import settings
from django.db.models import Count
from django.shortcuts import get_object_or_404
from rest_framework import status
from rest_framework.decorators import action
@@ -10,7 +9,6 @@ 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.utils import get_subquery
from . import serializers
@@ -33,10 +31,7 @@ class IPAMFieldChoicesViewSet(FieldChoicesViewSet):
#
class VRFViewSet(CustomFieldModelViewSet):
queryset = VRF.objects.select_related('tenant').prefetch_related('tags').annotate(
ipaddress_count=get_subquery(IPAddress, 'vrf'),
prefix_count=get_subquery(Prefix, 'vrf')
)
queryset = VRF.objects.select_related('tenant').prefetch_related('tags')
serializer_class = serializers.VRFSerializer
filterset_class = filters.VRFFilter
@@ -46,9 +41,7 @@ class VRFViewSet(CustomFieldModelViewSet):
#
class RIRViewSet(ModelViewSet):
queryset = RIR.objects.annotate(
aggregate_count=Count('aggregates')
)
queryset = RIR.objects.all()
serializer_class = serializers.RIRSerializer
filterset_class = filters.RIRFilter
@@ -68,10 +61,7 @@ class AggregateViewSet(CustomFieldModelViewSet):
#
class RoleViewSet(ModelViewSet):
queryset = Role.objects.annotate(
prefix_count=get_subquery(Prefix, 'role'),
vlan_count=get_subquery(VLAN, 'role')
)
queryset = Role.objects.all()
serializer_class = serializers.RoleSerializer
filterset_class = filters.RoleFilter
@@ -81,11 +71,7 @@ class RoleViewSet(ModelViewSet):
#
class PrefixViewSet(CustomFieldModelViewSet):
queryset = Prefix.objects.select_related(
'site', 'vrf__tenant', 'tenant', 'vlan', 'role'
).prefetch_related(
'tags'
)
queryset = Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role').prefetch_related('tags')
serializer_class = serializers.PrefixSerializer
filterset_class = filters.PrefixFilter
@@ -277,9 +263,7 @@ class IPAddressViewSet(CustomFieldModelViewSet):
#
class VLANGroupViewSet(ModelViewSet):
queryset = VLANGroup.objects.select_related('site').annotate(
vlan_count=Count('vlans')
)
queryset = VLANGroup.objects.select_related('site')
serializer_class = serializers.VLANGroupSerializer
filterset_class = filters.VLANGroupFilter
@@ -289,13 +273,7 @@ class VLANGroupViewSet(ModelViewSet):
#
class VLANViewSet(CustomFieldModelViewSet):
queryset = VLAN.objects.select_related(
'site', 'group', 'tenant', 'role'
).prefetch_related(
'tags'
).annotate(
prefix_count=get_subquery(Prefix, 'role')
)
queryset = VLAN.objects.select_related('site', 'group', 'tenant', 'role').prefetch_related('tags')
serializer_class = serializers.VLANSerializer
filterset_class = filters.VLANFilter

View File

@@ -7,7 +7,7 @@ from netaddr.core import AddrFormatError
from dcim.models import Site, Device, Interface
from extras.filters import CustomFieldFilterSet
from tenancy.models import Tenant
from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter
from utilities.filters import NumericInFilter, TagFilter
from virtualization.models import VirtualMachine
from .constants import IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, PREFIX_STATUS_CHOICES, VLAN_STATUS_CHOICES
from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
@@ -48,7 +48,7 @@ class VRFFilter(CustomFieldFilterSet, django_filters.FilterSet):
fields = ['name', 'rd', 'enforce_unique']
class RIRFilter(NameSlugSearchFilterSet):
class RIRFilter(django_filters.FilterSet):
id__in = NumericInFilter(
field_name='id',
lookup_expr='in'
@@ -96,11 +96,7 @@ class AggregateFilter(CustomFieldFilterSet, django_filters.FilterSet):
return queryset.filter(qs_filter)
class RoleFilter(NameSlugSearchFilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
)
class RoleFilter(django_filters.FilterSet):
class Meta:
model = Role
@@ -116,10 +112,6 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
method='search',
label='Search',
)
prefix = django_filters.CharFilter(
method='filter_prefix',
label='Prefix',
)
within = django_filters.CharFilter(
method='search_within',
label='Within prefix',
@@ -205,15 +197,6 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
pass
return queryset.filter(qs_filter)
def filter_prefix(self, queryset, name, value):
if not value.strip():
return queryset
try:
query = str(netaddr.IPNetwork(value).cidr)
return queryset.filter(prefix=query)
except ValidationError:
return queryset.none()
def search_within(self, queryset, name, value):
value = value.strip()
if not value:
@@ -331,13 +314,12 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
class Meta:
model = IPAddress
fields = ['family', 'dns_name']
fields = ['family']
def search(self, queryset, name, value):
if not value.strip():
return queryset
qs_filter = (
Q(dns_name__icontains=value) |
Q(description__icontains=value) |
Q(address__istartswith=value)
)
@@ -378,7 +360,7 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
return queryset.none()
class VLANGroupFilter(NameSlugSearchFilterSet):
class VLANGroupFilter(django_filters.FilterSet):
site_id = django_filters.ModelMultipleChoiceFilter(
queryset=Site.objects.all(),
label='Site (ID)',

View File

@@ -1,6 +1,7 @@
from django import forms
from django.core.exceptions import MultipleObjectsReturned
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db.models import Count
from taggit.forms import TagField
from dcim.models import Site, Rack, Device, Interface
@@ -8,9 +9,9 @@ from extras.forms import AddRemoveTagsForm, CustomFieldForm, CustomFieldBulkEdit
from tenancy.forms import TenancyForm
from tenancy.models import Tenant
from utilities.forms import (
add_blank_choice, APISelect, APISelectMultiple, BootstrapMixin, BulkEditNullBooleanSelect, ChainedModelChoiceField,
CSVChoiceField, ExpandableIPAddressField, FilterChoiceField, FlexibleModelChoiceField, ReturnURLForm, SlugField,
StaticSelect2, StaticSelect2Multiple, BOOLEAN_WITH_BLANK_CHOICES
AnnotatedMultipleChoiceField, APISelect, BootstrapMixin, BulkEditNullBooleanSelect, ChainedModelChoiceField,
CSVChoiceField, ExpandableIPAddressField, FilterChoiceField, FlexibleModelChoiceField, Livesearch, ReturnURLForm,
SlugField, add_blank_choice,
)
from virtualization.models import VirtualMachine
from .constants import (
@@ -76,10 +77,7 @@ class VRFBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm
)
tenant = forms.ModelChoiceField(
queryset=Tenant.objects.all(),
required=False,
widget=APISelect(
api_url="/api/tenancy/tenants/"
)
required=False
)
enforce_unique = forms.NullBooleanField(
required=False,
@@ -104,14 +102,11 @@ class VRFFilterForm(BootstrapMixin, CustomFieldFilterForm):
label='Search'
)
tenant = FilterChoiceField(
queryset=Tenant.objects.all(),
queryset=Tenant.objects.annotate(
filter_count=Count('vrfs')
),
to_field_name='slug',
null_label='-- None --',
widget=APISelectMultiple(
api_url="/api/tenancy/tenants/",
value_field="slug",
null_option=True,
)
null_label='-- None --'
)
@@ -144,8 +139,12 @@ class RIRFilterForm(BootstrapMixin, forms.Form):
is_private = forms.NullBooleanField(
required=False,
label='Private',
widget=StaticSelect2(
choices=BOOLEAN_WITH_BLANK_CHOICES
widget=forms.Select(
choices=[
('', '---------'),
('True', 'Yes'),
('False', 'No'),
]
)
)
@@ -169,11 +168,6 @@ class AggregateForm(BootstrapMixin, CustomFieldForm):
'rir': "Regional Internet Registry responsible for this prefix",
'date_added': "Format: YYYY-MM-DD",
}
widgets = {
'rir': APISelect(
api_url="/api/ipam/rirs/"
)
}
class AggregateCSVForm(forms.ModelForm):
@@ -199,10 +193,7 @@ class AggregateBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEd
rir = forms.ModelChoiceField(
queryset=RIR.objects.all(),
required=False,
label='RIR',
widget=APISelect(
api_url="/api/ipam/rirs/"
)
label='RIR'
)
date_added = forms.DateField(
required=False
@@ -227,17 +218,12 @@ class AggregateFilterForm(BootstrapMixin, CustomFieldFilterForm):
family = forms.ChoiceField(
required=False,
choices=IP_FAMILY_CHOICES,
label='Address family',
widget=StaticSelect2()
label='Address family'
)
rir = FilterChoiceField(
queryset=RIR.objects.all(),
queryset=RIR.objects.annotate(filter_count=Count('aggregates')),
to_field_name='slug',
label='RIR',
widget=APISelectMultiple(
api_url="/api/ipam/rirs/",
value_field="slug",
)
label='RIR'
)
@@ -275,13 +261,9 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldForm):
queryset=Site.objects.all(),
required=False,
label='Site',
widget=APISelect(
api_url="/api/dcim/sites/",
filter_for={
'vlan_group': 'site_id',
'vlan': 'site_id',
},
widget=forms.Select(
attrs={
'filter-for': 'vlan_group',
'nullable': 'true',
}
)
@@ -294,11 +276,9 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldForm):
required=False,
label='VLAN group',
widget=APISelect(
api_url='/api/ipam/vlan-groups/',
filter_for={
'vlan': 'group_id'
},
api_url='/api/ipam/vlan-groups/?site_id={{site}}',
attrs={
'filter-for': 'vlan',
'nullable': 'true',
}
)
@@ -312,7 +292,7 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldForm):
required=False,
label='VLAN',
widget=APISelect(
api_url='/api/ipam/vlans/',
api_url='/api/ipam/vlans/?site_id={{site}}&group_id={{vlan_group}}',
display_field='display_name'
)
)
@@ -324,15 +304,6 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldForm):
'prefix', 'vrf', 'site', 'vlan', 'status', 'role', 'is_pool', 'description', 'tenant_group', 'tenant',
'tags',
]
widgets = {
'vrf': APISelect(
api_url="/api/ipam/vrfs/"
),
'status': StaticSelect2(),
'role': APISelect(
api_url="/api/ipam/roles/"
)
}
def __init__(self, *args, **kwargs):
@@ -349,11 +320,11 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldForm):
class PrefixCSVForm(forms.ModelForm):
vrf = FlexibleModelChoiceField(
vrf = forms.ModelChoiceField(
queryset=VRF.objects.all(),
to_field_name='rd',
required=False,
help_text='Route distinguisher of parent VRF (or {ID})',
to_field_name='rd',
help_text='Route distinguisher of parent VRF',
error_messages={
'invalid_choice': 'VRF not found.',
}
@@ -444,42 +415,24 @@ class PrefixBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF
)
site = forms.ModelChoiceField(
queryset=Site.objects.all(),
required=False,
widget=APISelect(
api_url="/api/dcim/sites/"
)
required=False
)
vrf = forms.ModelChoiceField(
queryset=VRF.objects.all(),
required=False,
label='VRF',
widget=APISelect(
api_url="/api/ipam/vrfs/"
)
)
prefix_length = forms.IntegerField(
min_value=1,
max_value=127,
required=False
label='VRF'
)
tenant = forms.ModelChoiceField(
queryset=Tenant.objects.all(),
required=False,
widget=APISelect(
api_url="/api/tenancy/tenants/"
)
required=False
)
status = forms.ChoiceField(
choices=add_blank_choice(PREFIX_STATUS_CHOICES),
required=False,
widget=StaticSelect2()
required=False
)
role = forms.ModelChoiceField(
queryset=Role.objects.all(),
required=False,
widget=APISelect(
api_url="/api/ipam/roles/"
)
required=False
)
is_pool = forms.NullBooleanField(
required=False,
@@ -515,65 +468,47 @@ class PrefixFilterForm(BootstrapMixin, CustomFieldFilterForm):
family = forms.ChoiceField(
required=False,
choices=IP_FAMILY_CHOICES,
label='Address family',
widget=StaticSelect2()
label='Address family'
)
mask_length = forms.ChoiceField(
required=False,
choices=PREFIX_MASK_LENGTH_CHOICES,
label='Mask length',
widget=StaticSelect2()
label='Mask length'
)
vrf_id = FilterChoiceField(
queryset=VRF.objects.all(),
vrf = FilterChoiceField(
queryset=VRF.objects.annotate(
filter_count=Count('prefixes')
),
to_field_name='rd',
label='VRF',
null_label='-- Global --',
widget=APISelectMultiple(
api_url="/api/ipam/vrfs/",
null_option=True,
)
null_label='-- Global --'
)
tenant = FilterChoiceField(
queryset=Tenant.objects.all(),
queryset=Tenant.objects.annotate(
filter_count=Count('prefixes')
),
to_field_name='slug',
null_label='-- None --',
widget=APISelectMultiple(
api_url="/api/tenancy/tenants/",
value_field="slug",
null_option=True,
)
null_label='-- None --'
)
status = forms.MultipleChoiceField(
status = AnnotatedMultipleChoiceField(
choices=PREFIX_STATUS_CHOICES,
required=False,
widget=StaticSelect2Multiple()
annotate=Prefix.objects.all(),
annotate_field='status',
required=False
)
site = FilterChoiceField(
queryset=Site.objects.all(),
queryset=Site.objects.annotate(
filter_count=Count('prefixes')
),
to_field_name='slug',
null_label='-- None --',
widget=APISelectMultiple(
api_url="/api/dcim/sites/",
value_field="slug",
null_option=True,
)
null_label='-- None --'
)
role = FilterChoiceField(
queryset=Role.objects.all(),
queryset=Role.objects.annotate(
filter_count=Count('prefixes')
),
to_field_name='slug',
null_label='-- None --',
widget=APISelectMultiple(
api_url="/api/ipam/roles/",
value_field="slug",
null_option=True,
)
)
is_pool = forms.NullBooleanField(
required=False,
label='Is a pool',
widget=StaticSelect2(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
null_label='-- None --'
)
expand = forms.BooleanField(
required=False,
@@ -594,11 +529,9 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm)
queryset=Site.objects.all(),
required=False,
label='Site',
widget=APISelect(
api_url="/api/dcim/sites/",
filter_for={
'nat_rack': 'site_id',
'nat_device': 'site_id'
widget=forms.Select(
attrs={
'filter-for': 'nat_rack'
}
)
)
@@ -610,12 +543,10 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm)
required=False,
label='Rack',
widget=APISelect(
api_url='/api/dcim/racks/',
api_url='/api/dcim/racks/?site_id={{nat_site}}',
display_field='display_name',
filter_for={
'nat_device': 'rack_id'
},
attrs={
'filter-for': 'nat_device',
'nullable': 'true'
}
)
@@ -629,11 +560,9 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm)
required=False,
label='Device',
widget=APISelect(
api_url='/api/dcim/devices/',
api_url='/api/dcim/devices/?site_id={{nat_site}}&rack_id={{nat_rack}}',
display_field='display_name',
filter_for={
'nat_inside': 'device_id'
}
attrs={'filter-for': 'nat_inside'}
)
)
nat_inside = ChainedModelChoiceField(
@@ -644,10 +573,20 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm)
required=False,
label='IP Address',
widget=APISelect(
api_url='/api/ipam/ip-addresses/',
api_url='/api/ipam/ip-addresses/?device_id={{nat_device}}',
display_field='address'
)
)
livesearch = forms.CharField(
required=False,
label='Search',
widget=Livesearch(
query_key='q',
query_url='ipam-api:ipaddress-list',
field_to_update='nat_inside',
obj_label='address'
)
)
primary_for_parent = forms.BooleanField(
required=False,
label='Make this the primary IP for the device/VM'
@@ -659,16 +598,9 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm)
class Meta:
model = IPAddress
fields = [
'address', 'vrf', 'status', 'role', 'dns_name', 'description', 'interface', 'primary_for_parent',
'nat_site', 'nat_rack', 'nat_inside', 'tenant_group', 'tenant', 'tags',
'address', 'vrf', 'status', 'role', 'description', 'interface', 'primary_for_parent', 'nat_site',
'nat_rack', 'nat_inside', 'tenant_group', 'tenant', 'tags',
]
widgets = {
'status': StaticSelect2(),
'role': StaticSelect2(),
'vrf': APISelect(
api_url="/api/ipam/vrfs/"
)
}
def __init__(self, *args, **kwargs):
@@ -746,15 +678,8 @@ class IPAddressBulkAddForm(BootstrapMixin, TenancyForm, CustomFieldForm):
class Meta:
model = IPAddress
fields = [
'address', 'vrf', 'status', 'role', 'dns_name', 'description', 'tenant_group', 'tenant',
'address', 'vrf', 'status', 'role', 'description', 'tenant_group', 'tenant',
]
widgets = {
'status': StaticSelect2(),
'role': StaticSelect2(),
'vrf': APISelect(
api_url="/api/ipam/vrfs/"
)
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -762,11 +687,11 @@ class IPAddressBulkAddForm(BootstrapMixin, TenancyForm, CustomFieldForm):
class IPAddressCSVForm(forms.ModelForm):
vrf = FlexibleModelChoiceField(
vrf = forms.ModelChoiceField(
queryset=VRF.objects.all(),
to_field_name='rd',
required=False,
help_text='Route distinguisher of parent VRF (or {ID})',
to_field_name='rd',
help_text='Route distinguisher of the assigned VRF',
error_messages={
'invalid_choice': 'VRF not found.',
}
@@ -892,45 +817,27 @@ class IPAddressBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEd
vrf = forms.ModelChoiceField(
queryset=VRF.objects.all(),
required=False,
label='VRF',
widget=APISelect(
api_url="/api/ipam/vrfs/"
)
)
mask_length = forms.IntegerField(
min_value=1,
max_value=128,
required=False
label='VRF'
)
tenant = forms.ModelChoiceField(
queryset=Tenant.objects.all(),
required=False,
widget=APISelect(
api_url="/api/tenancy/tenants/"
)
required=False
)
status = forms.ChoiceField(
choices=add_blank_choice(IPADDRESS_STATUS_CHOICES),
required=False,
widget=StaticSelect2()
required=False
)
role = forms.ChoiceField(
choices=add_blank_choice(IPADDRESS_ROLE_CHOICES),
required=False,
widget=StaticSelect2()
)
dns_name = forms.CharField(
max_length=255,
required=False
)
description = forms.CharField(
max_length=100,
required=False
max_length=100, required=False
)
class Meta:
nullable_fields = [
'vrf', 'role', 'tenant', 'dns_name', 'description',
'vrf', 'role', 'tenant', 'description',
]
@@ -939,10 +846,7 @@ class IPAddressAssignForm(BootstrapMixin, forms.Form):
queryset=VRF.objects.all(),
required=False,
label='VRF',
empty_label='Global',
widget=APISelect(
api_url="/api/ipam/vrfs/"
)
empty_label='Global'
)
address = forms.CharField(
label='IP Address'
@@ -967,43 +871,39 @@ class IPAddressFilterForm(BootstrapMixin, CustomFieldFilterForm):
family = forms.ChoiceField(
required=False,
choices=IP_FAMILY_CHOICES,
label='Address family',
widget=StaticSelect2()
label='Address family'
)
mask_length = forms.ChoiceField(
required=False,
choices=IPADDRESS_MASK_LENGTH_CHOICES,
label='Mask length',
widget=StaticSelect2()
label='Mask length'
)
vrf_id = FilterChoiceField(
queryset=VRF.objects.all(),
vrf = FilterChoiceField(
queryset=VRF.objects.annotate(
filter_count=Count('ip_addresses')
),
to_field_name='rd',
label='VRF',
null_label='-- Global --',
widget=APISelectMultiple(
api_url="/api/ipam/vrfs/",
null_option=True,
)
null_label='-- Global --'
)
tenant = FilterChoiceField(
queryset=Tenant.objects.all(),
queryset=Tenant.objects.annotate(
filter_count=Count('ip_addresses')
),
to_field_name='slug',
null_label='-- None --',
widget=APISelectMultiple(
api_url="/api/tenancy/tenants/",
value_field="slug",
null_option=True,
)
null_label='-- None --'
)
status = forms.MultipleChoiceField(
status = AnnotatedMultipleChoiceField(
choices=IPADDRESS_STATUS_CHOICES,
required=False,
widget=StaticSelect2Multiple()
annotate=IPAddress.objects.all(),
annotate_field='status',
required=False
)
role = forms.MultipleChoiceField(
role = AnnotatedMultipleChoiceField(
choices=IPADDRESS_ROLE_CHOICES,
required=False,
widget=StaticSelect2Multiple()
annotate=IPAddress.objects.all(),
annotate_field='role',
required=False
)
@@ -1019,11 +919,6 @@ class VLANGroupForm(BootstrapMixin, forms.ModelForm):
fields = [
'site', 'name', 'slug',
]
widgets = {
'site': APISelect(
api_url="/api/dcim/sites/"
)
}
class VLANGroupCSVForm(forms.ModelForm):
@@ -1048,14 +943,11 @@ class VLANGroupCSVForm(forms.ModelForm):
class VLANGroupFilterForm(BootstrapMixin, forms.Form):
site = FilterChoiceField(
queryset=Site.objects.all(),
queryset=Site.objects.annotate(
filter_count=Count('vlan_groups')
),
to_field_name='slug',
null_label='-- Global --',
widget=APISelectMultiple(
api_url="/api/dcim/sites/",
value_field="slug",
null_option=True,
)
null_label='-- Global --'
)
@@ -1067,12 +959,9 @@ class VLANForm(BootstrapMixin, TenancyForm, CustomFieldForm):
site = forms.ModelChoiceField(
queryset=Site.objects.all(),
required=False,
widget=APISelect(
api_url="/api/dcim/sites/",
filter_for={
'group': 'site_id'
},
widget=forms.Select(
attrs={
'filter-for': 'group',
'nullable': 'true',
}
)
@@ -1085,7 +974,7 @@ class VLANForm(BootstrapMixin, TenancyForm, CustomFieldForm):
required=False,
label='Group',
widget=APISelect(
api_url='/api/ipam/vlan-groups/',
api_url='/api/ipam/vlan-groups/?site_id={{site}}',
)
)
tags = TagField(required=False)
@@ -1103,12 +992,6 @@ class VLANForm(BootstrapMixin, TenancyForm, CustomFieldForm):
'status': "Operational status of this VLAN",
'role': "The primary function of this VLAN",
}
widgets = {
'status': StaticSelect2(),
'role': APISelect(
api_url="/api/ipam/roles/"
)
}
class VLANCSVForm(forms.ModelForm):
@@ -1184,36 +1067,23 @@ class VLANBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor
)
site = forms.ModelChoiceField(
queryset=Site.objects.all(),
required=False,
widget=APISelect(
api_url="/api/dcim/sites/"
)
required=False
)
group = forms.ModelChoiceField(
queryset=VLANGroup.objects.all(),
required=False,
widget=APISelect(
api_url="/api/ipam/vlan-groups/"
)
required=False
)
tenant = forms.ModelChoiceField(
queryset=Tenant.objects.all(),
required=False,
widget=APISelect(
api_url="/api/tenancy/tenants/"
)
required=False
)
status = forms.ChoiceField(
choices=add_blank_choice(VLAN_STATUS_CHOICES),
required=False,
widget=StaticSelect2()
required=False
)
role = forms.ModelChoiceField(
queryset=Role.objects.all(),
required=False,
widget=APISelect(
api_url="/api/ipam/roles/"
)
required=False
)
description = forms.CharField(
max_length=100,
@@ -1233,48 +1103,38 @@ class VLANFilterForm(BootstrapMixin, CustomFieldFilterForm):
label='Search'
)
site = FilterChoiceField(
queryset=Site.objects.all(),
queryset=Site.objects.annotate(
filter_count=Count('vlans')
),
to_field_name='slug',
null_label='-- Global --',
widget=APISelectMultiple(
api_url="/api/dcim/sites/",
value_field="slug",
null_option=True,
)
null_label='-- Global --'
)
group_id = FilterChoiceField(
queryset=VLANGroup.objects.all(),
queryset=VLANGroup.objects.annotate(
filter_count=Count('vlans')
),
label='VLAN group',
null_label='-- None --',
widget=APISelectMultiple(
api_url="/api/ipam/vlan-groups/",
null_option=True,
)
null_label='-- None --'
)
tenant = FilterChoiceField(
queryset=Tenant.objects.all(),
queryset=Tenant.objects.annotate(
filter_count=Count('vlans')
),
to_field_name='slug',
null_label='-- None --',
widget=APISelectMultiple(
api_url="/api/tenancy/tenants/",
value_field="slug",
null_option=True,
)
null_label='-- None --'
)
status = forms.MultipleChoiceField(
status = AnnotatedMultipleChoiceField(
choices=VLAN_STATUS_CHOICES,
required=False,
widget=StaticSelect2Multiple()
annotate=VLAN.objects.all(),
annotate_field='status',
required=False
)
role = FilterChoiceField(
queryset=Role.objects.all(),
queryset=Role.objects.annotate(
filter_count=Count('vlans')
),
to_field_name='slug',
null_label='-- None --',
widget=APISelectMultiple(
api_url="/api/ipam/roles/",
value_field="slug",
null_option=True,
)
null_label='-- None --'
)
@@ -1296,10 +1156,6 @@ class ServiceForm(BootstrapMixin, CustomFieldForm):
'ipaddresses': "IP address assignment is optional. If no IPs are selected, the service is assumed to be "
"reachable via all IPs assigned to the device.",
}
widgets = {
'protocol': StaticSelect2(),
'ipaddresses': StaticSelect2Multiple(),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -1326,11 +1182,10 @@ class ServiceFilterForm(BootstrapMixin, CustomFieldFilterForm):
)
protocol = forms.ChoiceField(
choices=add_blank_choice(IP_PROTOCOL_CHOICES),
required=False,
widget=StaticSelect2Multiple()
required=False
)
port = forms.IntegerField(
required=False,
required=False
)
@@ -1341,8 +1196,7 @@ class ServiceBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
)
protocol = forms.ChoiceField(
choices=add_blank_choice(IP_PROTOCOL_CHOICES),
required=False,
widget=StaticSelect2()
required=False
)
port = forms.IntegerField(
validators=[
@@ -1358,5 +1212,5 @@ class ServiceBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
class Meta:
nullable_fields = [
'site', 'tenant', 'role', 'description',
'site', 'group', 'tenant', 'role', 'description',
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 2.1.5 on 2019-01-31 18:14
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ipam', '0023_change_logging'),
]
operations = [
migrations.AlterField(
model_name='vrf',
name='rd',
field=models.CharField(blank=True, max_length=21, null=True, unique=True),
),
]

View File

@@ -1,45 +0,0 @@
# Generated by Django 2.1.4 on 2019-02-20 06:56
from django.db import migrations
import taggit.managers
class Migration(migrations.Migration):
dependencies = [
('ipam', '0024_vrf_allow_null_rd'),
('extras', '0019_tag_taggeditem'),
]
operations = [
migrations.AlterField(
model_name='aggregate',
name='tags',
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
),
migrations.AlterField(
model_name='ipaddress',
name='tags',
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
),
migrations.AlterField(
model_name='prefix',
name='tags',
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
),
migrations.AlterField(
model_name='service',
name='tags',
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
),
migrations.AlterField(
model_name='vlan',
name='tags',
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
),
migrations.AlterField(
model_name='vrf',
name='tags',
field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 2.2 on 2019-04-20 00:57
from django.db import migrations
import django.db.models.expressions
class Migration(migrations.Migration):
dependencies = [
('ipam', '0025_custom_tag_models'),
]
operations = [
migrations.AlterModelOptions(
name='prefix',
options={'ordering': [django.db.models.expressions.OrderBy(django.db.models.expressions.F('vrf'), nulls_first=True), 'family', 'prefix'], 'verbose_name_plural': 'prefixes'},
),
]

View File

@@ -1,19 +0,0 @@
# Generated by Django 2.2 on 2019-04-22 21:43
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ipam', '0026_prefix_ordering_vrf_nulls_first'),
]
operations = [
migrations.AddField(
model_name='ipaddress',
name='dns_name',
field=models.CharField(blank=True, max_length=255, validators=[django.core.validators.RegexValidator(code='invalid', message='Only alphanumeric characters, hyphens, and periods are allowed in DNS names', regex='^[0-9A-Za-z.-]+$')]),
),
]

View File

@@ -1,22 +1,20 @@
import netaddr
from django.conf import settings
from django.contrib.contenttypes.fields import GenericRelation
from django.core.exceptions import ValidationError, ObjectDoesNotExist
from django.core.exceptions import ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from django.db.models import F, Q
from django.db.models import Q
from django.db.models.expressions import RawSQL
from django.urls import reverse
from taggit.managers import TaggableManager
from dcim.models import Interface
from extras.models import CustomFieldModel, ObjectChange, TaggedItem
from extras.models import CustomFieldModel
from utilities.models import ChangeLoggedModel
from utilities.utils import serialize_object
from .constants import *
from .fields import IPNetworkField, IPAddressField
from .querysets import PrefixQuerySet
from .validators import DNSValidator
class VRF(ChangeLoggedModel, CustomFieldModel):
@@ -31,8 +29,6 @@ class VRF(ChangeLoggedModel, CustomFieldModel):
rd = models.CharField(
max_length=21,
unique=True,
blank=True,
null=True,
verbose_name='Route distinguisher'
)
tenant = models.ForeignKey(
@@ -57,7 +53,7 @@ class VRF(ChangeLoggedModel, CustomFieldModel):
object_id_field='obj_id'
)
tags = TaggableManager(through=TaggedItem)
tags = TaggableManager()
csv_headers = ['name', 'rd', 'tenant', 'enforce_unique', 'description']
@@ -83,9 +79,9 @@ class VRF(ChangeLoggedModel, CustomFieldModel):
@property
def display_name(self):
if self.rd:
if self.name and self.rd:
return "{} ({})".format(self.name, self.rd)
return self.name
return None
class RIR(ChangeLoggedModel):
@@ -156,7 +152,7 @@ class Aggregate(ChangeLoggedModel, CustomFieldModel):
object_id_field='obj_id'
)
tags = TaggableManager(through=TaggedItem)
tags = TaggableManager()
csv_headers = ['prefix', 'rir', 'date_added', 'description']
@@ -326,14 +322,14 @@ class Prefix(ChangeLoggedModel, CustomFieldModel):
)
objects = PrefixQuerySet.as_manager()
tags = TaggableManager(through=TaggedItem)
tags = TaggableManager()
csv_headers = [
'prefix', 'vrf', 'tenant', 'site', 'vlan_group', 'vlan_vid', 'status', 'role', 'is_pool', 'description',
]
class Meta:
ordering = [F('vrf').asc(nulls_first=True), 'family', 'prefix']
ordering = ['vrf', 'family', 'prefix']
verbose_name_plural = 'prefixes'
def __str__(self):
@@ -368,15 +364,11 @@ class Prefix(ChangeLoggedModel, CustomFieldModel):
})
def save(self, *args, **kwargs):
if isinstance(self.prefix, netaddr.IPNetwork):
if self.prefix:
# Clear host bits from prefix
self.prefix = self.prefix.cidr
# Record address family
# Infer address family from IPNetwork object
self.family = self.prefix.version
super().save(*args, **kwargs)
def to_csv(self):
@@ -393,15 +385,6 @@ class Prefix(ChangeLoggedModel, CustomFieldModel):
self.description,
)
def _set_prefix_length(self, value):
"""
Expose the IPNetwork object's prefixlen attribute on the parent model so that it can be manipulated directly,
e.g. for bulk editing.
"""
if self.prefix is not None:
self.prefix.prefixlen = value
prefix_length = property(fset=_set_prefix_length)
def get_status_class(self):
return STATUS_CHOICE_CLASSES[self.status]
@@ -446,23 +429,12 @@ class Prefix(ChangeLoggedModel, CustomFieldModel):
child_ips = netaddr.IPSet([ip.address.ip for ip in self.get_child_ips()])
available_ips = prefix - child_ips
# All IP addresses within a pool are considered usable
if self.is_pool:
return available_ips
# All IP addresses within a point-to-point prefix (IPv4 /31 or IPv6 /127) are considered usable
if (
self.family == 4 and self.prefix.prefixlen == 31 # RFC 3021
) or (
self.family == 6 and self.prefix.prefixlen == 127 # RFC 6164
):
return available_ips
# Omit first and last IP address from the available set
available_ips -= netaddr.IPSet([
netaddr.IPAddress(self.prefix.first),
netaddr.IPAddress(self.prefix.last),
])
# Remove unusable IPs from non-pool prefixes
if not self.is_pool:
available_ips -= netaddr.IPSet([
netaddr.IPAddress(self.prefix.first),
netaddr.IPAddress(self.prefix.last),
])
return available_ips
@@ -578,13 +550,6 @@ class IPAddress(ChangeLoggedModel, CustomFieldModel):
verbose_name='NAT (Inside)',
help_text='The IP for which this address is the "outside" IP'
)
dns_name = models.CharField(
max_length=255,
blank=True,
validators=[DNSValidator],
verbose_name='DNS Name',
help_text='Hostname or FQDN (not case-sensitive)'
)
description = models.CharField(
max_length=100,
blank=True
@@ -596,11 +561,11 @@ class IPAddress(ChangeLoggedModel, CustomFieldModel):
)
objects = IPAddressManager()
tags = TaggableManager(through=TaggedItem)
tags = TaggableManager()
csv_headers = [
'address', 'vrf', 'tenant', 'status', 'role', 'device', 'virtual_machine', 'interface_name', 'is_primary',
'dns_name', 'description',
'description',
]
class Meta:
@@ -637,37 +602,11 @@ class IPAddress(ChangeLoggedModel, CustomFieldModel):
})
def save(self, *args, **kwargs):
# Record address family
if isinstance(self.address, netaddr.IPNetwork):
if self.address:
# Infer address family from IPAddress object
self.family = self.address.version
# Force dns_name to lowercase
self.dns_name = self.dns_name.lower()
super().save(*args, **kwargs)
def log_change(self, user, request_id, action):
"""
Include the connected Interface (if any).
"""
# It's possible that an IPAddress can be deleted _after_ its parent Interface, in which case trying to resolve
# the interface will raise DoesNotExist.
try:
parent_obj = self.interface
except ObjectDoesNotExist:
parent_obj = None
ObjectChange(
user=user,
request_id=request_id,
changed_object=self,
related_object=parent_obj,
action=action,
object_data=serialize_object(self)
).save()
def to_csv(self):
# Determine if this IP is primary for a Device
@@ -688,19 +627,9 @@ class IPAddress(ChangeLoggedModel, CustomFieldModel):
self.virtual_machine.name if self.virtual_machine else None,
self.interface.name if self.interface else None,
is_primary,
self.dns_name,
self.description,
)
def _set_mask_length(self, value):
"""
Expose the IPNetwork object's prefixlen attribute on the parent model so that it can be manipulated directly,
e.g. for bulk editing.
"""
if self.address is not None:
self.address.prefixlen = value
mask_length = property(fset=_set_mask_length)
@property
def device(self):
if self.interface:
@@ -830,7 +759,7 @@ class VLAN(ChangeLoggedModel, CustomFieldModel):
object_id_field='obj_id'
)
tags = TaggableManager(through=TaggedItem)
tags = TaggableManager()
csv_headers = ['site', 'group_name', 'vid', 'name', 'tenant', 'status', 'role', 'description']
@@ -932,7 +861,7 @@ class Service(ChangeLoggedModel, CustomFieldModel):
object_id_field='obj_id'
)
tags = TaggableManager(through=TaggedItem)
tags = TaggableManager()
csv_headers = ['device', 'virtual_machine', 'name', 'protocol', 'description']

View File

@@ -1,7 +1,7 @@
from django.db.models import QuerySet
from utilities.sql import NullsFirstQuerySet
class PrefixQuerySet(QuerySet):
class PrefixQuerySet(NullsFirstQuerySet):
def annotate_depth(self, limit=None):
"""

View File

@@ -30,7 +30,7 @@ RIR_ACTIONS = """
<i class="fa fa-history"></i>
</a>
{% if perms.ipam.change_rir %}
<a href="{% url 'ipam:rir_edit' slug=record.slug %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
<a href="{% url 'ipam:rir_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
{% endif %}
"""
@@ -52,7 +52,7 @@ ROLE_ACTIONS = """
<i class="fa fa-history"></i>
</a>
{% if perms.ipam.change_role %}
<a href="{% url 'ipam:role_edit' slug=record.slug %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
<a href="{% url 'ipam:role_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
{% endif %}
"""
@@ -152,7 +152,7 @@ VLANGROUP_ACTIONS = """
{% endif %}
{% endwith %}
{% if perms.ipam.change_vlangroup %}
<a href="{% url 'ipam:vlangroup_edit' pk=record.pk %}?return_url={{ request.path }}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
<a href="{% url 'ipam:vlangroup_edit' pk=record.pk %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
{% endif %}
"""
@@ -203,7 +203,7 @@ class RIRTable(BaseTable):
name = tables.LinkColumn(verbose_name='Name')
is_private = BooleanColumn(verbose_name='Private')
aggregate_count = tables.Column(verbose_name='Aggregates')
actions = tables.TemplateColumn(template_code=RIR_ACTIONS, attrs={'td': {'class': 'text-right noprint'}}, verbose_name='')
actions = tables.TemplateColumn(template_code=RIR_ACTIONS, attrs={'td': {'class': 'text-right'}}, verbose_name='')
class Meta(BaseTable.Meta):
model = RIR
@@ -288,7 +288,7 @@ class RoleTable(BaseTable):
orderable=False,
verbose_name='VLANs'
)
actions = tables.TemplateColumn(template_code=ROLE_ACTIONS, attrs={'td': {'class': 'text-right noprint'}}, verbose_name='')
actions = tables.TemplateColumn(template_code=ROLE_ACTIONS, attrs={'td': {'class': 'text-right'}}, verbose_name='')
class Meta(BaseTable.Meta):
model = Role
@@ -339,9 +339,7 @@ class IPAddressTable(BaseTable):
class Meta(BaseTable.Meta):
model = IPAddress
fields = (
'pk', 'address', 'vrf', 'status', 'role', 'tenant', 'parent', 'interface', 'dns_name', 'description',
)
fields = ('pk', 'address', 'vrf', 'status', 'role', 'tenant', 'parent', 'interface', 'description')
row_attrs = {
'class': lambda record: 'success' if not isinstance(record, IPAddress) else '',
}
@@ -354,8 +352,7 @@ class IPAddressDetailTable(IPAddressTable):
class Meta(IPAddressTable.Meta):
fields = (
'pk', 'address', 'vrf', 'status', 'role', 'tenant', 'nat_inside', 'parent', 'interface', 'dns_name',
'description',
'pk', 'address', 'vrf', 'status', 'role', 'tenant', 'nat_inside', 'parent', 'interface', 'description',
)
@@ -395,7 +392,7 @@ class VLANGroupTable(BaseTable):
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
vlan_count = tables.Column(verbose_name='VLANs')
slug = tables.Column(verbose_name='Slug')
actions = tables.TemplateColumn(template_code=VLANGROUP_ACTIONS, attrs={'td': {'class': 'text-right noprint'}},
actions = tables.TemplateColumn(template_code=VLANGROUP_ACTIONS, attrs={'td': {'class': 'text-right'}},
verbose_name='')
class Meta(BaseTable.Meta):
@@ -440,7 +437,7 @@ class VLANMemberTable(BaseTable):
)
actions = tables.TemplateColumn(
template_code=VLAN_MEMBER_ACTIONS,
attrs={'td': {'class': 'text-right noprint'}},
attrs={'td': {'class': 'text-right'}},
verbose_name=''
)

View File

@@ -16,7 +16,7 @@ class VRFTest(APITestCase):
self.vrf1 = VRF.objects.create(name='Test VRF 1', rd='65000:1')
self.vrf2 = VRF.objects.create(name='Test VRF 2', rd='65000:2')
self.vrf3 = VRF.objects.create(name='Test VRF 3') # No RD
self.vrf3 = VRF.objects.create(name='Test VRF 3', rd='65000:3')
def test_get_vrf(self):
@@ -39,31 +39,24 @@ class VRFTest(APITestCase):
self.assertEqual(
sorted(response.data['results'][0]),
['id', 'name', 'prefix_count', 'rd', 'url']
['id', 'name', 'rd', 'url']
)
def test_create_vrf(self):
data_list = [
# VRF with RD
{
'name': 'Test VRF 4',
'rd': '65000:4',
},
# VRF without RD
{
'name': 'Test VRF 5',
}
]
data = {
'name': 'Test VRF 4',
'rd': '65000:4',
}
url = reverse('ipam-api:vrf-list')
response = self.client.post(url, data, format='json', **self.header)
for data in data_list:
response = self.client.post(url, data, format='json', **self.header)
self.assertHttpStatus(response, status.HTTP_201_CREATED)
vrf = VRF.objects.get(pk=response.data['id'])
self.assertEqual(vrf.name, data['name'])
self.assertEqual(vrf.rd, data['rd'] if 'rd' in data else None)
self.assertHttpStatus(response, status.HTTP_201_CREATED)
self.assertEqual(VRF.objects.count(), 4)
vrf4 = VRF.objects.get(pk=response.data['id'])
self.assertEqual(vrf4.name, data['name'])
self.assertEqual(vrf4.rd, data['rd'])
def test_create_vrf_bulk(self):
@@ -147,7 +140,7 @@ class RIRTest(APITestCase):
self.assertEqual(
sorted(response.data['results'][0]),
['aggregate_count', 'id', 'name', 'slug', 'url']
['id', 'name', 'slug', 'url']
)
def test_create_rir(self):
@@ -351,7 +344,7 @@ class RoleTest(APITestCase):
self.assertEqual(
sorted(response.data['results'][0]),
['id', 'name', 'prefix_count', 'slug', 'url', 'vlan_count']
['id', 'name', 'slug', 'url']
)
def test_create_role(self):
@@ -790,7 +783,7 @@ class VLANGroupTest(APITestCase):
self.assertEqual(
sorted(response.data['results'][0]),
['id', 'name', 'slug', 'url', 'vlan_count']
['id', 'name', 'slug', 'url']
)
def test_create_vlangroup(self):

View File

@@ -1,286 +0,0 @@
from netaddr import IPNetwork
import urllib.parse
from django.test import Client, TestCase
from django.urls import reverse
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
from ipam.constants import IP_PROTOCOL_TCP
from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
from utilities.testing import create_test_user
class VRFTestCase(TestCase):
def setUp(self):
user = create_test_user(permissions=['ipam.view_vrf'])
self.client = Client()
self.client.force_login(user)
VRF.objects.bulk_create([
VRF(name='VRF 1', rd='65000:1'),
VRF(name='VRF 2', rd='65000:2'),
VRF(name='VRF 3', rd='65000:3'),
])
def test_vrf_list(self):
url = reverse('ipam:vrf_list')
params = {
"q": "65000",
}
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
self.assertEqual(response.status_code, 200)
def test_configcontext(self):
vrf = VRF.objects.first()
response = self.client.get(vrf.get_absolute_url())
self.assertEqual(response.status_code, 200)
class RIRTestCase(TestCase):
def setUp(self):
user = create_test_user(permissions=['ipam.view_rir'])
self.client = Client()
self.client.force_login(user)
RIR.objects.bulk_create([
RIR(name='RIR 1', slug='rir-1'),
RIR(name='RIR 2', slug='rir-2'),
RIR(name='RIR 3', slug='rir-3'),
])
def test_rir_list(self):
url = reverse('ipam:rir_list')
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
class AggregateTestCase(TestCase):
def setUp(self):
user = create_test_user(permissions=['ipam.view_aggregate'])
self.client = Client()
self.client.force_login(user)
rir = RIR(name='RIR 1', slug='rir-1')
rir.save()
Aggregate.objects.bulk_create([
Aggregate(family=4, prefix=IPNetwork('10.1.0.0/16'), rir=rir),
Aggregate(family=4, prefix=IPNetwork('10.2.0.0/16'), rir=rir),
Aggregate(family=4, prefix=IPNetwork('10.3.0.0/16'), rir=rir),
])
def test_aggregate_list(self):
url = reverse('ipam:aggregate_list')
params = {
"rir": RIR.objects.first().slug,
}
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
self.assertEqual(response.status_code, 200)
def test_aggregate(self):
aggregate = Aggregate.objects.first()
response = self.client.get(aggregate.get_absolute_url())
self.assertEqual(response.status_code, 200)
class RoleTestCase(TestCase):
def setUp(self):
user = create_test_user(permissions=['ipam.view_role'])
self.client = Client()
self.client.force_login(user)
Role.objects.bulk_create([
Role(name='Role 1', slug='role-1'),
Role(name='Role 2', slug='role-2'),
Role(name='Role 3', slug='role-3'),
])
def test_role_list(self):
url = reverse('ipam:role_list')
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
class PrefixTestCase(TestCase):
def setUp(self):
user = create_test_user(permissions=['ipam.view_prefix'])
self.client = Client()
self.client.force_login(user)
site = Site(name='Site 1', slug='site-1')
site.save()
Prefix.objects.bulk_create([
Prefix(family=4, prefix=IPNetwork('10.1.0.0/16'), site=site),
Prefix(family=4, prefix=IPNetwork('10.2.0.0/16'), site=site),
Prefix(family=4, prefix=IPNetwork('10.3.0.0/16'), site=site),
])
def test_prefix_list(self):
url = reverse('ipam:prefix_list')
params = {
"site": Site.objects.first().slug,
}
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
self.assertEqual(response.status_code, 200)
def test_prefix(self):
prefix = Prefix.objects.first()
response = self.client.get(prefix.get_absolute_url())
self.assertEqual(response.status_code, 200)
class IPAddressTestCase(TestCase):
def setUp(self):
user = create_test_user(permissions=['ipam.view_ipaddress'])
self.client = Client()
self.client.force_login(user)
vrf = VRF(name='VRF 1', rd='65000:1')
vrf.save()
IPAddress.objects.bulk_create([
IPAddress(family=4, address=IPNetwork('10.1.0.0/16'), vrf=vrf),
IPAddress(family=4, address=IPNetwork('10.2.0.0/16'), vrf=vrf),
IPAddress(family=4, address=IPNetwork('10.3.0.0/16'), vrf=vrf),
])
def test_ipaddress_list(self):
url = reverse('ipam:ipaddress_list')
params = {
"vrf": VRF.objects.first().rd,
}
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
self.assertEqual(response.status_code, 200)
def test_ipaddress(self):
ipaddress = IPAddress.objects.first()
response = self.client.get(ipaddress.get_absolute_url())
self.assertEqual(response.status_code, 200)
class VLANGroupTestCase(TestCase):
def setUp(self):
user = create_test_user(permissions=['ipam.view_vlangroup'])
self.client = Client()
self.client.force_login(user)
site = Site(name='Site 1', slug='site-1')
site.save()
VLANGroup.objects.bulk_create([
VLANGroup(name='VLAN Group 1', slug='vlan-group-1', site=site),
VLANGroup(name='VLAN Group 2', slug='vlan-group-2', site=site),
VLANGroup(name='VLAN Group 3', slug='vlan-group-3', site=site),
])
def test_vlangroup_list(self):
url = reverse('ipam:vlangroup_list')
params = {
"site": Site.objects.first().slug,
}
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
self.assertEqual(response.status_code, 200)
class VLANTestCase(TestCase):
def setUp(self):
user = create_test_user(permissions=['ipam.view_vlan'])
self.client = Client()
self.client.force_login(user)
vlangroup = VLANGroup(name='VLAN Group 1', slug='vlan-group-1')
vlangroup.save()
VLAN.objects.bulk_create([
VLAN(group=vlangroup, vid=101, name='VLAN101'),
VLAN(group=vlangroup, vid=102, name='VLAN102'),
VLAN(group=vlangroup, vid=103, name='VLAN103'),
])
def test_vlan_list(self):
url = reverse('ipam:vlan_list')
params = {
"group": VLANGroup.objects.first().slug,
}
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
self.assertEqual(response.status_code, 200)
def test_vlan(self):
vlan = VLAN.objects.first()
response = self.client.get(vlan.get_absolute_url())
self.assertEqual(response.status_code, 200)
class ServiceTestCase(TestCase):
def setUp(self):
user = create_test_user(permissions=['ipam.view_service'])
self.client = Client()
self.client.force_login(user)
site = Site(name='Site 1', slug='site-1')
site.save()
manufacturer = Manufacturer(name='Manufacturer 1', slug='manufacturer-1')
manufacturer.save()
devicetype = DeviceType(manufacturer=manufacturer, model='Device Type 1')
devicetype.save()
devicerole = DeviceRole(name='Device Role 1', slug='device-role-1')
devicerole.save()
device = Device(name='Device 1', site=site, device_type=devicetype, device_role=devicerole)
device.save()
Service.objects.bulk_create([
Service(device=device, name='Service 1', protocol=IP_PROTOCOL_TCP, port=101),
Service(device=device, name='Service 2', protocol=IP_PROTOCOL_TCP, port=102),
Service(device=device, name='Service 3', protocol=IP_PROTOCOL_TCP, port=103),
])
def test_service_list(self):
url = reverse('ipam:service_list')
params = {
"device_id": Device.objects.first(),
}
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
self.assertEqual(response.status_code, 200)
def test_service(self):
service = Service.objects.first()
response = self.client.get(service.get_absolute_url())
self.assertEqual(response.status_code, 200)

View File

@@ -1,8 +0,0 @@
from django.core.validators import RegexValidator
DNSValidator = RegexValidator(
regex='^[0-9A-Za-z.-]+$',
message='Only alphanumeric characters, hyphens, and periods are allowed in DNS names',
code='invalid'
)

View File

@@ -113,8 +113,7 @@ def add_available_vlans(vlan_group, vlans):
# VRFs
#
class VRFListView(PermissionRequiredMixin, ObjectListView):
permission_required = 'ipam.view_vrf'
class VRFListView(ObjectListView):
queryset = VRF.objects.select_related('tenant')
filter = filters.VRFFilter
filter_form = forms.VRFFilterForm
@@ -122,17 +121,19 @@ class VRFListView(PermissionRequiredMixin, ObjectListView):
template_name = 'ipam/vrf_list.html'
class VRFView(PermissionRequiredMixin, View):
permission_required = 'ipam.view_vrf'
class VRFView(View):
def get(self, request, pk):
vrf = get_object_or_404(VRF.objects.all(), pk=pk)
prefix_count = Prefix.objects.filter(vrf=vrf).count()
prefix_table = tables.PrefixTable(
list(Prefix.objects.filter(vrf=vrf).select_related('site', 'role')), orderable=False
)
prefix_table.exclude = ('vrf',)
return render(request, 'ipam/vrf.html', {
'vrf': vrf,
'prefix_count': prefix_count,
'prefix_table': prefix_table,
})
@@ -182,8 +183,7 @@ class VRFBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
# RIRs
#
class RIRListView(PermissionRequiredMixin, ObjectListView):
permission_required = 'ipam.view_rir'
class RIRListView(ObjectListView):
queryset = RIR.objects.annotate(aggregate_count=Count('aggregates'))
filter = filters.RIRFilter
filter_form = forms.RIRFilterForm
@@ -289,8 +289,7 @@ class RIRBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
# Aggregates
#
class AggregateListView(PermissionRequiredMixin, ObjectListView):
permission_required = 'ipam.view_aggregate'
class AggregateListView(ObjectListView):
queryset = Aggregate.objects.select_related('rir').extra(select={
'child_count': 'SELECT COUNT(*) FROM ipam_prefix WHERE ipam_prefix.prefix <<= ipam_aggregate.prefix',
})
@@ -316,8 +315,7 @@ class AggregateListView(PermissionRequiredMixin, ObjectListView):
}
class AggregateView(PermissionRequiredMixin, View):
permission_required = 'ipam.view_aggregate'
class AggregateView(View):
def get(self, request, pk):
@@ -403,8 +401,7 @@ class AggregateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
# Prefix/VLAN roles
#
class RoleListView(PermissionRequiredMixin, ObjectListView):
permission_required = 'ipam.view_role'
class RoleListView(ObjectListView):
queryset = Role.objects.all()
table = tables.RoleTable
template_name = 'ipam/role_list.html'
@@ -439,8 +436,7 @@ class RoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
# Prefixes
#
class PrefixListView(PermissionRequiredMixin, ObjectListView):
permission_required = 'ipam.view_prefix'
class PrefixListView(ObjectListView):
queryset = Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role')
filter = filters.PrefixFilter
filter_form = forms.PrefixFilterForm
@@ -453,8 +449,7 @@ class PrefixListView(PermissionRequiredMixin, ObjectListView):
return self.queryset.annotate_depth(limit=limit)
class PrefixView(PermissionRequiredMixin, View):
permission_required = 'ipam.view_prefix'
class PrefixView(View):
def get(self, request, pk):
@@ -497,8 +492,7 @@ class PrefixView(PermissionRequiredMixin, View):
})
class PrefixPrefixesView(PermissionRequiredMixin, View):
permission_required = 'ipam.view_prefix'
class PrefixPrefixesView(View):
def get(self, request, pk):
@@ -540,8 +534,7 @@ class PrefixPrefixesView(PermissionRequiredMixin, View):
})
class PrefixIPAddressesView(PermissionRequiredMixin, View):
permission_required = 'ipam.view_prefix'
class PrefixIPAddressesView(View):
def get(self, request, pk):
@@ -627,8 +620,7 @@ class PrefixBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
# IP addresses
#
class IPAddressListView(PermissionRequiredMixin, ObjectListView):
permission_required = 'ipam.view_ipaddress'
class IPAddressListView(ObjectListView):
queryset = IPAddress.objects.select_related(
'vrf__tenant', 'tenant', 'nat_inside'
).prefetch_related(
@@ -640,8 +632,7 @@ class IPAddressListView(PermissionRequiredMixin, ObjectListView):
template_name = 'ipam/ipaddress_list.html'
class IPAddressView(PermissionRequiredMixin, View):
permission_required = 'ipam.view_ipaddress'
class IPAddressView(View):
def get(self, request, pk):
@@ -800,8 +791,7 @@ class IPAddressBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
# VLAN groups
#
class VLANGroupListView(PermissionRequiredMixin, ObjectListView):
permission_required = 'ipam.view_vlangroup'
class VLANGroupListView(ObjectListView):
queryset = VLANGroup.objects.select_related('site').annotate(vlan_count=Count('vlans'))
filter = filters.VLANGroupFilter
filter_form = forms.VLANGroupFilterForm
@@ -835,9 +825,7 @@ class VLANGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
default_return_url = 'ipam:vlangroup_list'
class VLANGroupVLANsView(PermissionRequiredMixin, View):
permission_required = 'ipam.view_vlangroup'
class VLANGroupVLANsView(View):
def get(self, request, pk):
vlan_group = get_object_or_404(VLANGroup.objects.all(), pk=pk)
@@ -876,8 +864,7 @@ class VLANGroupVLANsView(PermissionRequiredMixin, View):
# VLANs
#
class VLANListView(PermissionRequiredMixin, ObjectListView):
permission_required = 'ipam.view_vlan'
class VLANListView(ObjectListView):
queryset = VLAN.objects.select_related('site', 'group', 'tenant', 'role').prefetch_related('prefixes')
filter = filters.VLANFilter
filter_form = forms.VLANFilterForm
@@ -885,8 +872,7 @@ class VLANListView(PermissionRequiredMixin, ObjectListView):
template_name = 'ipam/vlan_list.html'
class VLANView(PermissionRequiredMixin, View):
permission_required = 'ipam.view_vlan'
class VLANView(View):
def get(self, request, pk):
@@ -903,8 +889,7 @@ class VLANView(PermissionRequiredMixin, View):
})
class VLANMembersView(PermissionRequiredMixin, View):
permission_required = 'ipam.view_vlan'
class VLANMembersView(View):
def get(self, request, pk):
@@ -972,8 +957,7 @@ class VLANBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
# Services
#
class ServiceListView(PermissionRequiredMixin, ObjectListView):
permission_required = 'ipam.view_service'
class ServiceListView(ObjectListView):
queryset = Service.objects.select_related('device', 'virtual_machine')
filter = filters.ServiceFilter
filter_form = forms.ServiceFilterForm
@@ -981,8 +965,7 @@ class ServiceListView(PermissionRequiredMixin, ObjectListView):
template_name = 'ipam/service_list.html'
class ServiceView(PermissionRequiredMixin, View):
permission_required = 'ipam.view_service'
class ServiceView(View):
def get(self, request, pk):

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