mirror of
https://github.com/netbox-community/netbox.git
synced 2026-01-13 22:03:32 +01:00
Compare commits
94 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c1f9dba52 | ||
|
|
ea1df2b5c3 | ||
|
|
b3423e1722 | ||
|
|
bfb91fcf10 | ||
|
|
44c62f8f44 | ||
|
|
c8c47961db | ||
|
|
78b0e50742 | ||
|
|
a7371c048b | ||
|
|
f3dfa81811 | ||
|
|
5b4793a2d5 | ||
|
|
b6660c72e1 | ||
|
|
a6eeed4061 | ||
|
|
239fddcac2 | ||
|
|
b27f9bf74c | ||
|
|
09b856bf0b | ||
|
|
9954c6a571 | ||
|
|
44b24de5d0 | ||
|
|
22927bfc76 | ||
|
|
a39522a25e | ||
|
|
ea6c8a1a65 | ||
|
|
546bbe5418 | ||
|
|
5ca7f375d3 | ||
|
|
568148a349 | ||
|
|
fedf745d25 | ||
|
|
8823aeb9d7 | ||
|
|
dc57332988 | ||
|
|
138231059b | ||
|
|
834b233c30 | ||
|
|
72d41eac85 | ||
|
|
0fec03ad3f | ||
|
|
7dc71f92d0 | ||
|
|
f74b47ca16 | ||
|
|
4dff20cc8c | ||
|
|
c855570b55 | ||
|
|
395add8114 | ||
|
|
019a5563c4 | ||
|
|
e9b21aaf86 | ||
|
|
d6a0cbb1a0 | ||
|
|
3900b97136 | ||
|
|
2d4ae38a09 | ||
|
|
7f2f98885b | ||
|
|
a4955b420a | ||
|
|
fe78f60b1f | ||
|
|
21d14a782e | ||
|
|
c0f1243879 | ||
|
|
67945f2f33 | ||
|
|
30ffa4c3f2 | ||
|
|
97d5873e3d | ||
|
|
fb1173bc30 | ||
|
|
f0acaa16c4 | ||
|
|
3bd99e1910 | ||
|
|
ad19b09ae3 | ||
|
|
ffa4cd134b | ||
|
|
fbffef1cc4 | ||
|
|
2a402b632d | ||
|
|
3bba1089ed | ||
|
|
c9c8108a53 | ||
|
|
067fdaeb8f | ||
|
|
b6e862bd10 | ||
|
|
f1cdd72575 | ||
|
|
c59c4290f9 | ||
|
|
fd9d9d9d35 | ||
|
|
2a5b497d8a | ||
|
|
b93570eeb0 | ||
|
|
3ef6284a0d | ||
|
|
1024782b9e | ||
|
|
d35ac1347c | ||
|
|
c4e88fd11a | ||
|
|
0de50e0afe | ||
|
|
763b02975c | ||
|
|
cc57d1edf7 | ||
|
|
bb988701fe | ||
|
|
75fdff4d41 | ||
|
|
8fc49f37a7 | ||
|
|
51f6d2f45e | ||
|
|
1abaa5e60c | ||
|
|
f4792eea04 | ||
|
|
1fe1e1ce7e | ||
|
|
58659cf3b6 | ||
|
|
9a588231c5 | ||
|
|
f408ad16e4 | ||
|
|
fecca5ad83 | ||
|
|
9e5d41c48a | ||
|
|
87b4cfff1a | ||
|
|
19a2b9042f | ||
|
|
2618dde1e2 | ||
|
|
1dd9f8c1d4 | ||
|
|
4939b6b641 | ||
|
|
52747e364a | ||
|
|
1901e93b1e | ||
|
|
648b9dd7d8 | ||
|
|
396c91f8f7 | ||
|
|
83f520f7a8 | ||
|
|
07f39b31da |
1
.github/FUNDING.yml
vendored
1
.github/FUNDING.yml
vendored
@@ -1 +0,0 @@
|
||||
github: [jeremystretch]
|
||||
40
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
40
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@@ -5,21 +5,25 @@ labels: ["type: bug"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: "**NOTE:** This form is only for reporting _reproducible bugs_ in a
|
||||
current NetBox installation. If you're having trouble with installation or just
|
||||
looking for assistance with using NetBox, please visit our
|
||||
[discussion forum](https://github.com/netbox-community/netbox/discussions) instead."
|
||||
value: >
|
||||
**NOTE:** This form is only for reporting _reproducible bugs_ in a current NetBox
|
||||
installation. If you're having trouble with installation or just looking for
|
||||
assistance with using NetBox, please visit our
|
||||
[discussion forum](https://github.com/netbox-community/netbox/discussions) instead.
|
||||
- type: input
|
||||
attributes:
|
||||
label: NetBox version
|
||||
description: "What version of NetBox are you currently running?"
|
||||
placeholder: v2.10.4
|
||||
description: >
|
||||
What version of NetBox are you currently running? (If you don't have access to the most
|
||||
recent NetBox release, consider testing on our [demo instance](https://demo.netbox.dev/)
|
||||
before opening a bug report to see if your issue has already been addressed.)
|
||||
placeholder: v2.11.4
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Python version
|
||||
description: "What version of Python are you currently running?"
|
||||
description: What version of Python are you currently running?
|
||||
options:
|
||||
- 3.6
|
||||
- 3.7
|
||||
@@ -30,12 +34,14 @@ body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Steps to Reproduce
|
||||
description: "Describe in detail the exact steps that someone else can take to
|
||||
reproduce this bug using the current stable release of NetBox. 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 client library such as
|
||||
pynetbox."
|
||||
description: >
|
||||
Describe in detail the exact steps that someone else can take to
|
||||
reproduce this bug using the current stable release of NetBox. 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 client library such as
|
||||
pynetbox. Additionally, **do not rely on the demo instance** for reproducing
|
||||
suspected bugs, as its data is prone to modification or deletion at any time.
|
||||
placeholder: |
|
||||
1. Click on "create widget"
|
||||
2. Set foo to 12 and bar to G
|
||||
@@ -45,14 +51,14 @@ body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Expected Behavior
|
||||
description: "What did you expect to happen?"
|
||||
placeholder: "A new widget should have been created with the specified attributes"
|
||||
description: What did you expect to happen?
|
||||
placeholder: A new widget should have been created with the specified attributes
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Observed Behavior
|
||||
description: "What happened instead?"
|
||||
placeholder: "A TypeError exception was raised"
|
||||
description: What happened instead?
|
||||
placeholder: A TypeError exception was raised
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -30,6 +30,6 @@ body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Proposed Changes
|
||||
description: "Describe the proposed changes and why they are necessary"
|
||||
description: Describe the proposed changes and why they are necessary.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
36
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
36
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
@@ -5,15 +5,16 @@ labels: ["type: feature"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: "**NOTE:** This form is only for submitting well-formed proposals to extend or
|
||||
modify NetBox in some way. If you're trying to solve a problem but can't figure out how,
|
||||
or if you still need time to work on the details of a proposed new feature, please start
|
||||
a [discussion](https://github.com/netbox-community/netbox/discussions) instead."
|
||||
value: >
|
||||
**NOTE:** This form is only for submitting well-formed proposals to extend or modify
|
||||
NetBox in some way. If you're trying to solve a problem but can't figure out how, or if
|
||||
you still need time to work on the details of a proposed new feature, please start a
|
||||
[discussion](https://github.com/netbox-community/netbox/discussions) instead.
|
||||
- type: input
|
||||
attributes:
|
||||
label: NetBox version
|
||||
description: "What version of NetBox are you currently running?"
|
||||
placeholder: v2.10.4
|
||||
description: What version of NetBox are you currently running?
|
||||
placeholder: v2.11.4
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
@@ -28,26 +29,29 @@ body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Proposed functionality
|
||||
description: "Describe in detail the new feature or behavior you'd like to propose.
|
||||
Include any specific changes to work flows, data models, or the user interface."
|
||||
description: >
|
||||
Describe in detail the new feature or behavior you'd like to propose. Include any specific
|
||||
changes to work flows, data models, or the user interface.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Use case
|
||||
description: "Explain how adding this functionality would benefit NetBox users. What
|
||||
need does it address?"
|
||||
description: >
|
||||
Explain how adding this functionality would benefit NetBox users. What need does it address?
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Database changes
|
||||
description: "Note any changes to the database schema necessary to support the new
|
||||
feature. For example, does the proposal require adding a new model or field? (Not
|
||||
all new features require database changes.)"
|
||||
description: >
|
||||
Note any changes to the database schema necessary to support the new feature. For example,
|
||||
does the proposal require adding a new model or field? (Not all new features require database
|
||||
changes.)
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: External dependencies
|
||||
description: "List any new dependencies on external libraries or services that this
|
||||
new feature would introduce. For example, does the proposal require the installation
|
||||
of a new Python package? (Not all new features introduce new dependencies.)"
|
||||
description: >
|
||||
List any new dependencies on external libraries or services that this new feature would
|
||||
introduce. For example, does the proposal require the installation of a new Python package?
|
||||
(Not all new features introduce new dependencies.)
|
||||
|
||||
12
.github/ISSUE_TEMPLATE/housekeeping.yaml
vendored
12
.github/ISSUE_TEMPLATE/housekeeping.yaml
vendored
@@ -5,18 +5,20 @@ labels: ["type: housekeeping"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: "**NOTE:** This template is for use by maintainers only. Please do not submit
|
||||
an issue using this template unless you have been specifically asked to do so."
|
||||
value: >
|
||||
**NOTE:** This template is for use by maintainers only. Please do not submit
|
||||
an issue using this template unless you have been specifically asked to do so.
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Proposed Changes
|
||||
description: "Describe in detail the new feature or behavior you'd like to propose.
|
||||
Include any specific changes to work flows, data models, or the user interface."
|
||||
description: >
|
||||
Describe in detail the new feature or behavior you'd like to propose.
|
||||
Include any specific changes to work flows, data models, or the user interface.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Justification
|
||||
description: "Please provide justification for the proposed change(s)."
|
||||
description: Please provide justification for the proposed change(s).
|
||||
validations:
|
||||
required: true
|
||||
|
||||
5
.github/workflows/stale.yml
vendored
5
.github/workflows/stale.yml
vendored
@@ -17,9 +17,10 @@ jobs:
|
||||
necessary.
|
||||
close-pr-message: >
|
||||
This PR has been automatically closed due to lack of activity.
|
||||
days-before-stale: 45
|
||||
days-before-close: 15
|
||||
days-before-stale: 60
|
||||
days-before-close: 30
|
||||
exempt-issue-labels: 'status: accepted,status: blocked,status: needs milestone'
|
||||
operations-per-run: 100
|
||||
remove-stale-when-updated: false
|
||||
stale-issue-label: 'pending closure'
|
||||
stale-issue-message: >
|
||||
|
||||
55
README.md
55
README.md
@@ -1,4 +1,6 @@
|
||||

|
||||
<div align="center">
|
||||
<img src="https://raw.githubusercontent.com/netbox-community/netbox/develop/docs/netbox_logo.svg" width="400" alt="NetBox logo" />
|
||||
</div>
|
||||
|
||||
NetBox is an IP address management (IPAM) and data center infrastructure
|
||||
management (DCIM) tool. Initially conceived by the network engineering team at
|
||||
@@ -10,7 +12,23 @@ NetBox runs as a web application atop the [Django](https://www.djangoproject.com
|
||||
Python framework with a [PostgreSQL](https://www.postgresql.org/) database. For a
|
||||
complete list of requirements, see `requirements.txt`. The code is available [on GitHub](https://github.com/netbox-community/netbox).
|
||||
|
||||
The complete documentation for NetBox can be found at [Read the Docs](https://netbox.readthedocs.io/en/stable/).
|
||||
The complete documentation for NetBox can be found at [Read the Docs](https://netbox.readthedocs.io/en/stable/). A public demo instance is available at https://demo.netbox.dev.
|
||||
|
||||
| | status |
|
||||
|-------------|------------|
|
||||
| **master** |  |
|
||||
| **develop** |  |
|
||||
|
||||
<div align="center">
|
||||
<h4>Thank you to our sponsors!</h4>
|
||||
|
||||
[](https://try.digitalocean.com/developer-cloud)
|
||||
|
||||
[](https://ns1.com/)
|
||||
|
||||
[](https://stellar.tech/)
|
||||
|
||||
</div>
|
||||
|
||||
### Discussion
|
||||
|
||||
@@ -18,33 +36,14 @@ The complete documentation for NetBox can be found at [Read the Docs](https://ne
|
||||
* [Slack](https://slack.netbox.dev/) - Real-time chat hosted by the NetDev Community; best for unstructured discussion or just hanging out
|
||||
* [Google Group](https://groups.google.com/g/netbox-discuss) - Legacy mailing list; slowly being replaced by GitHub discussions
|
||||
|
||||
### Build Status
|
||||
|
||||
| | status |
|
||||
|-------------|------------|
|
||||
| **master** |  |
|
||||
| **develop** |  |
|
||||
|
||||
### Screenshots
|
||||
|
||||

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

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

|
||||
|
||||
## Installation
|
||||
### Installation
|
||||
|
||||
Please see [the documentation](https://netbox.readthedocs.io/en/stable/) for
|
||||
instructions on installing NetBox. To upgrade NetBox, please download the
|
||||
[latest release](https://github.com/netbox-community/netbox/releases) and
|
||||
run `upgrade.sh`.
|
||||
|
||||
## Providing Feedback
|
||||
### Providing Feedback
|
||||
|
||||
The best platform for general feedback, assistance, and other discussion is our
|
||||
[GitHub discussions](https://github.com/netbox-community/netbox/discussions).
|
||||
@@ -54,7 +53,15 @@ the [appropriate template](https://github.com/netbox-community/netbox/issues/new
|
||||
If you are interested in contributing to the development of NetBox, please read
|
||||
our [contributing guide](CONTRIBUTING.md) prior to beginning any work.
|
||||
|
||||
## Related projects
|
||||
### Screenshots
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
### Related projects
|
||||
|
||||
Please see [our wiki](https://github.com/netbox-community/netbox/wiki/Community-Contributions)
|
||||
for a list of relevant community projects.
|
||||
|
||||
@@ -6,7 +6,7 @@ If a change is made to any of the objects returned by the query within that time
|
||||
|
||||
## Invalidating Cached Data
|
||||
|
||||
Although caching is performed automatically and rarely requires administrative intervention, NetBox provides the `invalidate` management command to force invalidation of cached results. This command can reference a specific object my its type and numeric ID:
|
||||
Although caching is performed automatically and rarely requires administrative intervention, NetBox provides the `invalidate` management command to force invalidation of cached results. This command can reference a specific object by its type and numeric ID:
|
||||
|
||||
```no-highlight
|
||||
$ python netbox/manage.py invalidate dcim.Device.34
|
||||
|
||||
@@ -515,6 +515,14 @@ The file path to the location where custom scripts will be kept. By default, thi
|
||||
|
||||
---
|
||||
|
||||
## SESSION_COOKIE_NAME
|
||||
|
||||
Default: `sessionid`
|
||||
|
||||
The name used for the session cookie. See the [Django documentation](https://docs.djangoproject.com/en/stable/ref/settings/#session-cookie-name) for more detail.
|
||||
|
||||
---
|
||||
|
||||
## SESSION_FILE_PATH
|
||||
|
||||
Default: None
|
||||
|
||||
@@ -70,7 +70,11 @@ Ensure that continuous integration testing on the `develop` branch is completing
|
||||
|
||||
### Update Version and Changelog
|
||||
|
||||
Update the `VERSION` constant in `settings.py` to the new release version and annotate the current data in the release notes for the new version. Commit these changes to the `develop` branch.
|
||||
* Update the `VERSION` constant in `settings.py` to the new release version.
|
||||
* Update the example version numbers in the feature request and bug report templates under `.github/ISSUE_TEMPLATES/`.
|
||||
* Replace the "FUTURE" placeholder in the release notes with the current date.
|
||||
|
||||
Commit these changes to the `develop` branch.
|
||||
|
||||
### Submit a Pull Request
|
||||
|
||||
|
||||
@@ -198,7 +198,7 @@ All Python packages required by NetBox are listed in `requirements.txt` and will
|
||||
The [NAPALM automation](https://napalm-automation.net/) library allows NetBox to fetch live data from devices and return it to a requester via its REST API. The `NAPALM_USERNAME` and `NAPALM_PASSWORD` configuration parameters define the credentials to be used when connecting to a device.
|
||||
|
||||
```no-highlight
|
||||
sudo echo napalm >> /opt/netbox/local_requirements.txt
|
||||
sudo sh -c "echo 'napalm' >> /opt/netbox/local_requirements.txt"
|
||||
```
|
||||
|
||||
### Remote File Storage
|
||||
@@ -206,7 +206,7 @@ sudo echo napalm >> /opt/netbox/local_requirements.txt
|
||||
By default, NetBox will use the local filesystem to store uploaded files. To use a remote filesystem, install the [`django-storages`](https://django-storages.readthedocs.io/en/stable/) library and configure your [desired storage backend](../configuration/optional-settings.md#storage_backend) in `configuration.py`.
|
||||
|
||||
```no-highlight
|
||||
sudo echo django-storages >> /opt/netbox/local_requirements.txt
|
||||
sudo sh -c "echo 'django-storages' >> /opt/netbox/local_requirements.txt"
|
||||
```
|
||||
|
||||
## Run the Upgrade Script
|
||||
|
||||
@@ -30,7 +30,7 @@ pip3 install django-auth-ldap
|
||||
Once installed, add the package to `local_requirements.txt` to ensure it is re-installed during future rebuilds of the virtual environment:
|
||||
|
||||
```no-highlight
|
||||
sudo echo django-auth-ldap >> /opt/netbox/local_requirements.txt
|
||||
sudo sh -c "echo 'django-auth-ldap' >> /opt/netbox/local_requirements.txt"
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Power Feed
|
||||
|
||||
A power feed represents the distribution of power from a power panel to a particular device, typically a power distribution unit (PDU). The power pot (inlet) on a device can be connected via a cable to a power feed. A power feed may optionally be assigned to a rack to allow more easily tracking the distribution of power among racks.
|
||||
A power feed represents the distribution of power from a power panel to a particular device, typically a power distribution unit (PDU). The power port (inlet) on a device can be connected via a cable to a power feed. A power feed may optionally be assigned to a rack to allow more easily tracking the distribution of power among racks.
|
||||
|
||||
Each power feed is assigned an operational type (primary or redundant) and one of the following statuses:
|
||||
|
||||
|
||||
@@ -1,5 +1,77 @@
|
||||
# NetBox v2.11
|
||||
|
||||
## v2.11.4 (2021-05-25)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#5121](https://github.com/netbox-community/netbox/issues/5121) - Add content type filters for tags
|
||||
* [#6358](https://github.com/netbox-community/netbox/issues/6358) - Add search field for VLAN groups
|
||||
* [#6393](https://github.com/netbox-community/netbox/issues/6393) - Add `description` filter for IP addresses
|
||||
* [#6400](https://github.com/netbox-community/netbox/issues/6400) - Add cyan color choice for plugin buttons
|
||||
* [#6422](https://github.com/netbox-community/netbox/issues/6422) - Enable filtering users by group under admin UI
|
||||
* [#6441](https://github.com/netbox-community/netbox/issues/6441) - Improve UI paginator to optimize page object count
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#6376](https://github.com/netbox-community/netbox/issues/6376) - Fix assignment of VLAN groups to clusters, cluster groups via REST API
|
||||
* [#6398](https://github.com/netbox-community/netbox/issues/6398) - Avoid exception when deleting device connected to self via circuit
|
||||
* [#6426](https://github.com/netbox-community/netbox/issues/6426) - Allow assigning virtual chassis member interfaces to LAG on VC master
|
||||
* [#6438](https://github.com/netbox-community/netbox/issues/6438) - Fix missing descriptions and label for device type imports and exports
|
||||
* [#6465](https://github.com/netbox-community/netbox/issues/6465) - Fix typo in installed plugins REST API endpoint
|
||||
* [#6467](https://github.com/netbox-community/netbox/issues/6467) - Fix access to metrics on custom `BASE_PATH` when login is required
|
||||
* [#6468](https://github.com/netbox-community/netbox/issues/6468) - Disable ordering VLAN groups list by scope object
|
||||
|
||||
---
|
||||
|
||||
## v2.11.3 (2021-05-07)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#6197](https://github.com/netbox-community/netbox/issues/6197) - Introduced `SESSION_COOKIE_NAME` config parameter
|
||||
* [#6318](https://github.com/netbox-community/netbox/issues/6318) - Add OM5 MMF cable type
|
||||
* [#6351](https://github.com/netbox-community/netbox/issues/6351) - Add aggregates count to tenant view
|
||||
* [#6359](https://github.com/netbox-community/netbox/issues/6359) - Enable custom links for organizational and nested group models
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#6240](https://github.com/netbox-community/netbox/issues/6240) - Fix display of available VLAN ranges under VLAN group view
|
||||
* [#6308](https://github.com/netbox-community/netbox/issues/6308) - Fix linking of available VLANs in VLAN group view
|
||||
* [#6309](https://github.com/netbox-community/netbox/issues/6309) - Restrict parent VM interface assignment to the parent VM
|
||||
* [#6312](https://github.com/netbox-community/netbox/issues/6312) - Interface device filter should return all virtual chassis interfaces only if device is master
|
||||
* [#6313](https://github.com/netbox-community/netbox/issues/6313) - Fix device type instance count under manufacturer view
|
||||
* [#6321](https://github.com/netbox-community/netbox/issues/6321) - Restore "add an IP" button under prefix IPs view
|
||||
* [#6333](https://github.com/netbox-community/netbox/issues/6333) - Fix filtering of circuit terminations by primary key
|
||||
* [#6339](https://github.com/netbox-community/netbox/issues/6339) - Improve ordering of interfaces when viewing virtual chassis master
|
||||
* [#6350](https://github.com/netbox-community/netbox/issues/6350) - Include first & last IP addresses when allocating available IPv6 addresses via the REST API
|
||||
* [#6355](https://github.com/netbox-community/netbox/issues/6355) - Fix caching error when swapping A/Z circuit terminations
|
||||
* [#6357](https://github.com/netbox-community/netbox/issues/6357) - Fix ProviderNetwork nested API serializer
|
||||
* [#6363](https://github.com/netbox-community/netbox/issues/6363) - Correct pre-population of cluster group when creating a cluster
|
||||
* [#6369](https://github.com/netbox-community/netbox/issues/6369) - Fix interface assignment for VLANs in non-scoped groups
|
||||
|
||||
---
|
||||
|
||||
## v2.11.2 (2021-04-27)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#6275](https://github.com/netbox-community/netbox/issues/6275) - Linkify rack, device counts on locations list
|
||||
* [#6278](https://github.com/netbox-community/netbox/issues/6278) - Note device locations on cable traces
|
||||
* [#6287](https://github.com/netbox-community/netbox/issues/6287) - Add option to clear assigned max length filter on prefixes list
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#6236](https://github.com/netbox-community/netbox/issues/6236) - Journal entry title should account for configured timezone
|
||||
* [#6246](https://github.com/netbox-community/netbox/issues/6246) - Permit full-length descriptions when creating device components and VM interfaces
|
||||
* [#6248](https://github.com/netbox-community/netbox/issues/6248) - Fix table column reconfiguration under Chrome
|
||||
* [#6252](https://github.com/netbox-community/netbox/issues/6252) - Fix assignment of console port speed values above 19.2kbps
|
||||
* [#6254](https://github.com/netbox-community/netbox/issues/6254) - Disable ordering of space column in racks table
|
||||
* [#6258](https://github.com/netbox-community/netbox/issues/6258) - Fix parent assignment for SiteGroup API serializer
|
||||
* [#6262](https://github.com/netbox-community/netbox/issues/6262) - Support filtering by created/updated time for all relevant objects
|
||||
* [#6267](https://github.com/netbox-community/netbox/issues/6267) - Fix cable tracing API endpoint for circuit terminations
|
||||
* [#6289](https://github.com/netbox-community/netbox/issues/6289) - Fix assignment of VC member interfaces to LAG interfaces
|
||||
|
||||
---
|
||||
|
||||
## v2.11.1 (2021-04-21)
|
||||
|
||||
### Enhancements
|
||||
@@ -175,6 +247,7 @@ A new provider network model has been introduced to represent the boundary of a
|
||||
* circuits.CircuitTermination
|
||||
* Added the `provider_network` field
|
||||
* Removed the `connected_endpoint`, `connected_endpoint_type`, and `connected_endpoint_reachable` fields
|
||||
* The `trace/` endpoint has been replaced with `paths/`
|
||||
* circuits.ProviderNetwork
|
||||
* Added the `/api/circuits/provider-networks/` endpoint
|
||||
* dcim.Device
|
||||
|
||||
@@ -20,7 +20,7 @@ class NestedProviderNetworkSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:providernetwork-detail')
|
||||
|
||||
class Meta:
|
||||
model = Provider
|
||||
model = ProviderNetwork
|
||||
fields = ['id', 'url', 'display', 'name']
|
||||
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
from rest_framework.routers import APIRootView
|
||||
|
||||
from circuits import filters
|
||||
from circuits import filtersets
|
||||
from circuits.models import *
|
||||
from dcim.api.views import PathEndpointMixin
|
||||
from dcim.api.views import PassThroughPortMixin
|
||||
from extras.api.views import CustomFieldModelViewSet
|
||||
from netbox.api.views import ModelViewSet
|
||||
from utilities.utils import count_related
|
||||
@@ -26,7 +26,7 @@ class ProviderViewSet(CustomFieldModelViewSet):
|
||||
circuit_count=count_related(Circuit, 'provider')
|
||||
)
|
||||
serializer_class = serializers.ProviderSerializer
|
||||
filterset_class = filters.ProviderFilterSet
|
||||
filterset_class = filtersets.ProviderFilterSet
|
||||
|
||||
|
||||
#
|
||||
@@ -38,7 +38,7 @@ class CircuitTypeViewSet(CustomFieldModelViewSet):
|
||||
circuit_count=count_related(Circuit, 'type')
|
||||
)
|
||||
serializer_class = serializers.CircuitTypeSerializer
|
||||
filterset_class = filters.CircuitTypeFilterSet
|
||||
filterset_class = filtersets.CircuitTypeFilterSet
|
||||
|
||||
|
||||
#
|
||||
@@ -50,19 +50,19 @@ class CircuitViewSet(CustomFieldModelViewSet):
|
||||
'type', 'tenant', 'provider', 'termination_a', 'termination_z'
|
||||
).prefetch_related('tags')
|
||||
serializer_class = serializers.CircuitSerializer
|
||||
filterset_class = filters.CircuitFilterSet
|
||||
filterset_class = filtersets.CircuitFilterSet
|
||||
|
||||
|
||||
#
|
||||
# Circuit Terminations
|
||||
#
|
||||
|
||||
class CircuitTerminationViewSet(PathEndpointMixin, ModelViewSet):
|
||||
class CircuitTerminationViewSet(PassThroughPortMixin, ModelViewSet):
|
||||
queryset = CircuitTermination.objects.prefetch_related(
|
||||
'circuit', 'site', 'provider_network', 'cable'
|
||||
)
|
||||
serializer_class = serializers.CircuitTerminationSerializer
|
||||
filterset_class = filters.CircuitTerminationFilterSet
|
||||
filterset_class = filtersets.CircuitTerminationFilterSet
|
||||
brief_prefetch_fields = ['circuit']
|
||||
|
||||
|
||||
@@ -73,4 +73,4 @@ class CircuitTerminationViewSet(PathEndpointMixin, ModelViewSet):
|
||||
class ProviderNetworkViewSet(CustomFieldModelViewSet):
|
||||
queryset = ProviderNetwork.objects.prefetch_related('tags')
|
||||
serializer_class = serializers.ProviderNetworkSerializer
|
||||
filterset_class = filters.ProviderNetworkFilterSet
|
||||
filterset_class = filtersets.ProviderNetworkFilterSet
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import django_filters
|
||||
from django.db.models import Q
|
||||
|
||||
from dcim.filters import CableTerminationFilterSet, PathEndpointFilterSet
|
||||
from dcim.filtersets import CableTerminationFilterSet
|
||||
from dcim.models import Region, Site, SiteGroup
|
||||
from extras.filters import CustomFieldModelFilterSet, CreatedUpdatedFilterSet
|
||||
from tenancy.filters import TenancyFilterSet
|
||||
from utilities.filters import (
|
||||
BaseFilterSet, NameSlugSearchFilterSet, TagFilter, TreeNodeMultipleChoiceFilter
|
||||
)
|
||||
from extras.filters import TagFilter
|
||||
from netbox.filtersets import ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, PrimaryModelFilterSet
|
||||
from tenancy.filtersets import TenancyFilterSet
|
||||
from utilities.filters import TreeNodeMultipleChoiceFilter
|
||||
from .choices import *
|
||||
from .models import *
|
||||
|
||||
@@ -20,7 +19,7 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
class ProviderFilterSet(BaseFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
|
||||
class ProviderFilterSet(PrimaryModelFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
@@ -80,7 +79,7 @@ class ProviderFilterSet(BaseFilterSet, CustomFieldModelFilterSet, CreatedUpdated
|
||||
)
|
||||
|
||||
|
||||
class ProviderNetworkFilterSet(BaseFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
|
||||
class ProviderNetworkFilterSet(PrimaryModelFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
@@ -110,14 +109,14 @@ class ProviderNetworkFilterSet(BaseFilterSet, CustomFieldModelFilterSet, Created
|
||||
).distinct()
|
||||
|
||||
|
||||
class CircuitTypeFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
|
||||
class CircuitTypeFilterSet(OrganizationalModelFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = CircuitType
|
||||
fields = ['id', 'name', 'slug']
|
||||
|
||||
|
||||
class CircuitFilterSet(BaseFilterSet, CustomFieldModelFilterSet, TenancyFilterSet, CreatedUpdatedFilterSet):
|
||||
class CircuitFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
@@ -207,7 +206,7 @@ class CircuitFilterSet(BaseFilterSet, CustomFieldModelFilterSet, TenancyFilterSe
|
||||
).distinct()
|
||||
|
||||
|
||||
class CircuitTerminationFilterSet(BaseFilterSet, CableTerminationFilterSet):
|
||||
class CircuitTerminationFilterSet(ChangeLoggedModelFilterSet, CableTerminationFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
@@ -233,7 +232,7 @@ class CircuitTerminationFilterSet(BaseFilterSet, CableTerminationFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = CircuitTermination
|
||||
fields = ['term_side', 'port_speed', 'upstream_speed', 'xconnect_id']
|
||||
fields = ['id', 'term_side', 'port_speed', 'upstream_speed', 'xconnect_id']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
@@ -20,7 +20,7 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||
class Provider(PrimaryModel):
|
||||
"""
|
||||
Each Circuit belongs to a Provider. This is usually a telecommunications company or similar organization. This model
|
||||
@@ -96,7 +96,7 @@ class Provider(PrimaryModel):
|
||||
# Provider networks
|
||||
#
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||
class ProviderNetwork(PrimaryModel):
|
||||
"""
|
||||
This represents a provider network which exists outside of NetBox, the details of which are unknown or
|
||||
@@ -149,7 +149,7 @@ class ProviderNetwork(PrimaryModel):
|
||||
)
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'export_templates', 'webhooks')
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
class CircuitType(OrganizationalModel):
|
||||
"""
|
||||
Circuits can be organized by their functional role. For example, a user might wish to define CircuitTypes named
|
||||
@@ -189,7 +189,7 @@ class CircuitType(OrganizationalModel):
|
||||
)
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||
class Circuit(PrimaryModel):
|
||||
"""
|
||||
A communications circuit connects two points. Each Circuit belongs to a Provider; Providers may have multiple
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
from django.db.models.signals import post_delete, post_save
|
||||
from django.dispatch import receiver
|
||||
from django.utils import timezone
|
||||
|
||||
from dcim.signals import rebuild_paths
|
||||
from .models import Circuit, CircuitTermination
|
||||
from .models import CircuitTermination
|
||||
|
||||
|
||||
@receiver(post_save, sender=CircuitTermination)
|
||||
@@ -11,11 +10,9 @@ def update_circuit(instance, **kwargs):
|
||||
"""
|
||||
When a CircuitTermination has been modified, update its parent Circuit.
|
||||
"""
|
||||
fields = {
|
||||
'last_updated': timezone.now(),
|
||||
f'termination_{instance.term_side.lower()}': instance.pk,
|
||||
}
|
||||
Circuit.objects.filter(pk=instance.circuit_id).update(**fields)
|
||||
termination_name = f'termination_{instance.term_side.lower()}'
|
||||
setattr(instance.circuit, termination_name, instance)
|
||||
instance.circuit.save()
|
||||
|
||||
|
||||
@receiver((post_save, post_delete), sender=CircuitTermination)
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
from django.test import TestCase
|
||||
|
||||
from circuits.choices import *
|
||||
from circuits.filters import *
|
||||
from circuits.filtersets import *
|
||||
from circuits.models import *
|
||||
from dcim.models import Cable, Region, Site, SiteGroup
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from utilities.testing import ChangeLoggedFilterSetTests
|
||||
|
||||
|
||||
class ProviderTestCase(TestCase):
|
||||
class ProviderTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = Provider.objects.all()
|
||||
filterset = ProviderFilterSet
|
||||
|
||||
@@ -61,10 +62,6 @@ class ProviderTestCase(TestCase):
|
||||
CircuitTermination(circuit=circuits[1], site=sites[0], term_side='A'),
|
||||
))
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Provider 1', 'Provider 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -103,7 +100,7 @@ class ProviderTestCase(TestCase):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
class CircuitTypeTestCase(TestCase):
|
||||
class CircuitTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = CircuitType.objects.all()
|
||||
filterset = CircuitTypeFilterSet
|
||||
|
||||
@@ -116,10 +113,6 @@ class CircuitTypeTestCase(TestCase):
|
||||
CircuitType(name='Circuit Type 3', slug='circuit-type-3'),
|
||||
))
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': [self.queryset.first().pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Circuit Type 1']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
@@ -129,7 +122,7 @@ class CircuitTypeTestCase(TestCase):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
|
||||
class CircuitTestCase(TestCase):
|
||||
class CircuitTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = Circuit.objects.all()
|
||||
filterset = CircuitFilterSet
|
||||
|
||||
@@ -213,10 +206,6 @@ class CircuitTestCase(TestCase):
|
||||
))
|
||||
CircuitTermination.objects.bulk_create(circuit_terminations)
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_cid(self):
|
||||
params = {'cid': ['Test Circuit 1', 'Test Circuit 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -288,7 +277,7 @@ class CircuitTestCase(TestCase):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
|
||||
class CircuitTerminationTestCase(TestCase):
|
||||
class CircuitTerminationTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = CircuitTermination.objects.all()
|
||||
filterset = CircuitTerminationFilterSet
|
||||
|
||||
@@ -382,7 +371,7 @@ class CircuitTerminationTestCase(TestCase):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
class ProviderNetworkTestCase(TestCase):
|
||||
class ProviderNetworkTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = ProviderNetwork.objects.all()
|
||||
filterset = ProviderNetworkFilterSet
|
||||
|
||||
@@ -403,10 +392,6 @@ class ProviderNetworkTestCase(TestCase):
|
||||
)
|
||||
ProviderNetwork.objects.bulk_create(provider_networks)
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Provider Network 1', 'Provider Network 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -7,7 +7,7 @@ from netbox.views import generic
|
||||
from utilities.forms import ConfirmationForm
|
||||
from utilities.tables import paginate_table
|
||||
from utilities.utils import count_related
|
||||
from . import filters, forms, tables
|
||||
from . import filtersets, forms, tables
|
||||
from .choices import CircuitTerminationSideChoices
|
||||
from .models import *
|
||||
|
||||
@@ -20,7 +20,7 @@ class ProviderListView(generic.ObjectListView):
|
||||
queryset = Provider.objects.annotate(
|
||||
count_circuits=count_related(Circuit, 'provider')
|
||||
)
|
||||
filterset = filters.ProviderFilterSet
|
||||
filterset = filtersets.ProviderFilterSet
|
||||
filterset_form = forms.ProviderFilterForm
|
||||
table = tables.ProviderTable
|
||||
|
||||
@@ -63,7 +63,7 @@ class ProviderBulkEditView(generic.BulkEditView):
|
||||
queryset = Provider.objects.annotate(
|
||||
count_circuits=count_related(Circuit, 'provider')
|
||||
)
|
||||
filterset = filters.ProviderFilterSet
|
||||
filterset = filtersets.ProviderFilterSet
|
||||
table = tables.ProviderTable
|
||||
form = forms.ProviderBulkEditForm
|
||||
|
||||
@@ -72,7 +72,7 @@ class ProviderBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = Provider.objects.annotate(
|
||||
count_circuits=count_related(Circuit, 'provider')
|
||||
)
|
||||
filterset = filters.ProviderFilterSet
|
||||
filterset = filtersets.ProviderFilterSet
|
||||
table = tables.ProviderTable
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@ class ProviderBulkDeleteView(generic.BulkDeleteView):
|
||||
|
||||
class ProviderNetworkListView(generic.ObjectListView):
|
||||
queryset = ProviderNetwork.objects.all()
|
||||
filterset = filters.ProviderNetworkFilterSet
|
||||
filterset = filtersets.ProviderNetworkFilterSet
|
||||
filterset_form = forms.ProviderNetworkFilterForm
|
||||
table = tables.ProviderNetworkTable
|
||||
|
||||
@@ -125,14 +125,14 @@ class ProviderNetworkBulkImportView(generic.BulkImportView):
|
||||
|
||||
class ProviderNetworkBulkEditView(generic.BulkEditView):
|
||||
queryset = ProviderNetwork.objects.all()
|
||||
filterset = filters.ProviderNetworkFilterSet
|
||||
filterset = filtersets.ProviderNetworkFilterSet
|
||||
table = tables.ProviderNetworkTable
|
||||
form = forms.ProviderNetworkBulkEditForm
|
||||
|
||||
|
||||
class ProviderNetworkBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = ProviderNetwork.objects.all()
|
||||
filterset = filters.ProviderNetworkFilterSet
|
||||
filterset = filtersets.ProviderNetworkFilterSet
|
||||
table = tables.ProviderNetworkTable
|
||||
|
||||
|
||||
@@ -183,7 +183,7 @@ class CircuitTypeBulkEditView(generic.BulkEditView):
|
||||
queryset = CircuitType.objects.annotate(
|
||||
circuit_count=count_related(Circuit, 'type')
|
||||
)
|
||||
filterset = filters.CircuitTypeFilterSet
|
||||
filterset = filtersets.CircuitTypeFilterSet
|
||||
table = tables.CircuitTypeTable
|
||||
form = forms.CircuitTypeBulkEditForm
|
||||
|
||||
@@ -203,7 +203,7 @@ class CircuitListView(generic.ObjectListView):
|
||||
queryset = Circuit.objects.prefetch_related(
|
||||
'provider', 'type', 'tenant', 'termination_a', 'termination_z'
|
||||
)
|
||||
filterset = filters.CircuitFilterSet
|
||||
filterset = filtersets.CircuitFilterSet
|
||||
filterset_form = forms.CircuitFilterForm
|
||||
table = tables.CircuitTable
|
||||
|
||||
@@ -211,27 +211,6 @@ class CircuitListView(generic.ObjectListView):
|
||||
class CircuitView(generic.ObjectView):
|
||||
queryset = Circuit.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
|
||||
# A-side termination
|
||||
termination_a = CircuitTermination.objects.restrict(request.user, 'view').prefetch_related(
|
||||
'site__region'
|
||||
).filter(
|
||||
circuit=instance, term_side=CircuitTerminationSideChoices.SIDE_A
|
||||
).first()
|
||||
|
||||
# Z-side termination
|
||||
termination_z = CircuitTermination.objects.restrict(request.user, 'view').prefetch_related(
|
||||
'site__region'
|
||||
).filter(
|
||||
circuit=instance, term_side=CircuitTerminationSideChoices.SIDE_Z
|
||||
).first()
|
||||
|
||||
return {
|
||||
'termination_a': termination_a,
|
||||
'termination_z': termination_z,
|
||||
}
|
||||
|
||||
|
||||
class CircuitEditView(generic.ObjectEditView):
|
||||
queryset = Circuit.objects.all()
|
||||
@@ -252,7 +231,7 @@ class CircuitBulkEditView(generic.BulkEditView):
|
||||
queryset = Circuit.objects.prefetch_related(
|
||||
'provider', 'type', 'tenant', 'terminations'
|
||||
)
|
||||
filterset = filters.CircuitFilterSet
|
||||
filterset = filtersets.CircuitFilterSet
|
||||
table = tables.CircuitTable
|
||||
form = forms.CircuitBulkEditForm
|
||||
|
||||
@@ -261,7 +240,7 @@ class CircuitBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = Circuit.objects.prefetch_related(
|
||||
'provider', 'type', 'tenant', 'terminations'
|
||||
)
|
||||
filterset = filters.CircuitFilterSet
|
||||
filterset = filtersets.CircuitFilterSet
|
||||
table = tables.CircuitTable
|
||||
|
||||
|
||||
@@ -296,16 +275,11 @@ class CircuitSwapTerminations(generic.ObjectEditView):
|
||||
|
||||
if form.is_valid():
|
||||
|
||||
termination_a = CircuitTermination.objects.filter(
|
||||
circuit=circuit, term_side=CircuitTerminationSideChoices.SIDE_A
|
||||
).first()
|
||||
termination_z = CircuitTermination.objects.filter(
|
||||
circuit=circuit, term_side=CircuitTerminationSideChoices.SIDE_Z
|
||||
).first()
|
||||
termination_a = CircuitTermination.objects.filter(pk=circuit.termination_a_id).first()
|
||||
termination_z = CircuitTermination.objects.filter(pk=circuit.termination_z_id).first()
|
||||
|
||||
if termination_a and termination_z:
|
||||
# Use a placeholder to avoid an IntegrityError on the (circuit, term_side) unique constraint
|
||||
print('swapping')
|
||||
with transaction.atomic():
|
||||
termination_a.term_side = '_'
|
||||
termination_a.save()
|
||||
@@ -316,11 +290,20 @@ class CircuitSwapTerminations(generic.ObjectEditView):
|
||||
elif termination_a:
|
||||
termination_a.term_side = 'Z'
|
||||
termination_a.save()
|
||||
circuit.refresh_from_db()
|
||||
circuit.termination_a = None
|
||||
circuit.save()
|
||||
else:
|
||||
termination_z.term_side = 'A'
|
||||
termination_z.save()
|
||||
circuit.refresh_from_db()
|
||||
circuit.termination_z = None
|
||||
circuit.save()
|
||||
|
||||
messages.success(request, "Swapped terminations for circuit {}.".format(circuit))
|
||||
print(f'term A: {circuit.termination_a}')
|
||||
print(f'term Z: {circuit.termination_z}')
|
||||
|
||||
messages.success(request, f"Swapped terminations for circuit {circuit}.")
|
||||
return redirect('circuits:circuit', pk=circuit.pk)
|
||||
|
||||
return render(request, 'circuits/circuit_terminations_swap.html', {
|
||||
|
||||
@@ -90,7 +90,7 @@ class RegionSerializer(NestedGroupModelSerializer):
|
||||
|
||||
class SiteGroupSerializer(NestedGroupModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:sitegroup-detail')
|
||||
parent = NestedRegionSerializer(required=False, allow_null=True)
|
||||
parent = NestedSiteGroupSerializer(required=False, allow_null=True)
|
||||
site_count = serializers.IntegerField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
|
||||
@@ -16,7 +16,7 @@ from rest_framework.routers import APIRootView
|
||||
from rest_framework.viewsets import GenericViewSet, ViewSet
|
||||
|
||||
from circuits.models import Circuit
|
||||
from dcim import filters
|
||||
from dcim import filtersets
|
||||
from dcim.models import *
|
||||
from extras.api.views import ConfigContextQuerySetMixin, CustomFieldModelViewSet
|
||||
from ipam.models import Prefix, VLAN
|
||||
@@ -103,7 +103,7 @@ class RegionViewSet(CustomFieldModelViewSet):
|
||||
cumulative=True
|
||||
)
|
||||
serializer_class = serializers.RegionSerializer
|
||||
filterset_class = filters.RegionFilterSet
|
||||
filterset_class = filtersets.RegionFilterSet
|
||||
|
||||
|
||||
#
|
||||
@@ -119,7 +119,7 @@ class SiteGroupViewSet(CustomFieldModelViewSet):
|
||||
cumulative=True
|
||||
)
|
||||
serializer_class = serializers.SiteGroupSerializer
|
||||
filterset_class = filters.SiteGroupFilterSet
|
||||
filterset_class = filtersets.SiteGroupFilterSet
|
||||
|
||||
|
||||
#
|
||||
@@ -138,7 +138,7 @@ class SiteViewSet(CustomFieldModelViewSet):
|
||||
virtualmachine_count=count_related(VirtualMachine, 'cluster__site')
|
||||
)
|
||||
serializer_class = serializers.SiteSerializer
|
||||
filterset_class = filters.SiteFilterSet
|
||||
filterset_class = filtersets.SiteFilterSet
|
||||
|
||||
|
||||
#
|
||||
@@ -160,7 +160,7 @@ class LocationViewSet(CustomFieldModelViewSet):
|
||||
cumulative=True
|
||||
).prefetch_related('site')
|
||||
serializer_class = serializers.LocationSerializer
|
||||
filterset_class = filters.LocationFilterSet
|
||||
filterset_class = filtersets.LocationFilterSet
|
||||
|
||||
|
||||
#
|
||||
@@ -172,7 +172,7 @@ class RackRoleViewSet(CustomFieldModelViewSet):
|
||||
rack_count=count_related(Rack, 'role')
|
||||
)
|
||||
serializer_class = serializers.RackRoleSerializer
|
||||
filterset_class = filters.RackRoleFilterSet
|
||||
filterset_class = filtersets.RackRoleFilterSet
|
||||
|
||||
|
||||
#
|
||||
@@ -187,7 +187,7 @@ class RackViewSet(CustomFieldModelViewSet):
|
||||
powerfeed_count=count_related(PowerFeed, 'rack')
|
||||
)
|
||||
serializer_class = serializers.RackSerializer
|
||||
filterset_class = filters.RackFilterSet
|
||||
filterset_class = filtersets.RackFilterSet
|
||||
|
||||
@swagger_auto_schema(
|
||||
responses={200: serializers.RackUnitSerializer(many=True)},
|
||||
@@ -244,7 +244,7 @@ class RackViewSet(CustomFieldModelViewSet):
|
||||
class RackReservationViewSet(ModelViewSet):
|
||||
queryset = RackReservation.objects.prefetch_related('rack', 'user', 'tenant')
|
||||
serializer_class = serializers.RackReservationSerializer
|
||||
filterset_class = filters.RackReservationFilterSet
|
||||
filterset_class = filtersets.RackReservationFilterSet
|
||||
|
||||
# Assign user from request
|
||||
def perform_create(self, serializer):
|
||||
@@ -262,7 +262,7 @@ class ManufacturerViewSet(CustomFieldModelViewSet):
|
||||
platform_count=count_related(Platform, 'manufacturer')
|
||||
)
|
||||
serializer_class = serializers.ManufacturerSerializer
|
||||
filterset_class = filters.ManufacturerFilterSet
|
||||
filterset_class = filtersets.ManufacturerFilterSet
|
||||
|
||||
|
||||
#
|
||||
@@ -274,7 +274,7 @@ class DeviceTypeViewSet(CustomFieldModelViewSet):
|
||||
device_count=count_related(Device, 'device_type')
|
||||
)
|
||||
serializer_class = serializers.DeviceTypeSerializer
|
||||
filterset_class = filters.DeviceTypeFilterSet
|
||||
filterset_class = filtersets.DeviceTypeFilterSet
|
||||
brief_prefetch_fields = ['manufacturer']
|
||||
|
||||
|
||||
@@ -285,49 +285,49 @@ class DeviceTypeViewSet(CustomFieldModelViewSet):
|
||||
class ConsolePortTemplateViewSet(ModelViewSet):
|
||||
queryset = ConsolePortTemplate.objects.prefetch_related('device_type__manufacturer')
|
||||
serializer_class = serializers.ConsolePortTemplateSerializer
|
||||
filterset_class = filters.ConsolePortTemplateFilterSet
|
||||
filterset_class = filtersets.ConsolePortTemplateFilterSet
|
||||
|
||||
|
||||
class ConsoleServerPortTemplateViewSet(ModelViewSet):
|
||||
queryset = ConsoleServerPortTemplate.objects.prefetch_related('device_type__manufacturer')
|
||||
serializer_class = serializers.ConsoleServerPortTemplateSerializer
|
||||
filterset_class = filters.ConsoleServerPortTemplateFilterSet
|
||||
filterset_class = filtersets.ConsoleServerPortTemplateFilterSet
|
||||
|
||||
|
||||
class PowerPortTemplateViewSet(ModelViewSet):
|
||||
queryset = PowerPortTemplate.objects.prefetch_related('device_type__manufacturer')
|
||||
serializer_class = serializers.PowerPortTemplateSerializer
|
||||
filterset_class = filters.PowerPortTemplateFilterSet
|
||||
filterset_class = filtersets.PowerPortTemplateFilterSet
|
||||
|
||||
|
||||
class PowerOutletTemplateViewSet(ModelViewSet):
|
||||
queryset = PowerOutletTemplate.objects.prefetch_related('device_type__manufacturer')
|
||||
serializer_class = serializers.PowerOutletTemplateSerializer
|
||||
filterset_class = filters.PowerOutletTemplateFilterSet
|
||||
filterset_class = filtersets.PowerOutletTemplateFilterSet
|
||||
|
||||
|
||||
class InterfaceTemplateViewSet(ModelViewSet):
|
||||
queryset = InterfaceTemplate.objects.prefetch_related('device_type__manufacturer')
|
||||
serializer_class = serializers.InterfaceTemplateSerializer
|
||||
filterset_class = filters.InterfaceTemplateFilterSet
|
||||
filterset_class = filtersets.InterfaceTemplateFilterSet
|
||||
|
||||
|
||||
class FrontPortTemplateViewSet(ModelViewSet):
|
||||
queryset = FrontPortTemplate.objects.prefetch_related('device_type__manufacturer')
|
||||
serializer_class = serializers.FrontPortTemplateSerializer
|
||||
filterset_class = filters.FrontPortTemplateFilterSet
|
||||
filterset_class = filtersets.FrontPortTemplateFilterSet
|
||||
|
||||
|
||||
class RearPortTemplateViewSet(ModelViewSet):
|
||||
queryset = RearPortTemplate.objects.prefetch_related('device_type__manufacturer')
|
||||
serializer_class = serializers.RearPortTemplateSerializer
|
||||
filterset_class = filters.RearPortTemplateFilterSet
|
||||
filterset_class = filtersets.RearPortTemplateFilterSet
|
||||
|
||||
|
||||
class DeviceBayTemplateViewSet(ModelViewSet):
|
||||
queryset = DeviceBayTemplate.objects.prefetch_related('device_type__manufacturer')
|
||||
serializer_class = serializers.DeviceBayTemplateSerializer
|
||||
filterset_class = filters.DeviceBayTemplateFilterSet
|
||||
filterset_class = filtersets.DeviceBayTemplateFilterSet
|
||||
|
||||
|
||||
#
|
||||
@@ -340,7 +340,7 @@ class DeviceRoleViewSet(CustomFieldModelViewSet):
|
||||
virtualmachine_count=count_related(VirtualMachine, 'role')
|
||||
)
|
||||
serializer_class = serializers.DeviceRoleSerializer
|
||||
filterset_class = filters.DeviceRoleFilterSet
|
||||
filterset_class = filtersets.DeviceRoleFilterSet
|
||||
|
||||
|
||||
#
|
||||
@@ -353,7 +353,7 @@ class PlatformViewSet(CustomFieldModelViewSet):
|
||||
virtualmachine_count=count_related(VirtualMachine, 'platform')
|
||||
)
|
||||
serializer_class = serializers.PlatformSerializer
|
||||
filterset_class = filters.PlatformFilterSet
|
||||
filterset_class = filtersets.PlatformFilterSet
|
||||
|
||||
|
||||
#
|
||||
@@ -365,7 +365,7 @@ class DeviceViewSet(ConfigContextQuerySetMixin, CustomFieldModelViewSet):
|
||||
'device_type__manufacturer', 'device_role', 'tenant', 'platform', 'site', 'location', 'rack', 'parent_bay',
|
||||
'virtual_chassis__master', 'primary_ip4__nat_outside', 'primary_ip6__nat_outside', 'tags',
|
||||
)
|
||||
filterset_class = filters.DeviceFilterSet
|
||||
filterset_class = filtersets.DeviceFilterSet
|
||||
|
||||
def get_serializer_class(self):
|
||||
"""
|
||||
@@ -510,7 +510,7 @@ class DeviceViewSet(ConfigContextQuerySetMixin, CustomFieldModelViewSet):
|
||||
class ConsolePortViewSet(PathEndpointMixin, ModelViewSet):
|
||||
queryset = ConsolePort.objects.prefetch_related('device', '_path__destination', 'cable', '_cable_peer', 'tags')
|
||||
serializer_class = serializers.ConsolePortSerializer
|
||||
filterset_class = filters.ConsolePortFilterSet
|
||||
filterset_class = filtersets.ConsolePortFilterSet
|
||||
brief_prefetch_fields = ['device']
|
||||
|
||||
|
||||
@@ -519,21 +519,21 @@ class ConsoleServerPortViewSet(PathEndpointMixin, ModelViewSet):
|
||||
'device', '_path__destination', 'cable', '_cable_peer', 'tags'
|
||||
)
|
||||
serializer_class = serializers.ConsoleServerPortSerializer
|
||||
filterset_class = filters.ConsoleServerPortFilterSet
|
||||
filterset_class = filtersets.ConsoleServerPortFilterSet
|
||||
brief_prefetch_fields = ['device']
|
||||
|
||||
|
||||
class PowerPortViewSet(PathEndpointMixin, ModelViewSet):
|
||||
queryset = PowerPort.objects.prefetch_related('device', '_path__destination', 'cable', '_cable_peer', 'tags')
|
||||
serializer_class = serializers.PowerPortSerializer
|
||||
filterset_class = filters.PowerPortFilterSet
|
||||
filterset_class = filtersets.PowerPortFilterSet
|
||||
brief_prefetch_fields = ['device']
|
||||
|
||||
|
||||
class PowerOutletViewSet(PathEndpointMixin, ModelViewSet):
|
||||
queryset = PowerOutlet.objects.prefetch_related('device', '_path__destination', 'cable', '_cable_peer', 'tags')
|
||||
serializer_class = serializers.PowerOutletSerializer
|
||||
filterset_class = filters.PowerOutletFilterSet
|
||||
filterset_class = filtersets.PowerOutletFilterSet
|
||||
brief_prefetch_fields = ['device']
|
||||
|
||||
|
||||
@@ -542,35 +542,35 @@ class InterfaceViewSet(PathEndpointMixin, ModelViewSet):
|
||||
'device', 'parent', 'lag', '_path__destination', 'cable', '_cable_peer', 'ip_addresses', 'tags'
|
||||
)
|
||||
serializer_class = serializers.InterfaceSerializer
|
||||
filterset_class = filters.InterfaceFilterSet
|
||||
filterset_class = filtersets.InterfaceFilterSet
|
||||
brief_prefetch_fields = ['device']
|
||||
|
||||
|
||||
class FrontPortViewSet(PassThroughPortMixin, ModelViewSet):
|
||||
queryset = FrontPort.objects.prefetch_related('device__device_type__manufacturer', 'rear_port', 'cable', 'tags')
|
||||
serializer_class = serializers.FrontPortSerializer
|
||||
filterset_class = filters.FrontPortFilterSet
|
||||
filterset_class = filtersets.FrontPortFilterSet
|
||||
brief_prefetch_fields = ['device']
|
||||
|
||||
|
||||
class RearPortViewSet(PassThroughPortMixin, ModelViewSet):
|
||||
queryset = RearPort.objects.prefetch_related('device__device_type__manufacturer', 'cable', 'tags')
|
||||
serializer_class = serializers.RearPortSerializer
|
||||
filterset_class = filters.RearPortFilterSet
|
||||
filterset_class = filtersets.RearPortFilterSet
|
||||
brief_prefetch_fields = ['device']
|
||||
|
||||
|
||||
class DeviceBayViewSet(ModelViewSet):
|
||||
queryset = DeviceBay.objects.prefetch_related('installed_device').prefetch_related('tags')
|
||||
serializer_class = serializers.DeviceBaySerializer
|
||||
filterset_class = filters.DeviceBayFilterSet
|
||||
filterset_class = filtersets.DeviceBayFilterSet
|
||||
brief_prefetch_fields = ['device']
|
||||
|
||||
|
||||
class InventoryItemViewSet(ModelViewSet):
|
||||
queryset = InventoryItem.objects.prefetch_related('device', 'manufacturer').prefetch_related('tags')
|
||||
serializer_class = serializers.InventoryItemSerializer
|
||||
filterset_class = filters.InventoryItemFilterSet
|
||||
filterset_class = filtersets.InventoryItemFilterSet
|
||||
brief_prefetch_fields = ['device']
|
||||
|
||||
|
||||
@@ -583,7 +583,7 @@ class ConsoleConnectionViewSet(ListModelMixin, GenericViewSet):
|
||||
_path__destination_id__isnull=False
|
||||
)
|
||||
serializer_class = serializers.ConsolePortSerializer
|
||||
filterset_class = filters.ConsoleConnectionFilterSet
|
||||
filterset_class = filtersets.ConsoleConnectionFilterSet
|
||||
|
||||
|
||||
class PowerConnectionViewSet(ListModelMixin, GenericViewSet):
|
||||
@@ -591,7 +591,7 @@ class PowerConnectionViewSet(ListModelMixin, GenericViewSet):
|
||||
_path__destination_id__isnull=False
|
||||
)
|
||||
serializer_class = serializers.PowerPortSerializer
|
||||
filterset_class = filters.PowerConnectionFilterSet
|
||||
filterset_class = filtersets.PowerConnectionFilterSet
|
||||
|
||||
|
||||
class InterfaceConnectionViewSet(ListModelMixin, GenericViewSet):
|
||||
@@ -603,7 +603,7 @@ class InterfaceConnectionViewSet(ListModelMixin, GenericViewSet):
|
||||
pk__lt=F('_path__destination_id')
|
||||
)
|
||||
serializer_class = serializers.InterfaceConnectionSerializer
|
||||
filterset_class = filters.InterfaceConnectionFilterSet
|
||||
filterset_class = filtersets.InterfaceConnectionFilterSet
|
||||
|
||||
|
||||
#
|
||||
@@ -616,7 +616,7 @@ class CableViewSet(ModelViewSet):
|
||||
'termination_a', 'termination_b'
|
||||
)
|
||||
serializer_class = serializers.CableSerializer
|
||||
filterset_class = filters.CableFilterSet
|
||||
filterset_class = filtersets.CableFilterSet
|
||||
|
||||
|
||||
#
|
||||
@@ -628,7 +628,7 @@ class VirtualChassisViewSet(ModelViewSet):
|
||||
member_count=count_related(Device, 'virtual_chassis')
|
||||
)
|
||||
serializer_class = serializers.VirtualChassisSerializer
|
||||
filterset_class = filters.VirtualChassisFilterSet
|
||||
filterset_class = filtersets.VirtualChassisFilterSet
|
||||
brief_prefetch_fields = ['master']
|
||||
|
||||
|
||||
@@ -643,7 +643,7 @@ class PowerPanelViewSet(ModelViewSet):
|
||||
powerfeed_count=count_related(PowerFeed, 'power_panel')
|
||||
)
|
||||
serializer_class = serializers.PowerPanelSerializer
|
||||
filterset_class = filters.PowerPanelFilterSet
|
||||
filterset_class = filtersets.PowerPanelFilterSet
|
||||
|
||||
|
||||
#
|
||||
@@ -655,7 +655,7 @@ class PowerFeedViewSet(PathEndpointMixin, CustomFieldModelViewSet):
|
||||
'power_panel', 'rack', '_path__destination', 'cable', '_cable_peer', 'tags'
|
||||
)
|
||||
serializer_class = serializers.PowerFeedSerializer
|
||||
filterset_class = filters.PowerFeedFilterSet
|
||||
filterset_class = filtersets.PowerFeedFilterSet
|
||||
|
||||
|
||||
#
|
||||
|
||||
@@ -1001,6 +1001,7 @@ class CableTypeChoices(ChoiceSet):
|
||||
TYPE_MMF_OM2 = 'mmf-om2'
|
||||
TYPE_MMF_OM3 = 'mmf-om3'
|
||||
TYPE_MMF_OM4 = 'mmf-om4'
|
||||
TYPE_MMF_OM5 = 'mmf-om5'
|
||||
TYPE_SMF = 'smf'
|
||||
TYPE_SMF_OS1 = 'smf-os1'
|
||||
TYPE_SMF_OS2 = 'smf-os2'
|
||||
@@ -1031,6 +1032,7 @@ class CableTypeChoices(ChoiceSet):
|
||||
(TYPE_MMF_OM2, 'Multimode Fiber (OM2)'),
|
||||
(TYPE_MMF_OM3, 'Multimode Fiber (OM3)'),
|
||||
(TYPE_MMF_OM4, 'Multimode Fiber (OM4)'),
|
||||
(TYPE_MMF_OM5, 'Multimode Fiber (OM5)'),
|
||||
(TYPE_SMF, 'Singlemode Fiber'),
|
||||
(TYPE_SMF_OS1, 'Singlemode Fiber (OS1)'),
|
||||
(TYPE_SMF_OS2, 'Singlemode Fiber (OS2)'),
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import django_filters
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
from extras.filters import CustomFieldModelFilterSet, LocalConfigContextFilterSet, CreatedUpdatedFilterSet
|
||||
from tenancy.filters import TenancyFilterSet
|
||||
from extras.filters import TagFilter
|
||||
from extras.filtersets import LocalConfigContextFilterSet
|
||||
from netbox.filtersets import (
|
||||
BaseFilterSet, ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, PrimaryModelFilterSet,
|
||||
)
|
||||
from tenancy.filtersets import TenancyFilterSet
|
||||
from tenancy.models import Tenant
|
||||
from utilities.choices import ColorChoices
|
||||
from utilities.filters import (
|
||||
BaseFilterSet, MultiValueCharFilter, MultiValueMACAddressFilter, MultiValueNumberFilter,
|
||||
NameSlugSearchFilterSet, TagFilter, TreeNodeMultipleChoiceFilter,
|
||||
MultiValueCharFilter, MultiValueMACAddressFilter, MultiValueNumberFilter, TreeNodeMultipleChoiceFilter,
|
||||
)
|
||||
from virtualization.models import Cluster
|
||||
from .choices import *
|
||||
@@ -57,7 +60,7 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
class RegionFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
|
||||
class RegionFilterSet(OrganizationalModelFilterSet):
|
||||
parent_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
label='Parent region (ID)',
|
||||
@@ -74,7 +77,7 @@ class RegionFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
|
||||
fields = ['id', 'name', 'slug', 'description']
|
||||
|
||||
|
||||
class SiteGroupFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
|
||||
class SiteGroupFilterSet(OrganizationalModelFilterSet):
|
||||
parent_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
label='Parent site group (ID)',
|
||||
@@ -91,7 +94,7 @@ class SiteGroupFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
|
||||
fields = ['id', 'name', 'slug', 'description']
|
||||
|
||||
|
||||
class SiteFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
|
||||
class SiteFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
@@ -154,7 +157,7 @@ class SiteFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet,
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
|
||||
class LocationFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
|
||||
class LocationFilterSet(OrganizationalModelFilterSet):
|
||||
region_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='site__region',
|
||||
@@ -218,14 +221,14 @@ class LocationFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
|
||||
)
|
||||
|
||||
|
||||
class RackRoleFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
|
||||
class RackRoleFilterSet(OrganizationalModelFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = RackRole
|
||||
fields = ['id', 'name', 'slug', 'color']
|
||||
|
||||
|
||||
class RackFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
|
||||
class RackFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
@@ -323,7 +326,7 @@ class RackFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet,
|
||||
)
|
||||
|
||||
|
||||
class RackReservationFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet):
|
||||
class RackReservationFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
@@ -383,14 +386,14 @@ class RackReservationFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModel
|
||||
)
|
||||
|
||||
|
||||
class ManufacturerFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
|
||||
class ManufacturerFilterSet(OrganizationalModelFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = Manufacturer
|
||||
fields = ['id', 'name', 'slug', 'description']
|
||||
|
||||
|
||||
class DeviceTypeFilterSet(BaseFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
|
||||
class DeviceTypeFilterSet(PrimaryModelFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
@@ -476,7 +479,7 @@ class DeviceTypeFilterSet(BaseFilterSet, CustomFieldModelFilterSet, CreatedUpdat
|
||||
return queryset.exclude(devicebaytemplates__isnull=value)
|
||||
|
||||
|
||||
class DeviceTypeComponentFilterSet(NameSlugSearchFilterSet):
|
||||
class DeviceTypeComponentFilterSet(django_filters.FilterSet):
|
||||
devicetype_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=DeviceType.objects.all(),
|
||||
field_name='device_type_id',
|
||||
@@ -484,28 +487,28 @@ class DeviceTypeComponentFilterSet(NameSlugSearchFilterSet):
|
||||
)
|
||||
|
||||
|
||||
class ConsolePortTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet):
|
||||
class ConsolePortTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = ConsolePortTemplate
|
||||
fields = ['id', 'name', 'type']
|
||||
|
||||
|
||||
class ConsoleServerPortTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet):
|
||||
class ConsoleServerPortTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = ConsoleServerPortTemplate
|
||||
fields = ['id', 'name', 'type']
|
||||
|
||||
|
||||
class PowerPortTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet):
|
||||
class PowerPortTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = PowerPortTemplate
|
||||
fields = ['id', 'name', 'type', 'maximum_draw', 'allocated_draw']
|
||||
|
||||
|
||||
class PowerOutletTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet):
|
||||
class PowerOutletTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
|
||||
feed_leg = django_filters.MultipleChoiceFilter(
|
||||
choices=PowerOutletFeedLegChoices,
|
||||
null_value=None
|
||||
@@ -516,7 +519,7 @@ class PowerOutletTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet):
|
||||
fields = ['id', 'name', 'type', 'feed_leg']
|
||||
|
||||
|
||||
class InterfaceTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet):
|
||||
class InterfaceTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
|
||||
type = django_filters.MultipleChoiceFilter(
|
||||
choices=InterfaceTypeChoices,
|
||||
null_value=None
|
||||
@@ -527,7 +530,7 @@ class InterfaceTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet):
|
||||
fields = ['id', 'name', 'type', 'mgmt_only']
|
||||
|
||||
|
||||
class FrontPortTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet):
|
||||
class FrontPortTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
|
||||
type = django_filters.MultipleChoiceFilter(
|
||||
choices=PortTypeChoices,
|
||||
null_value=None
|
||||
@@ -538,7 +541,7 @@ class FrontPortTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet):
|
||||
fields = ['id', 'name', 'type']
|
||||
|
||||
|
||||
class RearPortTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet):
|
||||
class RearPortTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
|
||||
type = django_filters.MultipleChoiceFilter(
|
||||
choices=PortTypeChoices,
|
||||
null_value=None
|
||||
@@ -549,21 +552,21 @@ class RearPortTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet):
|
||||
fields = ['id', 'name', 'type', 'positions']
|
||||
|
||||
|
||||
class DeviceBayTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet):
|
||||
class DeviceBayTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = DeviceBayTemplate
|
||||
fields = ['id', 'name']
|
||||
|
||||
|
||||
class DeviceRoleFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
|
||||
class DeviceRoleFilterSet(OrganizationalModelFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = DeviceRole
|
||||
fields = ['id', 'name', 'slug', 'color', 'vm_role']
|
||||
|
||||
|
||||
class PlatformFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
|
||||
class PlatformFilterSet(OrganizationalModelFilterSet):
|
||||
manufacturer_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='manufacturer',
|
||||
queryset=Manufacturer.objects.all(),
|
||||
@@ -581,13 +584,7 @@ class PlatformFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
|
||||
fields = ['id', 'name', 'slug', 'napalm_driver', 'description']
|
||||
|
||||
|
||||
class DeviceFilterSet(
|
||||
BaseFilterSet,
|
||||
TenancyFilterSet,
|
||||
LocalConfigContextFilterSet,
|
||||
CustomFieldModelFilterSet,
|
||||
CreatedUpdatedFilterSet
|
||||
):
|
||||
class DeviceFilterSet(PrimaryModelFilterSet, TenancyFilterSet, LocalConfigContextFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
@@ -792,7 +789,7 @@ class DeviceFilterSet(
|
||||
return queryset.exclude(devicebays__isnull=value)
|
||||
|
||||
|
||||
class DeviceComponentFilterSet(CustomFieldModelFilterSet):
|
||||
class DeviceComponentFilterSet(django_filters.FilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
@@ -876,7 +873,7 @@ class PathEndpointFilterSet(django_filters.FilterSet):
|
||||
return queryset.filter(Q(_path__isnull=True) | Q(_path__is_active=False))
|
||||
|
||||
|
||||
class ConsolePortFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet, PathEndpointFilterSet):
|
||||
class ConsolePortFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet, PathEndpointFilterSet):
|
||||
type = django_filters.MultipleChoiceFilter(
|
||||
choices=ConsolePortTypeChoices,
|
||||
null_value=None
|
||||
@@ -887,12 +884,7 @@ class ConsolePortFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTermina
|
||||
fields = ['id', 'name', 'label', 'description']
|
||||
|
||||
|
||||
class ConsoleServerPortFilterSet(
|
||||
BaseFilterSet,
|
||||
DeviceComponentFilterSet,
|
||||
CableTerminationFilterSet,
|
||||
PathEndpointFilterSet
|
||||
):
|
||||
class ConsoleServerPortFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet, PathEndpointFilterSet):
|
||||
type = django_filters.MultipleChoiceFilter(
|
||||
choices=ConsolePortTypeChoices,
|
||||
null_value=None
|
||||
@@ -903,7 +895,7 @@ class ConsoleServerPortFilterSet(
|
||||
fields = ['id', 'name', 'label', 'description']
|
||||
|
||||
|
||||
class PowerPortFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet, PathEndpointFilterSet):
|
||||
class PowerPortFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet, PathEndpointFilterSet):
|
||||
type = django_filters.MultipleChoiceFilter(
|
||||
choices=PowerPortTypeChoices,
|
||||
null_value=None
|
||||
@@ -914,7 +906,7 @@ class PowerPortFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminati
|
||||
fields = ['id', 'name', 'label', 'maximum_draw', 'allocated_draw', 'description']
|
||||
|
||||
|
||||
class PowerOutletFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet, PathEndpointFilterSet):
|
||||
class PowerOutletFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet, PathEndpointFilterSet):
|
||||
type = django_filters.MultipleChoiceFilter(
|
||||
choices=PowerOutletTypeChoices,
|
||||
null_value=None
|
||||
@@ -929,7 +921,7 @@ class PowerOutletFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTermina
|
||||
fields = ['id', 'name', 'label', 'feed_leg', 'description']
|
||||
|
||||
|
||||
class InterfaceFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet, PathEndpointFilterSet):
|
||||
class InterfaceFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet, PathEndpointFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
@@ -984,7 +976,7 @@ class InterfaceFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminati
|
||||
devices = Device.objects.filter(**{'{}__in'.format(name): value})
|
||||
vc_interface_ids = []
|
||||
for device in devices:
|
||||
vc_interface_ids.extend(device.vc_interfaces.values_list('id', flat=True))
|
||||
vc_interface_ids.extend(device.vc_interfaces().values_list('id', flat=True))
|
||||
return queryset.filter(pk__in=vc_interface_ids)
|
||||
except Device.DoesNotExist:
|
||||
return queryset.none()
|
||||
@@ -995,7 +987,7 @@ class InterfaceFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminati
|
||||
try:
|
||||
devices = Device.objects.filter(pk__in=id_list)
|
||||
for device in devices:
|
||||
vc_interface_ids += device.vc_interfaces.values_list('id', flat=True)
|
||||
vc_interface_ids += device.vc_interfaces().values_list('id', flat=True)
|
||||
return queryset.filter(pk__in=vc_interface_ids)
|
||||
except Device.DoesNotExist:
|
||||
return queryset.none()
|
||||
@@ -1027,7 +1019,7 @@ class InterfaceFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminati
|
||||
}.get(value, queryset.none())
|
||||
|
||||
|
||||
class FrontPortFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet):
|
||||
class FrontPortFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet):
|
||||
type = django_filters.MultipleChoiceFilter(
|
||||
choices=PortTypeChoices,
|
||||
null_value=None
|
||||
@@ -1038,7 +1030,7 @@ class FrontPortFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminati
|
||||
fields = ['id', 'name', 'label', 'type', 'description']
|
||||
|
||||
|
||||
class RearPortFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet):
|
||||
class RearPortFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet):
|
||||
type = django_filters.MultipleChoiceFilter(
|
||||
choices=PortTypeChoices,
|
||||
null_value=None
|
||||
@@ -1049,14 +1041,14 @@ class RearPortFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminatio
|
||||
fields = ['id', 'name', 'label', 'type', 'positions', 'description']
|
||||
|
||||
|
||||
class DeviceBayFilterSet(BaseFilterSet, DeviceComponentFilterSet):
|
||||
class DeviceBayFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = DeviceBay
|
||||
fields = ['id', 'name', 'label', 'description']
|
||||
|
||||
|
||||
class InventoryItemFilterSet(BaseFilterSet, DeviceComponentFilterSet):
|
||||
class InventoryItemFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
@@ -1129,7 +1121,7 @@ class InventoryItemFilterSet(BaseFilterSet, DeviceComponentFilterSet):
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
|
||||
class VirtualChassisFilterSet(BaseFilterSet, CustomFieldModelFilterSet):
|
||||
class VirtualChassisFilterSet(PrimaryModelFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
@@ -1209,7 +1201,7 @@ class VirtualChassisFilterSet(BaseFilterSet, CustomFieldModelFilterSet):
|
||||
return queryset.filter(qs_filter).distinct()
|
||||
|
||||
|
||||
class CableFilterSet(BaseFilterSet, CustomFieldModelFilterSet):
|
||||
class CableFilterSet(PrimaryModelFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
@@ -1273,7 +1265,7 @@ class CableFilterSet(BaseFilterSet, CustomFieldModelFilterSet):
|
||||
return queryset
|
||||
|
||||
|
||||
class ConnectionFilterSet:
|
||||
class ConnectionFilterSet(BaseFilterSet):
|
||||
|
||||
def filter_site(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
@@ -1286,7 +1278,7 @@ class ConnectionFilterSet:
|
||||
return queryset.filter(**{f'{name}__in': value})
|
||||
|
||||
|
||||
class ConsoleConnectionFilterSet(ConnectionFilterSet, BaseFilterSet):
|
||||
class ConsoleConnectionFilterSet(ConnectionFilterSet):
|
||||
site = django_filters.CharFilter(
|
||||
method='filter_site',
|
||||
label='Site (slug)',
|
||||
@@ -1304,7 +1296,7 @@ class ConsoleConnectionFilterSet(ConnectionFilterSet, BaseFilterSet):
|
||||
fields = ['name']
|
||||
|
||||
|
||||
class PowerConnectionFilterSet(ConnectionFilterSet, BaseFilterSet):
|
||||
class PowerConnectionFilterSet(ConnectionFilterSet):
|
||||
site = django_filters.CharFilter(
|
||||
method='filter_site',
|
||||
label='Site (slug)',
|
||||
@@ -1322,7 +1314,7 @@ class PowerConnectionFilterSet(ConnectionFilterSet, BaseFilterSet):
|
||||
fields = ['name']
|
||||
|
||||
|
||||
class InterfaceConnectionFilterSet(ConnectionFilterSet, BaseFilterSet):
|
||||
class InterfaceConnectionFilterSet(ConnectionFilterSet):
|
||||
site = django_filters.CharFilter(
|
||||
method='filter_site',
|
||||
label='Site (slug)',
|
||||
@@ -1340,7 +1332,7 @@ class InterfaceConnectionFilterSet(ConnectionFilterSet, BaseFilterSet):
|
||||
fields = []
|
||||
|
||||
|
||||
class PowerPanelFilterSet(BaseFilterSet):
|
||||
class PowerPanelFilterSet(PrimaryModelFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
@@ -1402,13 +1394,7 @@ class PowerPanelFilterSet(BaseFilterSet):
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
|
||||
class PowerFeedFilterSet(
|
||||
BaseFilterSet,
|
||||
CableTerminationFilterSet,
|
||||
PathEndpointFilterSet,
|
||||
CustomFieldModelFilterSet,
|
||||
CreatedUpdatedFilterSet
|
||||
):
|
||||
class PowerFeedFilterSet(PrimaryModelFilterSet, CableTerminationFilterSet, PathEndpointFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
@@ -1825,7 +1825,7 @@ class ConsolePortTemplateImportForm(ComponentTemplateImportForm):
|
||||
class Meta:
|
||||
model = ConsolePortTemplate
|
||||
fields = [
|
||||
'device_type', 'name', 'label', 'type',
|
||||
'device_type', 'name', 'label', 'type', 'description',
|
||||
]
|
||||
|
||||
|
||||
@@ -1834,7 +1834,7 @@ class ConsoleServerPortTemplateImportForm(ComponentTemplateImportForm):
|
||||
class Meta:
|
||||
model = ConsoleServerPortTemplate
|
||||
fields = [
|
||||
'device_type', 'name', 'label', 'type',
|
||||
'device_type', 'name', 'label', 'type', 'description',
|
||||
]
|
||||
|
||||
|
||||
@@ -1843,7 +1843,7 @@ class PowerPortTemplateImportForm(ComponentTemplateImportForm):
|
||||
class Meta:
|
||||
model = PowerPortTemplate
|
||||
fields = [
|
||||
'device_type', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw',
|
||||
'device_type', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description',
|
||||
]
|
||||
|
||||
|
||||
@@ -1857,7 +1857,7 @@ class PowerOutletTemplateImportForm(ComponentTemplateImportForm):
|
||||
class Meta:
|
||||
model = PowerOutletTemplate
|
||||
fields = [
|
||||
'device_type', 'name', 'label', 'type', 'power_port', 'feed_leg',
|
||||
'device_type', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description',
|
||||
]
|
||||
|
||||
|
||||
@@ -1869,7 +1869,7 @@ class InterfaceTemplateImportForm(ComponentTemplateImportForm):
|
||||
class Meta:
|
||||
model = InterfaceTemplate
|
||||
fields = [
|
||||
'device_type', 'name', 'label', 'type', 'mgmt_only',
|
||||
'device_type', 'name', 'label', 'type', 'mgmt_only', 'description',
|
||||
]
|
||||
|
||||
|
||||
@@ -1886,7 +1886,7 @@ class FrontPortTemplateImportForm(ComponentTemplateImportForm):
|
||||
class Meta:
|
||||
model = FrontPortTemplate
|
||||
fields = [
|
||||
'device_type', 'name', 'type', 'rear_port', 'rear_port_position',
|
||||
'device_type', 'name', 'type', 'rear_port', 'rear_port_position', 'label', 'description',
|
||||
]
|
||||
|
||||
|
||||
@@ -1898,7 +1898,7 @@ class RearPortTemplateImportForm(ComponentTemplateImportForm):
|
||||
class Meta:
|
||||
model = RearPortTemplate
|
||||
fields = [
|
||||
'device_type', 'name', 'type', 'positions',
|
||||
'device_type', 'name', 'type', 'positions', 'label', 'description',
|
||||
]
|
||||
|
||||
|
||||
@@ -1907,7 +1907,7 @@ class DeviceBayTemplateImportForm(ComponentTemplateImportForm):
|
||||
class Meta:
|
||||
model = DeviceBayTemplate
|
||||
fields = [
|
||||
'device_type', 'name',
|
||||
'device_type', 'name', 'label', 'description',
|
||||
]
|
||||
|
||||
|
||||
@@ -2153,7 +2153,7 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||
ip_choices = [(None, '---------')]
|
||||
|
||||
# Gather PKs of all interfaces belonging to this Device or a peer VirtualChassis member
|
||||
interface_ids = self.instance.vc_interfaces.values_list('pk', flat=True)
|
||||
interface_ids = self.instance.vc_interfaces(if_master=False).values_list('pk', flat=True)
|
||||
|
||||
# Collect interface IPs
|
||||
interface_ips = IPAddress.objects.filter(
|
||||
@@ -2552,7 +2552,7 @@ class ComponentCreateForm(BootstrapMixin, CustomFieldForm, ComponentForm):
|
||||
queryset=Device.objects.all()
|
||||
)
|
||||
description = forms.CharField(
|
||||
max_length=100,
|
||||
max_length=200,
|
||||
required=False
|
||||
)
|
||||
tags = DynamicModelMultipleChoiceField(
|
||||
@@ -3126,9 +3126,13 @@ class InterfaceForm(BootstrapMixin, InterfaceCommonForm, CustomFieldModelForm):
|
||||
|
||||
device = Device.objects.get(pk=self.data['device']) if self.is_bound else self.instance.device
|
||||
|
||||
# Restrict parent/LAG interface assignment by device
|
||||
# Restrict parent/LAG interface assignment by device/VC
|
||||
self.fields['parent'].widget.add_query_param('device_id', device.pk)
|
||||
self.fields['lag'].widget.add_query_param('device_id', device.pk)
|
||||
if device.virtual_chassis and device.virtual_chassis.master:
|
||||
# Get available LAG interfaces by VirtualChassis master
|
||||
self.fields['lag'].widget.add_query_param('device_id', device.virtual_chassis.master.pk)
|
||||
else:
|
||||
self.fields['lag'].widget.add_query_param('device_id', device.pk)
|
||||
|
||||
# Limit VLAN choices by device
|
||||
self.fields['untagged_vlan'].widget.add_query_param('available_on_device', device.pk)
|
||||
|
||||
21
netbox/dcim/migrations/0131_consoleport_speed.py
Normal file
21
netbox/dcim/migrations/0131_consoleport_speed.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0130_sitegroup'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='consoleport',
|
||||
name='speed',
|
||||
field=models.PositiveIntegerField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='consoleserverport',
|
||||
name='speed',
|
||||
field=models.PositiveIntegerField(blank=True, null=True),
|
||||
),
|
||||
]
|
||||
@@ -30,7 +30,7 @@ __all__ = (
|
||||
# Cables
|
||||
#
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||
class Cable(PrimaryModel):
|
||||
"""
|
||||
A physical connection between two endpoints.
|
||||
|
||||
@@ -211,7 +211,7 @@ class PathEndpoint(models.Model):
|
||||
# Console ports
|
||||
#
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||
class ConsolePort(ComponentModel, CableTermination, PathEndpoint):
|
||||
"""
|
||||
A physical console port within a Device. ConsolePorts connect to ConsoleServerPorts.
|
||||
@@ -222,7 +222,7 @@ class ConsolePort(ComponentModel, CableTermination, PathEndpoint):
|
||||
blank=True,
|
||||
help_text='Physical port type'
|
||||
)
|
||||
speed = models.PositiveSmallIntegerField(
|
||||
speed = models.PositiveIntegerField(
|
||||
choices=ConsolePortSpeedChoices,
|
||||
blank=True,
|
||||
null=True,
|
||||
@@ -254,7 +254,7 @@ class ConsolePort(ComponentModel, CableTermination, PathEndpoint):
|
||||
# Console server ports
|
||||
#
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||
class ConsoleServerPort(ComponentModel, CableTermination, PathEndpoint):
|
||||
"""
|
||||
A physical port within a Device (typically a designated console server) which provides access to ConsolePorts.
|
||||
@@ -265,7 +265,7 @@ class ConsoleServerPort(ComponentModel, CableTermination, PathEndpoint):
|
||||
blank=True,
|
||||
help_text='Physical port type'
|
||||
)
|
||||
speed = models.PositiveSmallIntegerField(
|
||||
speed = models.PositiveIntegerField(
|
||||
choices=ConsolePortSpeedChoices,
|
||||
blank=True,
|
||||
null=True,
|
||||
@@ -297,7 +297,7 @@ class ConsoleServerPort(ComponentModel, CableTermination, PathEndpoint):
|
||||
# Power ports
|
||||
#
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||
class PowerPort(ComponentModel, CableTermination, PathEndpoint):
|
||||
"""
|
||||
A physical power supply (intake) port within a Device. PowerPorts connect to PowerOutlets.
|
||||
@@ -408,7 +408,7 @@ class PowerPort(ComponentModel, CableTermination, PathEndpoint):
|
||||
# Power outlets
|
||||
#
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||
class PowerOutlet(ComponentModel, CableTermination, PathEndpoint):
|
||||
"""
|
||||
A physical power outlet (output) within a Device which provides power to a PowerPort.
|
||||
@@ -512,7 +512,7 @@ class BaseInterface(models.Model):
|
||||
return self.ip_addresses.count()
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||
class Interface(ComponentModel, BaseInterface, CableTermination, PathEndpoint):
|
||||
"""
|
||||
A network interface within a Device. A physical Interface can connect to exactly one other Interface.
|
||||
@@ -683,7 +683,7 @@ class Interface(ComponentModel, BaseInterface, CableTermination, PathEndpoint):
|
||||
# Pass-through ports
|
||||
#
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||
class FrontPort(ComponentModel, CableTermination):
|
||||
"""
|
||||
A pass-through port on the front of a Device.
|
||||
@@ -748,7 +748,7 @@ class FrontPort(ComponentModel, CableTermination):
|
||||
})
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||
class RearPort(ComponentModel, CableTermination):
|
||||
"""
|
||||
A pass-through port on the rear of a Device.
|
||||
@@ -801,7 +801,7 @@ class RearPort(ComponentModel, CableTermination):
|
||||
# Device bays
|
||||
#
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||
class DeviceBay(ComponentModel):
|
||||
"""
|
||||
An empty space within a Device which can house a child device
|
||||
@@ -860,7 +860,7 @@ class DeviceBay(ComponentModel):
|
||||
# Inventory items
|
||||
#
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||
class InventoryItem(MPTTModel, ComponentModel):
|
||||
"""
|
||||
An InventoryItem represents a serialized piece of hardware within a Device, such as a line card or power supply.
|
||||
|
||||
@@ -36,7 +36,7 @@ __all__ = (
|
||||
# Device Types
|
||||
#
|
||||
|
||||
@extras_features('custom_fields', 'export_templates', 'webhooks')
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
class Manufacturer(OrganizationalModel):
|
||||
"""
|
||||
A Manufacturer represents a company which produces hardware devices; for example, Juniper or Dell.
|
||||
@@ -75,7 +75,7 @@ class Manufacturer(OrganizationalModel):
|
||||
)
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||
class DeviceType(PrimaryModel):
|
||||
"""
|
||||
A DeviceType represents a particular make (Manufacturer) and model of device. It specifies rack height and depth, as
|
||||
@@ -183,6 +183,8 @@ class DeviceType(PrimaryModel):
|
||||
{
|
||||
'name': c.name,
|
||||
'type': c.type,
|
||||
'label': c.label,
|
||||
'description': c.description,
|
||||
}
|
||||
for c in self.consoleporttemplates.all()
|
||||
]
|
||||
@@ -191,6 +193,8 @@ class DeviceType(PrimaryModel):
|
||||
{
|
||||
'name': c.name,
|
||||
'type': c.type,
|
||||
'label': c.label,
|
||||
'description': c.description,
|
||||
}
|
||||
for c in self.consoleserverporttemplates.all()
|
||||
]
|
||||
@@ -201,6 +205,8 @@ class DeviceType(PrimaryModel):
|
||||
'type': c.type,
|
||||
'maximum_draw': c.maximum_draw,
|
||||
'allocated_draw': c.allocated_draw,
|
||||
'label': c.label,
|
||||
'description': c.description,
|
||||
}
|
||||
for c in self.powerporttemplates.all()
|
||||
]
|
||||
@@ -211,6 +217,8 @@ class DeviceType(PrimaryModel):
|
||||
'type': c.type,
|
||||
'power_port': c.power_port.name if c.power_port else None,
|
||||
'feed_leg': c.feed_leg,
|
||||
'label': c.label,
|
||||
'description': c.description,
|
||||
}
|
||||
for c in self.poweroutlettemplates.all()
|
||||
]
|
||||
@@ -220,6 +228,8 @@ class DeviceType(PrimaryModel):
|
||||
'name': c.name,
|
||||
'type': c.type,
|
||||
'mgmt_only': c.mgmt_only,
|
||||
'label': c.label,
|
||||
'description': c.description,
|
||||
}
|
||||
for c in self.interfacetemplates.all()
|
||||
]
|
||||
@@ -230,6 +240,8 @@ class DeviceType(PrimaryModel):
|
||||
'type': c.type,
|
||||
'rear_port': c.rear_port.name,
|
||||
'rear_port_position': c.rear_port_position,
|
||||
'label': c.label,
|
||||
'description': c.description,
|
||||
}
|
||||
for c in self.frontporttemplates.all()
|
||||
]
|
||||
@@ -239,6 +251,8 @@ class DeviceType(PrimaryModel):
|
||||
'name': c.name,
|
||||
'type': c.type,
|
||||
'positions': c.positions,
|
||||
'label': c.label,
|
||||
'description': c.description,
|
||||
}
|
||||
for c in self.rearporttemplates.all()
|
||||
]
|
||||
@@ -246,6 +260,8 @@ class DeviceType(PrimaryModel):
|
||||
data['device-bays'] = [
|
||||
{
|
||||
'name': c.name,
|
||||
'label': c.label,
|
||||
'description': c.description,
|
||||
}
|
||||
for c in self.devicebaytemplates.all()
|
||||
]
|
||||
@@ -337,7 +353,7 @@ class DeviceType(PrimaryModel):
|
||||
# Devices
|
||||
#
|
||||
|
||||
@extras_features('custom_fields', 'export_templates', 'webhooks')
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
class DeviceRole(OrganizationalModel):
|
||||
"""
|
||||
Devices are organized by functional role; for example, "Core Switch" or "File Server". Each DeviceRole is assigned a
|
||||
@@ -388,7 +404,7 @@ class DeviceRole(OrganizationalModel):
|
||||
)
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'export_templates', 'webhooks')
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
class Platform(OrganizationalModel):
|
||||
"""
|
||||
Platform refers to the software or firmware running on a Device. For example, "Cisco IOS-XR" or "Juniper Junos".
|
||||
@@ -452,7 +468,7 @@ class Platform(OrganizationalModel):
|
||||
)
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||
class Device(PrimaryModel, ConfigContextModel):
|
||||
"""
|
||||
A Device represents a piece of physical hardware mounted within a Rack. Each Device is assigned a DeviceType,
|
||||
@@ -716,7 +732,7 @@ class Device(PrimaryModel, ConfigContextModel):
|
||||
pass
|
||||
|
||||
# Validate primary IP addresses
|
||||
vc_interfaces = self.vc_interfaces.all()
|
||||
vc_interfaces = self.vc_interfaces(if_master=False)
|
||||
if self.primary_ip4:
|
||||
if self.primary_ip4.family != 4:
|
||||
raise ValidationError({
|
||||
@@ -854,20 +870,25 @@ class Device(PrimaryModel, ConfigContextModel):
|
||||
else:
|
||||
return None
|
||||
|
||||
@property
|
||||
def interfaces_count(self):
|
||||
return self.vc_interfaces().count()
|
||||
|
||||
def get_vc_master(self):
|
||||
"""
|
||||
If this Device is a VirtualChassis member, return the VC master. Otherwise, return None.
|
||||
"""
|
||||
return self.virtual_chassis.master if self.virtual_chassis else None
|
||||
|
||||
@property
|
||||
def vc_interfaces(self):
|
||||
def vc_interfaces(self, if_master=True):
|
||||
"""
|
||||
Return a QuerySet matching all Interfaces assigned to this Device or, if this Device is a VC master, to another
|
||||
Device belonging to the same VirtualChassis.
|
||||
|
||||
:param if_master: If True, return VC member interfaces only if this Device is the VC master.
|
||||
"""
|
||||
filter = Q(device=self)
|
||||
if self.virtual_chassis and self.virtual_chassis.master == self:
|
||||
if self.virtual_chassis and (self.virtual_chassis.master == self or not if_master):
|
||||
filter |= Q(device__virtual_chassis=self.virtual_chassis, mgmt_only=False)
|
||||
return Interface.objects.filter(filter)
|
||||
|
||||
@@ -901,7 +922,7 @@ class Device(PrimaryModel, ConfigContextModel):
|
||||
# Virtual chassis
|
||||
#
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||
class VirtualChassis(PrimaryModel):
|
||||
"""
|
||||
A collection of Devices which operate with a shared control plane (e.g. a switch stack).
|
||||
|
||||
@@ -21,7 +21,7 @@ __all__ = (
|
||||
# Power
|
||||
#
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||
class PowerPanel(PrimaryModel):
|
||||
"""
|
||||
A distribution point for electrical power; e.g. a data center RPP.
|
||||
@@ -71,7 +71,7 @@ class PowerPanel(PrimaryModel):
|
||||
)
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||
class PowerFeed(PrimaryModel, PathEndpoint, CableTermination):
|
||||
"""
|
||||
An electrical circuit delivered from a PowerPanel.
|
||||
|
||||
@@ -35,7 +35,7 @@ __all__ = (
|
||||
# Racks
|
||||
#
|
||||
|
||||
@extras_features('custom_fields', 'export_templates', 'webhooks')
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
class RackRole(OrganizationalModel):
|
||||
"""
|
||||
Racks can be organized by functional role, similar to Devices.
|
||||
@@ -78,7 +78,7 @@ class RackRole(OrganizationalModel):
|
||||
)
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||
class Rack(PrimaryModel):
|
||||
"""
|
||||
Devices are housed within Racks. Each rack has a defined height measured in rack units, and a front and rear face.
|
||||
@@ -467,7 +467,7 @@ class Rack(PrimaryModel):
|
||||
return int(allocated_draw_total / available_power_total * 100)
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||
class RackReservation(PrimaryModel):
|
||||
"""
|
||||
One or more reserved units within a Rack.
|
||||
|
||||
@@ -26,7 +26,7 @@ __all__ = (
|
||||
# Regions
|
||||
#
|
||||
|
||||
@extras_features('custom_fields', 'export_templates', 'webhooks')
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
class Region(NestedGroupModel):
|
||||
"""
|
||||
A region represents a geographic collection of sites. For example, you might create regions representing countries,
|
||||
@@ -78,7 +78,7 @@ class Region(NestedGroupModel):
|
||||
# Site groups
|
||||
#
|
||||
|
||||
@extras_features('custom_fields', 'export_templates', 'webhooks')
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
class SiteGroup(NestedGroupModel):
|
||||
"""
|
||||
A site group is an arbitrary grouping of sites. For example, you might have corporate sites and customer sites; and
|
||||
@@ -130,7 +130,7 @@ class SiteGroup(NestedGroupModel):
|
||||
# Sites
|
||||
#
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||
class Site(PrimaryModel):
|
||||
"""
|
||||
A Site represents a geographic location within a network; typically a building or campus. The optional facility
|
||||
@@ -285,7 +285,7 @@ class Site(PrimaryModel):
|
||||
# Locations
|
||||
#
|
||||
|
||||
@extras_features('custom_fields', 'export_templates', 'webhooks')
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
class Location(NestedGroupModel):
|
||||
"""
|
||||
A Location represents a subgroup of Racks and/or Devices within a Site. A Location may represent a building within a
|
||||
|
||||
@@ -31,9 +31,10 @@ def rebuild_paths(obj):
|
||||
|
||||
with transaction.atomic():
|
||||
for cp in cable_paths:
|
||||
invalidate_obj(cp.origin)
|
||||
cp.delete()
|
||||
create_cablepath(cp.origin)
|
||||
if cp.origin:
|
||||
invalidate_obj(cp.origin)
|
||||
create_cablepath(cp.origin)
|
||||
|
||||
|
||||
#
|
||||
|
||||
@@ -520,6 +520,7 @@ class DeviceInterfaceTable(InterfaceTable):
|
||||
'description', 'mark_connected', 'cable', 'cable_color', 'cable_peer', 'connection', 'tags', 'ip_addresses',
|
||||
'untagged_vlan', 'tagged_vlans', 'actions',
|
||||
)
|
||||
order_by = ('name',)
|
||||
default_columns = (
|
||||
'pk', 'name', 'label', 'enabled', 'type', 'parent', 'lag', 'mtu', 'mode', 'description', 'ip_addresses',
|
||||
'cable', 'connection', 'actions',
|
||||
|
||||
@@ -73,6 +73,7 @@ class RackDetailTable(RackTable):
|
||||
verbose_name='Devices'
|
||||
)
|
||||
get_utilization = UtilizationColumn(
|
||||
orderable=False,
|
||||
verbose_name='Space'
|
||||
)
|
||||
get_power_utilization = UtilizationColumn(
|
||||
|
||||
@@ -102,10 +102,14 @@ class LocationTable(BaseTable):
|
||||
site = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
rack_count = tables.Column(
|
||||
rack_count = LinkedCountColumn(
|
||||
viewname='dcim:rack_list',
|
||||
url_params={'location_id': 'pk'},
|
||||
verbose_name='Racks'
|
||||
)
|
||||
device_count = tables.Column(
|
||||
device_count = LinkedCountColumn(
|
||||
viewname='dcim:device_list',
|
||||
url_params={'location_id': 'pk'},
|
||||
verbose_name='Devices'
|
||||
)
|
||||
actions = ButtonsColumn(
|
||||
|
||||
@@ -2,14 +2,15 @@ from django.contrib.auth.models import User
|
||||
from django.test import TestCase
|
||||
|
||||
from dcim.choices import *
|
||||
from dcim.filters import *
|
||||
from dcim.filtersets import *
|
||||
from dcim.models import *
|
||||
from ipam.models import IPAddress
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from utilities.testing import ChangeLoggedFilterSetTests
|
||||
from virtualization.models import Cluster, ClusterType
|
||||
|
||||
|
||||
class RegionTestCase(TestCase):
|
||||
class RegionTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = Region.objects.all()
|
||||
filterset = RegionFilterSet
|
||||
|
||||
@@ -35,10 +36,6 @@ class RegionTestCase(TestCase):
|
||||
for region in child_regions:
|
||||
region.save()
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Region 1', 'Region 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -59,7 +56,7 @@ class RegionTestCase(TestCase):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
|
||||
class SiteGroupTestCase(TestCase):
|
||||
class SiteGroupTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = SiteGroup.objects.all()
|
||||
filterset = SiteGroupFilterSet
|
||||
|
||||
@@ -85,10 +82,6 @@ class SiteGroupTestCase(TestCase):
|
||||
for sitegroup in child_sitegroups:
|
||||
sitegroup.save()
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Site Group 1', 'Site Group 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -109,7 +102,7 @@ class SiteGroupTestCase(TestCase):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
|
||||
class SiteTestCase(TestCase):
|
||||
class SiteTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = Site.objects.all()
|
||||
filterset = SiteFilterSet
|
||||
|
||||
@@ -154,10 +147,6 @@ class SiteTestCase(TestCase):
|
||||
)
|
||||
Site.objects.bulk_create(sites)
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Site 1', 'Site 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -227,7 +216,7 @@ class SiteTestCase(TestCase):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
class LocationTestCase(TestCase):
|
||||
class LocationTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = Location.objects.all()
|
||||
filterset = LocationFilterSet
|
||||
|
||||
@@ -273,10 +262,6 @@ class LocationTestCase(TestCase):
|
||||
for location in locations:
|
||||
location.save()
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Location 1', 'Location 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -318,7 +303,7 @@ class LocationTestCase(TestCase):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
class RackRoleTestCase(TestCase):
|
||||
class RackRoleTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = RackRole.objects.all()
|
||||
filterset = RackRoleFilterSet
|
||||
|
||||
@@ -332,10 +317,6 @@ class RackRoleTestCase(TestCase):
|
||||
)
|
||||
RackRole.objects.bulk_create(rack_roles)
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Rack Role 1', 'Rack Role 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -349,7 +330,7 @@ class RackRoleTestCase(TestCase):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
class RackTestCase(TestCase):
|
||||
class RackTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = Rack.objects.all()
|
||||
filterset = RackFilterSet
|
||||
|
||||
@@ -416,10 +397,6 @@ class RackTestCase(TestCase):
|
||||
)
|
||||
Rack.objects.bulk_create(racks)
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Rack 1', 'Rack 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -523,7 +500,7 @@ class RackTestCase(TestCase):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
class RackReservationTestCase(TestCase):
|
||||
class RackReservationTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = RackReservation.objects.all()
|
||||
filterset = RackReservationFilterSet
|
||||
|
||||
@@ -581,10 +558,6 @@ class RackReservationTestCase(TestCase):
|
||||
)
|
||||
RackReservation.objects.bulk_create(reservations)
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_site(self):
|
||||
sites = Site.objects.all()[:2]
|
||||
params = {'site_id': [sites[0].pk, sites[1].pk]}
|
||||
@@ -621,7 +594,7 @@ class RackReservationTestCase(TestCase):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
class ManufacturerTestCase(TestCase):
|
||||
class ManufacturerTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = Manufacturer.objects.all()
|
||||
filterset = ManufacturerFilterSet
|
||||
|
||||
@@ -635,10 +608,6 @@ class ManufacturerTestCase(TestCase):
|
||||
)
|
||||
Manufacturer.objects.bulk_create(manufacturers)
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Manufacturer 1', 'Manufacturer 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -652,7 +621,7 @@ class ManufacturerTestCase(TestCase):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
class DeviceTypeTestCase(TestCase):
|
||||
class DeviceTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = DeviceType.objects.all()
|
||||
filterset = DeviceTypeFilterSet
|
||||
|
||||
@@ -708,10 +677,6 @@ class DeviceTypeTestCase(TestCase):
|
||||
DeviceBayTemplate(device_type=device_types[1], name='Device Bay 2'),
|
||||
))
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_model(self):
|
||||
params = {'model': ['Model 1', 'Model 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -788,7 +753,7 @@ class DeviceTypeTestCase(TestCase):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
|
||||
class ConsolePortTemplateTestCase(TestCase):
|
||||
class ConsolePortTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = ConsolePortTemplate.objects.all()
|
||||
filterset = ConsolePortTemplateFilterSet
|
||||
|
||||
@@ -810,10 +775,6 @@ class ConsolePortTemplateTestCase(TestCase):
|
||||
ConsolePortTemplate(device_type=device_types[2], name='Console Port 3'),
|
||||
))
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Console Port 1', 'Console Port 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -824,7 +785,7 @@ class ConsolePortTemplateTestCase(TestCase):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
class ConsoleServerPortTemplateTestCase(TestCase):
|
||||
class ConsoleServerPortTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = ConsoleServerPortTemplate.objects.all()
|
||||
filterset = ConsoleServerPortTemplateFilterSet
|
||||
|
||||
@@ -846,10 +807,6 @@ class ConsoleServerPortTemplateTestCase(TestCase):
|
||||
ConsoleServerPortTemplate(device_type=device_types[2], name='Console Server Port 3'),
|
||||
))
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Console Server Port 1', 'Console Server Port 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -860,7 +817,7 @@ class ConsoleServerPortTemplateTestCase(TestCase):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
class PowerPortTemplateTestCase(TestCase):
|
||||
class PowerPortTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = PowerPortTemplate.objects.all()
|
||||
filterset = PowerPortTemplateFilterSet
|
||||
|
||||
@@ -882,10 +839,6 @@ class PowerPortTemplateTestCase(TestCase):
|
||||
PowerPortTemplate(device_type=device_types[2], name='Power Port 3', maximum_draw=300, allocated_draw=150),
|
||||
))
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Power Port 1', 'Power Port 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -904,7 +857,7 @@ class PowerPortTemplateTestCase(TestCase):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
class PowerOutletTemplateTestCase(TestCase):
|
||||
class PowerOutletTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = PowerOutletTemplate.objects.all()
|
||||
filterset = PowerOutletTemplateFilterSet
|
||||
|
||||
@@ -926,10 +879,6 @@ class PowerOutletTemplateTestCase(TestCase):
|
||||
PowerOutletTemplate(device_type=device_types[2], name='Power Outlet 3', feed_leg=PowerOutletFeedLegChoices.FEED_LEG_C),
|
||||
))
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Power Outlet 1', 'Power Outlet 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -944,7 +893,7 @@ class PowerOutletTemplateTestCase(TestCase):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
class InterfaceTemplateTestCase(TestCase):
|
||||
class InterfaceTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = InterfaceTemplate.objects.all()
|
||||
filterset = InterfaceTemplateFilterSet
|
||||
|
||||
@@ -966,10 +915,6 @@ class InterfaceTemplateTestCase(TestCase):
|
||||
InterfaceTemplate(device_type=device_types[2], name='Interface 3', type=InterfaceTypeChoices.TYPE_1GE_SFP, mgmt_only=False),
|
||||
))
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Interface 1', 'Interface 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -990,7 +935,7 @@ class InterfaceTemplateTestCase(TestCase):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
class FrontPortTemplateTestCase(TestCase):
|
||||
class FrontPortTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = FrontPortTemplate.objects.all()
|
||||
filterset = FrontPortTemplateFilterSet
|
||||
|
||||
@@ -1019,10 +964,6 @@ class FrontPortTemplateTestCase(TestCase):
|
||||
FrontPortTemplate(device_type=device_types[2], name='Front Port 3', rear_port=rear_ports[2], type=PortTypeChoices.TYPE_BNC),
|
||||
))
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Front Port 1', 'Front Port 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -1037,7 +978,7 @@ class FrontPortTemplateTestCase(TestCase):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
class RearPortTemplateTestCase(TestCase):
|
||||
class RearPortTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = RearPortTemplate.objects.all()
|
||||
filterset = RearPortTemplateFilterSet
|
||||
|
||||
@@ -1059,10 +1000,6 @@ class RearPortTemplateTestCase(TestCase):
|
||||
RearPortTemplate(device_type=device_types[2], name='Rear Port 3', type=PortTypeChoices.TYPE_BNC, positions=3),
|
||||
))
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Rear Port 1', 'Rear Port 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -1081,7 +1018,7 @@ class RearPortTemplateTestCase(TestCase):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
class DeviceBayTemplateTestCase(TestCase):
|
||||
class DeviceBayTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = DeviceBayTemplate.objects.all()
|
||||
filterset = DeviceBayTemplateFilterSet
|
||||
|
||||
@@ -1103,10 +1040,6 @@ class DeviceBayTemplateTestCase(TestCase):
|
||||
DeviceBayTemplate(device_type=device_types[2], name='Device Bay 3'),
|
||||
))
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Device Bay 1', 'Device Bay 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -1117,7 +1050,7 @@ class DeviceBayTemplateTestCase(TestCase):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
class DeviceRoleTestCase(TestCase):
|
||||
class DeviceRoleTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = DeviceRole.objects.all()
|
||||
filterset = DeviceRoleFilterSet
|
||||
|
||||
@@ -1131,10 +1064,6 @@ class DeviceRoleTestCase(TestCase):
|
||||
)
|
||||
DeviceRole.objects.bulk_create(device_roles)
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Device Role 1', 'Device Role 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -1154,7 +1083,7 @@ class DeviceRoleTestCase(TestCase):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
|
||||
class PlatformTestCase(TestCase):
|
||||
class PlatformTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = Platform.objects.all()
|
||||
filterset = PlatformFilterSet
|
||||
|
||||
@@ -1175,10 +1104,6 @@ class PlatformTestCase(TestCase):
|
||||
)
|
||||
Platform.objects.bulk_create(platforms)
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Platform 1', 'Platform 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -1203,7 +1128,7 @@ class PlatformTestCase(TestCase):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
class DeviceTestCase(TestCase):
|
||||
class DeviceTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = Device.objects.all()
|
||||
filterset = DeviceFilterSet
|
||||
|
||||
@@ -1356,10 +1281,6 @@ class DeviceTestCase(TestCase):
|
||||
Device.objects.filter(pk=devices[0].pk).update(virtual_chassis=virtual_chassis, vc_position=1, vc_priority=1)
|
||||
Device.objects.filter(pk=devices[1].pk).update(virtual_chassis=virtual_chassis, vc_position=2, vc_priority=2)
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Device 1', 'Device 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -1549,7 +1470,7 @@ class DeviceTestCase(TestCase):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
class ConsolePortTestCase(TestCase):
|
||||
class ConsolePortTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = ConsolePort.objects.all()
|
||||
filterset = ConsolePortFilterSet
|
||||
|
||||
@@ -1608,10 +1529,6 @@ class ConsolePortTestCase(TestCase):
|
||||
Cable(termination_a=console_ports[1], termination_b=console_server_ports[1]).save()
|
||||
# Third port is not connected
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Console Port 1', 'Console Port 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -1665,7 +1582,7 @@ class ConsolePortTestCase(TestCase):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
|
||||
class ConsoleServerPortTestCase(TestCase):
|
||||
class ConsoleServerPortTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = ConsoleServerPort.objects.all()
|
||||
filterset = ConsoleServerPortFilterSet
|
||||
|
||||
@@ -1724,10 +1641,6 @@ class ConsoleServerPortTestCase(TestCase):
|
||||
Cable(termination_a=console_server_ports[1], termination_b=console_ports[1]).save()
|
||||
# Third port is not connected
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Console Server Port 1', 'Console Server Port 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -1781,7 +1694,7 @@ class ConsoleServerPortTestCase(TestCase):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
|
||||
class PowerPortTestCase(TestCase):
|
||||
class PowerPortTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = PowerPort.objects.all()
|
||||
filterset = PowerPortFilterSet
|
||||
|
||||
@@ -1840,10 +1753,6 @@ class PowerPortTestCase(TestCase):
|
||||
Cable(termination_a=power_ports[1], termination_b=power_outlets[1]).save()
|
||||
# Third port is not connected
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Power Port 1', 'Power Port 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -1905,7 +1814,7 @@ class PowerPortTestCase(TestCase):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
|
||||
class PowerOutletTestCase(TestCase):
|
||||
class PowerOutletTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = PowerOutlet.objects.all()
|
||||
filterset = PowerOutletFilterSet
|
||||
|
||||
@@ -1964,10 +1873,6 @@ class PowerOutletTestCase(TestCase):
|
||||
Cable(termination_a=power_outlets[1], termination_b=power_ports[1]).save()
|
||||
# Third port is not connected
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Power Outlet 1', 'Power Outlet 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -2025,7 +1930,7 @@ class PowerOutletTestCase(TestCase):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
|
||||
class InterfaceTestCase(TestCase):
|
||||
class InterfaceTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = Interface.objects.all()
|
||||
filterset = InterfaceFilterSet
|
||||
|
||||
@@ -2081,10 +1986,6 @@ class InterfaceTestCase(TestCase):
|
||||
Cable(termination_a=interfaces[1], termination_b=interfaces[4]).save()
|
||||
# Third pair is not connected
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Interface 1', 'Interface 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -2200,7 +2101,7 @@ class InterfaceTestCase(TestCase):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
class FrontPortTestCase(TestCase):
|
||||
class FrontPortTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = FrontPort.objects.all()
|
||||
filterset = FrontPortFilterSet
|
||||
|
||||
@@ -2266,10 +2167,6 @@ class FrontPortTestCase(TestCase):
|
||||
Cable(termination_a=front_ports[1], termination_b=front_ports[4]).save()
|
||||
# Third port is not connected
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Front Port 1', 'Front Port 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -2321,7 +2218,7 @@ class FrontPortTestCase(TestCase):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
class RearPortTestCase(TestCase):
|
||||
class RearPortTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = RearPort.objects.all()
|
||||
filterset = RearPortFilterSet
|
||||
|
||||
@@ -2377,10 +2274,6 @@ class RearPortTestCase(TestCase):
|
||||
Cable(termination_a=rear_ports[1], termination_b=rear_ports[4]).save()
|
||||
# Third port is not connected
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Rear Port 1', 'Rear Port 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -2436,7 +2329,7 @@ class RearPortTestCase(TestCase):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
class DeviceBayTestCase(TestCase):
|
||||
class DeviceBayTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = DeviceBay.objects.all()
|
||||
filterset = DeviceBayFilterSet
|
||||
|
||||
@@ -2483,10 +2376,6 @@ class DeviceBayTestCase(TestCase):
|
||||
)
|
||||
DeviceBay.objects.bulk_create(device_bays)
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Device Bay 1', 'Device Bay 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -2528,7 +2417,7 @@ class DeviceBayTestCase(TestCase):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
class InventoryItemTestCase(TestCase):
|
||||
class InventoryItemTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = InventoryItem.objects.all()
|
||||
filterset = InventoryItemFilterSet
|
||||
|
||||
@@ -2591,10 +2480,6 @@ class InventoryItemTestCase(TestCase):
|
||||
for i in child_inventory_items:
|
||||
i.save()
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Inventory Item 1', 'Inventory Item 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -2666,7 +2551,7 @@ class InventoryItemTestCase(TestCase):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
|
||||
class VirtualChassisTestCase(TestCase):
|
||||
class VirtualChassisTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = VirtualChassis.objects.all()
|
||||
filterset = VirtualChassisFilterSet
|
||||
|
||||
@@ -2721,10 +2606,6 @@ class VirtualChassisTestCase(TestCase):
|
||||
Device.objects.filter(pk=devices[3].pk).update(virtual_chassis=virtual_chassis[1])
|
||||
Device.objects.filter(pk=devices[5].pk).update(virtual_chassis=virtual_chassis[2])
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_domain(self):
|
||||
params = {'domain': ['Domain 1', 'Domain 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -2762,7 +2643,7 @@ class VirtualChassisTestCase(TestCase):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
class CableTestCase(TestCase):
|
||||
class CableTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = Cable.objects.all()
|
||||
filterset = CableFilterSet
|
||||
|
||||
@@ -2827,10 +2708,6 @@ class CableTestCase(TestCase):
|
||||
Cable(termination_a=interfaces[9], termination_b=interfaces[10], label='Cable 5', type=CableTypeChoices.TYPE_CAT6, status=CableStatusChoices.STATUS_PLANNED, color='e91e63', length=10, length_unit=CableLengthUnitChoices.UNIT_METER).save()
|
||||
Cable(termination_a=interfaces[11], termination_b=interfaces[0], label='Cable 6', type=CableTypeChoices.TYPE_CAT6, status=CableStatusChoices.STATUS_PLANNED, color='e91e63', length=20, length_unit=CableLengthUnitChoices.UNIT_METER).save()
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_label(self):
|
||||
params = {'label': ['Cable 1', 'Cable 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -2886,7 +2763,7 @@ class CableTestCase(TestCase):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
|
||||
class PowerPanelTestCase(TestCase):
|
||||
class PowerPanelTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = PowerPanel.objects.all()
|
||||
filterset = PowerPanelFilterSet
|
||||
|
||||
@@ -2931,10 +2808,6 @@ class PowerPanelTestCase(TestCase):
|
||||
)
|
||||
PowerPanel.objects.bulk_create(power_panels)
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Power Panel 1', 'Power Panel 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -2966,7 +2839,7 @@ class PowerPanelTestCase(TestCase):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
class PowerFeedTestCase(TestCase):
|
||||
class PowerFeedTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = PowerFeed.objects.all()
|
||||
filterset = PowerFeedFilterSet
|
||||
|
||||
@@ -3029,10 +2902,6 @@ class PowerFeedTestCase(TestCase):
|
||||
Cable(termination_a=power_feeds[0], termination_b=power_ports[0]).save()
|
||||
Cable(termination_a=power_feeds[1], termination_b=power_ports[1]).save()
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Power Feed 1', 'Power Feed 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -24,7 +24,7 @@ from utilities.tables import paginate_table
|
||||
from utilities.utils import csv_format, count_related
|
||||
from utilities.views import GetReturnURLMixin, ObjectPermissionRequiredMixin
|
||||
from virtualization.models import VirtualMachine
|
||||
from . import filters, forms, tables
|
||||
from . import filtersets, forms, tables
|
||||
from .choices import DeviceFaceChoices
|
||||
from .constants import NONCONNECTABLE_IFACE_TYPES
|
||||
from .models import (
|
||||
@@ -107,7 +107,7 @@ class RegionListView(generic.ObjectListView):
|
||||
'site_count',
|
||||
cumulative=True
|
||||
)
|
||||
filterset = filters.RegionFilterSet
|
||||
filterset = filtersets.RegionFilterSet
|
||||
filterset_form = forms.RegionFilterForm
|
||||
table = tables.RegionTable
|
||||
|
||||
@@ -163,7 +163,7 @@ class RegionBulkEditView(generic.BulkEditView):
|
||||
'site_count',
|
||||
cumulative=True
|
||||
)
|
||||
filterset = filters.RegionFilterSet
|
||||
filterset = filtersets.RegionFilterSet
|
||||
table = tables.RegionTable
|
||||
form = forms.RegionBulkEditForm
|
||||
|
||||
@@ -176,7 +176,7 @@ class RegionBulkDeleteView(generic.BulkDeleteView):
|
||||
'site_count',
|
||||
cumulative=True
|
||||
)
|
||||
filterset = filters.RegionFilterSet
|
||||
filterset = filtersets.RegionFilterSet
|
||||
table = tables.RegionTable
|
||||
|
||||
|
||||
@@ -192,7 +192,7 @@ class SiteGroupListView(generic.ObjectListView):
|
||||
'site_count',
|
||||
cumulative=True
|
||||
)
|
||||
filterset = filters.SiteGroupFilterSet
|
||||
filterset = filtersets.SiteGroupFilterSet
|
||||
filterset_form = forms.SiteGroupFilterForm
|
||||
table = tables.SiteGroupTable
|
||||
|
||||
@@ -248,7 +248,7 @@ class SiteGroupBulkEditView(generic.BulkEditView):
|
||||
'site_count',
|
||||
cumulative=True
|
||||
)
|
||||
filterset = filters.SiteGroupFilterSet
|
||||
filterset = filtersets.SiteGroupFilterSet
|
||||
table = tables.SiteGroupTable
|
||||
form = forms.SiteGroupBulkEditForm
|
||||
|
||||
@@ -261,7 +261,7 @@ class SiteGroupBulkDeleteView(generic.BulkDeleteView):
|
||||
'site_count',
|
||||
cumulative=True
|
||||
)
|
||||
filterset = filters.SiteGroupFilterSet
|
||||
filterset = filtersets.SiteGroupFilterSet
|
||||
table = tables.SiteGroupTable
|
||||
|
||||
|
||||
@@ -271,7 +271,7 @@ class SiteGroupBulkDeleteView(generic.BulkDeleteView):
|
||||
|
||||
class SiteListView(generic.ObjectListView):
|
||||
queryset = Site.objects.all()
|
||||
filterset = filters.SiteFilterSet
|
||||
filterset = filtersets.SiteFilterSet
|
||||
filterset_form = forms.SiteFilterForm
|
||||
table = tables.SiteTable
|
||||
|
||||
@@ -326,14 +326,14 @@ class SiteBulkImportView(generic.BulkImportView):
|
||||
|
||||
class SiteBulkEditView(generic.BulkEditView):
|
||||
queryset = Site.objects.prefetch_related('region', 'tenant')
|
||||
filterset = filters.SiteFilterSet
|
||||
filterset = filtersets.SiteFilterSet
|
||||
table = tables.SiteTable
|
||||
form = forms.SiteBulkEditForm
|
||||
|
||||
|
||||
class SiteBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = Site.objects.prefetch_related('region', 'tenant')
|
||||
filterset = filters.SiteFilterSet
|
||||
filterset = filtersets.SiteFilterSet
|
||||
table = tables.SiteTable
|
||||
|
||||
|
||||
@@ -355,7 +355,7 @@ class LocationListView(generic.ObjectListView):
|
||||
'rack_count',
|
||||
cumulative=True
|
||||
)
|
||||
filterset = filters.LocationFilterSet
|
||||
filterset = filtersets.LocationFilterSet
|
||||
filterset_form = forms.LocationFilterForm
|
||||
table = tables.LocationTable
|
||||
|
||||
@@ -414,7 +414,7 @@ class LocationBulkEditView(generic.BulkEditView):
|
||||
'rack_count',
|
||||
cumulative=True
|
||||
).prefetch_related('site')
|
||||
filterset = filters.LocationFilterSet
|
||||
filterset = filtersets.LocationFilterSet
|
||||
table = tables.LocationTable
|
||||
form = forms.LocationBulkEditForm
|
||||
|
||||
@@ -427,7 +427,7 @@ class LocationBulkDeleteView(generic.BulkDeleteView):
|
||||
'rack_count',
|
||||
cumulative=True
|
||||
).prefetch_related('site')
|
||||
filterset = filters.LocationFilterSet
|
||||
filterset = filtersets.LocationFilterSet
|
||||
table = tables.LocationTable
|
||||
|
||||
|
||||
@@ -478,7 +478,7 @@ class RackRoleBulkEditView(generic.BulkEditView):
|
||||
queryset = RackRole.objects.annotate(
|
||||
rack_count=count_related(Rack, 'role')
|
||||
)
|
||||
filterset = filters.RackRoleFilterSet
|
||||
filterset = filtersets.RackRoleFilterSet
|
||||
table = tables.RackRoleTable
|
||||
form = forms.RackRoleBulkEditForm
|
||||
|
||||
@@ -500,7 +500,7 @@ class RackListView(generic.ObjectListView):
|
||||
).annotate(
|
||||
device_count=count_related(Device, 'rack')
|
||||
)
|
||||
filterset = filters.RackFilterSet
|
||||
filterset = filtersets.RackFilterSet
|
||||
filterset_form = forms.RackFilterForm
|
||||
table = tables.RackDetailTable
|
||||
|
||||
@@ -513,7 +513,7 @@ class RackElevationListView(generic.ObjectListView):
|
||||
|
||||
def get(self, request):
|
||||
|
||||
racks = filters.RackFilterSet(request.GET, self.queryset).qs
|
||||
racks = filtersets.RackFilterSet(request.GET, self.queryset).qs
|
||||
total_count = racks.count()
|
||||
|
||||
# Determine ordering
|
||||
@@ -602,14 +602,14 @@ class RackBulkImportView(generic.BulkImportView):
|
||||
|
||||
class RackBulkEditView(generic.BulkEditView):
|
||||
queryset = Rack.objects.prefetch_related('site', 'location', 'tenant', 'role')
|
||||
filterset = filters.RackFilterSet
|
||||
filterset = filtersets.RackFilterSet
|
||||
table = tables.RackTable
|
||||
form = forms.RackBulkEditForm
|
||||
|
||||
|
||||
class RackBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = Rack.objects.prefetch_related('site', 'location', 'tenant', 'role')
|
||||
filterset = filters.RackFilterSet
|
||||
filterset = filtersets.RackFilterSet
|
||||
table = tables.RackTable
|
||||
|
||||
|
||||
@@ -619,7 +619,7 @@ class RackBulkDeleteView(generic.BulkDeleteView):
|
||||
|
||||
class RackReservationListView(generic.ObjectListView):
|
||||
queryset = RackReservation.objects.all()
|
||||
filterset = filters.RackReservationFilterSet
|
||||
filterset = filtersets.RackReservationFilterSet
|
||||
filterset_form = forms.RackReservationFilterForm
|
||||
table = tables.RackReservationTable
|
||||
|
||||
@@ -662,14 +662,14 @@ class RackReservationImportView(generic.BulkImportView):
|
||||
|
||||
class RackReservationBulkEditView(generic.BulkEditView):
|
||||
queryset = RackReservation.objects.prefetch_related('rack', 'user')
|
||||
filterset = filters.RackReservationFilterSet
|
||||
filterset = filtersets.RackReservationFilterSet
|
||||
table = tables.RackReservationTable
|
||||
form = forms.RackReservationBulkEditForm
|
||||
|
||||
|
||||
class RackReservationBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = RackReservation.objects.prefetch_related('rack', 'user')
|
||||
filterset = filters.RackReservationFilterSet
|
||||
filterset = filtersets.RackReservationFilterSet
|
||||
table = tables.RackReservationTable
|
||||
|
||||
|
||||
@@ -692,6 +692,8 @@ class ManufacturerView(generic.ObjectView):
|
||||
def get_extra_context(self, request, instance):
|
||||
devicetypes = DeviceType.objects.restrict(request.user, 'view').filter(
|
||||
manufacturer=instance
|
||||
).annotate(
|
||||
instance_count=count_related(Device, 'device_type')
|
||||
)
|
||||
|
||||
devicetypes_table = tables.DeviceTypeTable(devicetypes)
|
||||
@@ -722,7 +724,7 @@ class ManufacturerBulkEditView(generic.BulkEditView):
|
||||
queryset = Manufacturer.objects.annotate(
|
||||
devicetype_count=count_related(DeviceType, 'manufacturer')
|
||||
)
|
||||
filterset = filters.ManufacturerFilterSet
|
||||
filterset = filtersets.ManufacturerFilterSet
|
||||
table = tables.ManufacturerTable
|
||||
form = forms.ManufacturerBulkEditForm
|
||||
|
||||
@@ -742,7 +744,7 @@ class DeviceTypeListView(generic.ObjectListView):
|
||||
queryset = DeviceType.objects.prefetch_related('manufacturer').annotate(
|
||||
instance_count=count_related(Device, 'device_type')
|
||||
)
|
||||
filterset = filters.DeviceTypeFilterSet
|
||||
filterset = filtersets.DeviceTypeFilterSet
|
||||
filterset_form = forms.DeviceTypeFilterForm
|
||||
table = tables.DeviceTypeTable
|
||||
|
||||
@@ -848,7 +850,7 @@ class DeviceTypeBulkEditView(generic.BulkEditView):
|
||||
queryset = DeviceType.objects.prefetch_related('manufacturer').annotate(
|
||||
instance_count=count_related(Device, 'device_type')
|
||||
)
|
||||
filterset = filters.DeviceTypeFilterSet
|
||||
filterset = filtersets.DeviceTypeFilterSet
|
||||
table = tables.DeviceTypeTable
|
||||
form = forms.DeviceTypeBulkEditForm
|
||||
|
||||
@@ -857,7 +859,7 @@ class DeviceTypeBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = DeviceType.objects.prefetch_related('manufacturer').annotate(
|
||||
instance_count=count_related(Device, 'device_type')
|
||||
)
|
||||
filterset = filters.DeviceTypeFilterSet
|
||||
filterset = filtersets.DeviceTypeFilterSet
|
||||
table = tables.DeviceTypeTable
|
||||
|
||||
|
||||
@@ -1190,7 +1192,7 @@ class DeviceRoleBulkEditView(generic.BulkEditView):
|
||||
device_count=count_related(Device, 'device_role'),
|
||||
vm_count=count_related(VirtualMachine, 'role')
|
||||
)
|
||||
filterset = filters.DeviceRoleFilterSet
|
||||
filterset = filtersets.DeviceRoleFilterSet
|
||||
table = tables.DeviceRoleTable
|
||||
form = forms.DeviceRoleBulkEditForm
|
||||
|
||||
@@ -1249,7 +1251,7 @@ class PlatformBulkImportView(generic.BulkImportView):
|
||||
|
||||
class PlatformBulkEditView(generic.BulkEditView):
|
||||
queryset = Platform.objects.all()
|
||||
filterset = filters.PlatformFilterSet
|
||||
filterset = filtersets.PlatformFilterSet
|
||||
table = tables.PlatformTable
|
||||
form = forms.PlatformBulkEditForm
|
||||
|
||||
@@ -1265,7 +1267,7 @@ class PlatformBulkDeleteView(generic.BulkDeleteView):
|
||||
|
||||
class DeviceListView(generic.ObjectListView):
|
||||
queryset = Device.objects.all()
|
||||
filterset = filters.DeviceFilterSet
|
||||
filterset = filtersets.DeviceFilterSet
|
||||
filterset_form = forms.DeviceFilterForm
|
||||
table = tables.DeviceTable
|
||||
template_name = 'dcim/device_list.html'
|
||||
@@ -1405,7 +1407,7 @@ class DeviceInterfacesView(generic.ObjectView):
|
||||
template_name = 'dcim/device/interfaces.html'
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
interfaces = instance.vc_interfaces.restrict(request.user, 'view').prefetch_related(
|
||||
interfaces = instance.vc_interfaces().restrict(request.user, 'view').prefetch_related(
|
||||
Prefetch('ip_addresses', queryset=IPAddress.objects.restrict(request.user)),
|
||||
Prefetch('member_interfaces', queryset=Interface.objects.restrict(request.user)),
|
||||
'lag', 'cable', '_path__destination', 'tags',
|
||||
@@ -1527,7 +1529,7 @@ class DeviceLLDPNeighborsView(generic.ObjectView):
|
||||
template_name = 'dcim/device/lldp_neighbors.html'
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
interfaces = instance.vc_interfaces.restrict(request.user, 'view').prefetch_related(
|
||||
interfaces = instance.vc_interfaces().restrict(request.user, 'view').prefetch_related(
|
||||
'_path__destination'
|
||||
).exclude(
|
||||
type__in=NONCONNECTABLE_IFACE_TYPES
|
||||
@@ -1600,14 +1602,14 @@ class ChildDeviceBulkImportView(generic.BulkImportView):
|
||||
|
||||
class DeviceBulkEditView(generic.BulkEditView):
|
||||
queryset = Device.objects.prefetch_related('tenant', 'site', 'rack', 'device_role', 'device_type__manufacturer')
|
||||
filterset = filters.DeviceFilterSet
|
||||
filterset = filtersets.DeviceFilterSet
|
||||
table = tables.DeviceTable
|
||||
form = forms.DeviceBulkEditForm
|
||||
|
||||
|
||||
class DeviceBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = Device.objects.prefetch_related('tenant', 'site', 'rack', 'device_role', 'device_type__manufacturer')
|
||||
filterset = filters.DeviceFilterSet
|
||||
filterset = filtersets.DeviceFilterSet
|
||||
table = tables.DeviceTable
|
||||
|
||||
|
||||
@@ -1617,7 +1619,7 @@ class DeviceBulkDeleteView(generic.BulkDeleteView):
|
||||
|
||||
class ConsolePortListView(generic.ObjectListView):
|
||||
queryset = ConsolePort.objects.all()
|
||||
filterset = filters.ConsolePortFilterSet
|
||||
filterset = filtersets.ConsolePortFilterSet
|
||||
filterset_form = forms.ConsolePortFilterForm
|
||||
table = tables.ConsolePortTable
|
||||
action_buttons = ('import', 'export')
|
||||
@@ -1652,7 +1654,7 @@ class ConsolePortBulkImportView(generic.BulkImportView):
|
||||
|
||||
class ConsolePortBulkEditView(generic.BulkEditView):
|
||||
queryset = ConsolePort.objects.all()
|
||||
filterset = filters.ConsolePortFilterSet
|
||||
filterset = filtersets.ConsolePortFilterSet
|
||||
table = tables.ConsolePortTable
|
||||
form = forms.ConsolePortBulkEditForm
|
||||
|
||||
@@ -1667,7 +1669,7 @@ class ConsolePortBulkDisconnectView(BulkDisconnectView):
|
||||
|
||||
class ConsolePortBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = ConsolePort.objects.all()
|
||||
filterset = filters.ConsolePortFilterSet
|
||||
filterset = filtersets.ConsolePortFilterSet
|
||||
table = tables.ConsolePortTable
|
||||
|
||||
|
||||
@@ -1677,7 +1679,7 @@ class ConsolePortBulkDeleteView(generic.BulkDeleteView):
|
||||
|
||||
class ConsoleServerPortListView(generic.ObjectListView):
|
||||
queryset = ConsoleServerPort.objects.all()
|
||||
filterset = filters.ConsoleServerPortFilterSet
|
||||
filterset = filtersets.ConsoleServerPortFilterSet
|
||||
filterset_form = forms.ConsoleServerPortFilterForm
|
||||
table = tables.ConsoleServerPortTable
|
||||
action_buttons = ('import', 'export')
|
||||
@@ -1712,7 +1714,7 @@ class ConsoleServerPortBulkImportView(generic.BulkImportView):
|
||||
|
||||
class ConsoleServerPortBulkEditView(generic.BulkEditView):
|
||||
queryset = ConsoleServerPort.objects.all()
|
||||
filterset = filters.ConsoleServerPortFilterSet
|
||||
filterset = filtersets.ConsoleServerPortFilterSet
|
||||
table = tables.ConsoleServerPortTable
|
||||
form = forms.ConsoleServerPortBulkEditForm
|
||||
|
||||
@@ -1727,7 +1729,7 @@ class ConsoleServerPortBulkDisconnectView(BulkDisconnectView):
|
||||
|
||||
class ConsoleServerPortBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = ConsoleServerPort.objects.all()
|
||||
filterset = filters.ConsoleServerPortFilterSet
|
||||
filterset = filtersets.ConsoleServerPortFilterSet
|
||||
table = tables.ConsoleServerPortTable
|
||||
|
||||
|
||||
@@ -1737,7 +1739,7 @@ class ConsoleServerPortBulkDeleteView(generic.BulkDeleteView):
|
||||
|
||||
class PowerPortListView(generic.ObjectListView):
|
||||
queryset = PowerPort.objects.all()
|
||||
filterset = filters.PowerPortFilterSet
|
||||
filterset = filtersets.PowerPortFilterSet
|
||||
filterset_form = forms.PowerPortFilterForm
|
||||
table = tables.PowerPortTable
|
||||
action_buttons = ('import', 'export')
|
||||
@@ -1772,7 +1774,7 @@ class PowerPortBulkImportView(generic.BulkImportView):
|
||||
|
||||
class PowerPortBulkEditView(generic.BulkEditView):
|
||||
queryset = PowerPort.objects.all()
|
||||
filterset = filters.PowerPortFilterSet
|
||||
filterset = filtersets.PowerPortFilterSet
|
||||
table = tables.PowerPortTable
|
||||
form = forms.PowerPortBulkEditForm
|
||||
|
||||
@@ -1787,7 +1789,7 @@ class PowerPortBulkDisconnectView(BulkDisconnectView):
|
||||
|
||||
class PowerPortBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = PowerPort.objects.all()
|
||||
filterset = filters.PowerPortFilterSet
|
||||
filterset = filtersets.PowerPortFilterSet
|
||||
table = tables.PowerPortTable
|
||||
|
||||
|
||||
@@ -1797,7 +1799,7 @@ class PowerPortBulkDeleteView(generic.BulkDeleteView):
|
||||
|
||||
class PowerOutletListView(generic.ObjectListView):
|
||||
queryset = PowerOutlet.objects.all()
|
||||
filterset = filters.PowerOutletFilterSet
|
||||
filterset = filtersets.PowerOutletFilterSet
|
||||
filterset_form = forms.PowerOutletFilterForm
|
||||
table = tables.PowerOutletTable
|
||||
action_buttons = ('import', 'export')
|
||||
@@ -1832,7 +1834,7 @@ class PowerOutletBulkImportView(generic.BulkImportView):
|
||||
|
||||
class PowerOutletBulkEditView(generic.BulkEditView):
|
||||
queryset = PowerOutlet.objects.all()
|
||||
filterset = filters.PowerOutletFilterSet
|
||||
filterset = filtersets.PowerOutletFilterSet
|
||||
table = tables.PowerOutletTable
|
||||
form = forms.PowerOutletBulkEditForm
|
||||
|
||||
@@ -1847,7 +1849,7 @@ class PowerOutletBulkDisconnectView(BulkDisconnectView):
|
||||
|
||||
class PowerOutletBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = PowerOutlet.objects.all()
|
||||
filterset = filters.PowerOutletFilterSet
|
||||
filterset = filtersets.PowerOutletFilterSet
|
||||
table = tables.PowerOutletTable
|
||||
|
||||
|
||||
@@ -1857,7 +1859,7 @@ class PowerOutletBulkDeleteView(generic.BulkDeleteView):
|
||||
|
||||
class InterfaceListView(generic.ObjectListView):
|
||||
queryset = Interface.objects.all()
|
||||
filterset = filters.InterfaceFilterSet
|
||||
filterset = filtersets.InterfaceFilterSet
|
||||
filterset_form = forms.InterfaceFilterForm
|
||||
table = tables.InterfaceTable
|
||||
action_buttons = ('import', 'export')
|
||||
@@ -1927,7 +1929,7 @@ class InterfaceBulkImportView(generic.BulkImportView):
|
||||
|
||||
class InterfaceBulkEditView(generic.BulkEditView):
|
||||
queryset = Interface.objects.all()
|
||||
filterset = filters.InterfaceFilterSet
|
||||
filterset = filtersets.InterfaceFilterSet
|
||||
table = tables.InterfaceTable
|
||||
form = forms.InterfaceBulkEditForm
|
||||
|
||||
@@ -1942,7 +1944,7 @@ class InterfaceBulkDisconnectView(BulkDisconnectView):
|
||||
|
||||
class InterfaceBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = Interface.objects.all()
|
||||
filterset = filters.InterfaceFilterSet
|
||||
filterset = filtersets.InterfaceFilterSet
|
||||
table = tables.InterfaceTable
|
||||
|
||||
|
||||
@@ -1952,7 +1954,7 @@ class InterfaceBulkDeleteView(generic.BulkDeleteView):
|
||||
|
||||
class FrontPortListView(generic.ObjectListView):
|
||||
queryset = FrontPort.objects.all()
|
||||
filterset = filters.FrontPortFilterSet
|
||||
filterset = filtersets.FrontPortFilterSet
|
||||
filterset_form = forms.FrontPortFilterForm
|
||||
table = tables.FrontPortTable
|
||||
action_buttons = ('import', 'export')
|
||||
@@ -1987,7 +1989,7 @@ class FrontPortBulkImportView(generic.BulkImportView):
|
||||
|
||||
class FrontPortBulkEditView(generic.BulkEditView):
|
||||
queryset = FrontPort.objects.all()
|
||||
filterset = filters.FrontPortFilterSet
|
||||
filterset = filtersets.FrontPortFilterSet
|
||||
table = tables.FrontPortTable
|
||||
form = forms.FrontPortBulkEditForm
|
||||
|
||||
@@ -2002,7 +2004,7 @@ class FrontPortBulkDisconnectView(BulkDisconnectView):
|
||||
|
||||
class FrontPortBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = FrontPort.objects.all()
|
||||
filterset = filters.FrontPortFilterSet
|
||||
filterset = filtersets.FrontPortFilterSet
|
||||
table = tables.FrontPortTable
|
||||
|
||||
|
||||
@@ -2012,7 +2014,7 @@ class FrontPortBulkDeleteView(generic.BulkDeleteView):
|
||||
|
||||
class RearPortListView(generic.ObjectListView):
|
||||
queryset = RearPort.objects.all()
|
||||
filterset = filters.RearPortFilterSet
|
||||
filterset = filtersets.RearPortFilterSet
|
||||
filterset_form = forms.RearPortFilterForm
|
||||
table = tables.RearPortTable
|
||||
action_buttons = ('import', 'export')
|
||||
@@ -2047,7 +2049,7 @@ class RearPortBulkImportView(generic.BulkImportView):
|
||||
|
||||
class RearPortBulkEditView(generic.BulkEditView):
|
||||
queryset = RearPort.objects.all()
|
||||
filterset = filters.RearPortFilterSet
|
||||
filterset = filtersets.RearPortFilterSet
|
||||
table = tables.RearPortTable
|
||||
form = forms.RearPortBulkEditForm
|
||||
|
||||
@@ -2062,7 +2064,7 @@ class RearPortBulkDisconnectView(BulkDisconnectView):
|
||||
|
||||
class RearPortBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = RearPort.objects.all()
|
||||
filterset = filters.RearPortFilterSet
|
||||
filterset = filtersets.RearPortFilterSet
|
||||
table = tables.RearPortTable
|
||||
|
||||
|
||||
@@ -2072,7 +2074,7 @@ class RearPortBulkDeleteView(generic.BulkDeleteView):
|
||||
|
||||
class DeviceBayListView(generic.ObjectListView):
|
||||
queryset = DeviceBay.objects.all()
|
||||
filterset = filters.DeviceBayFilterSet
|
||||
filterset = filtersets.DeviceBayFilterSet
|
||||
filterset_form = forms.DeviceBayFilterForm
|
||||
table = tables.DeviceBayTable
|
||||
action_buttons = ('import', 'export')
|
||||
@@ -2172,7 +2174,7 @@ class DeviceBayBulkImportView(generic.BulkImportView):
|
||||
|
||||
class DeviceBayBulkEditView(generic.BulkEditView):
|
||||
queryset = DeviceBay.objects.all()
|
||||
filterset = filters.DeviceBayFilterSet
|
||||
filterset = filtersets.DeviceBayFilterSet
|
||||
table = tables.DeviceBayTable
|
||||
form = forms.DeviceBayBulkEditForm
|
||||
|
||||
@@ -2183,7 +2185,7 @@ class DeviceBayBulkRenameView(generic.BulkRenameView):
|
||||
|
||||
class DeviceBayBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = DeviceBay.objects.all()
|
||||
filterset = filters.DeviceBayFilterSet
|
||||
filterset = filtersets.DeviceBayFilterSet
|
||||
table = tables.DeviceBayTable
|
||||
|
||||
|
||||
@@ -2193,7 +2195,7 @@ class DeviceBayBulkDeleteView(generic.BulkDeleteView):
|
||||
|
||||
class InventoryItemListView(generic.ObjectListView):
|
||||
queryset = InventoryItem.objects.all()
|
||||
filterset = filters.InventoryItemFilterSet
|
||||
filterset = filtersets.InventoryItemFilterSet
|
||||
filterset_form = forms.InventoryItemFilterForm
|
||||
table = tables.InventoryItemTable
|
||||
action_buttons = ('import', 'export')
|
||||
@@ -2227,7 +2229,7 @@ class InventoryItemBulkImportView(generic.BulkImportView):
|
||||
|
||||
class InventoryItemBulkEditView(generic.BulkEditView):
|
||||
queryset = InventoryItem.objects.prefetch_related('device', 'manufacturer')
|
||||
filterset = filters.InventoryItemFilterSet
|
||||
filterset = filtersets.InventoryItemFilterSet
|
||||
table = tables.InventoryItemTable
|
||||
form = forms.InventoryItemBulkEditForm
|
||||
|
||||
@@ -2252,7 +2254,7 @@ class DeviceBulkAddConsolePortView(generic.BulkComponentCreateView):
|
||||
form = forms.ConsolePortBulkCreateForm
|
||||
queryset = ConsolePort.objects.all()
|
||||
model_form = forms.ConsolePortForm
|
||||
filterset = filters.DeviceFilterSet
|
||||
filterset = filtersets.DeviceFilterSet
|
||||
table = tables.DeviceTable
|
||||
default_return_url = 'dcim:device_list'
|
||||
|
||||
@@ -2263,7 +2265,7 @@ class DeviceBulkAddConsoleServerPortView(generic.BulkComponentCreateView):
|
||||
form = forms.ConsoleServerPortBulkCreateForm
|
||||
queryset = ConsoleServerPort.objects.all()
|
||||
model_form = forms.ConsoleServerPortForm
|
||||
filterset = filters.DeviceFilterSet
|
||||
filterset = filtersets.DeviceFilterSet
|
||||
table = tables.DeviceTable
|
||||
default_return_url = 'dcim:device_list'
|
||||
|
||||
@@ -2274,7 +2276,7 @@ class DeviceBulkAddPowerPortView(generic.BulkComponentCreateView):
|
||||
form = forms.PowerPortBulkCreateForm
|
||||
queryset = PowerPort.objects.all()
|
||||
model_form = forms.PowerPortForm
|
||||
filterset = filters.DeviceFilterSet
|
||||
filterset = filtersets.DeviceFilterSet
|
||||
table = tables.DeviceTable
|
||||
default_return_url = 'dcim:device_list'
|
||||
|
||||
@@ -2285,7 +2287,7 @@ class DeviceBulkAddPowerOutletView(generic.BulkComponentCreateView):
|
||||
form = forms.PowerOutletBulkCreateForm
|
||||
queryset = PowerOutlet.objects.all()
|
||||
model_form = forms.PowerOutletForm
|
||||
filterset = filters.DeviceFilterSet
|
||||
filterset = filtersets.DeviceFilterSet
|
||||
table = tables.DeviceTable
|
||||
default_return_url = 'dcim:device_list'
|
||||
|
||||
@@ -2296,7 +2298,7 @@ class DeviceBulkAddInterfaceView(generic.BulkComponentCreateView):
|
||||
form = forms.InterfaceBulkCreateForm
|
||||
queryset = Interface.objects.all()
|
||||
model_form = forms.InterfaceForm
|
||||
filterset = filters.DeviceFilterSet
|
||||
filterset = filtersets.DeviceFilterSet
|
||||
table = tables.DeviceTable
|
||||
default_return_url = 'dcim:device_list'
|
||||
|
||||
@@ -2307,7 +2309,7 @@ class DeviceBulkAddInterfaceView(generic.BulkComponentCreateView):
|
||||
# form = forms.FrontPortBulkCreateForm
|
||||
# queryset = FrontPort.objects.all()
|
||||
# model_form = forms.FrontPortForm
|
||||
# filterset = filters.DeviceFilterSet
|
||||
# filterset = filtersets.DeviceFilterSet
|
||||
# table = tables.DeviceTable
|
||||
# default_return_url = 'dcim:device_list'
|
||||
|
||||
@@ -2318,7 +2320,7 @@ class DeviceBulkAddRearPortView(generic.BulkComponentCreateView):
|
||||
form = forms.RearPortBulkCreateForm
|
||||
queryset = RearPort.objects.all()
|
||||
model_form = forms.RearPortForm
|
||||
filterset = filters.DeviceFilterSet
|
||||
filterset = filtersets.DeviceFilterSet
|
||||
table = tables.DeviceTable
|
||||
default_return_url = 'dcim:device_list'
|
||||
|
||||
@@ -2329,7 +2331,7 @@ class DeviceBulkAddDeviceBayView(generic.BulkComponentCreateView):
|
||||
form = forms.DeviceBayBulkCreateForm
|
||||
queryset = DeviceBay.objects.all()
|
||||
model_form = forms.DeviceBayForm
|
||||
filterset = filters.DeviceFilterSet
|
||||
filterset = filtersets.DeviceFilterSet
|
||||
table = tables.DeviceTable
|
||||
default_return_url = 'dcim:device_list'
|
||||
|
||||
@@ -2340,7 +2342,7 @@ class DeviceBulkAddInventoryItemView(generic.BulkComponentCreateView):
|
||||
form = forms.InventoryItemBulkCreateForm
|
||||
queryset = InventoryItem.objects.all()
|
||||
model_form = forms.InventoryItemForm
|
||||
filterset = filters.DeviceFilterSet
|
||||
filterset = filtersets.DeviceFilterSet
|
||||
table = tables.DeviceTable
|
||||
default_return_url = 'dcim:device_list'
|
||||
|
||||
@@ -2351,7 +2353,7 @@ class DeviceBulkAddInventoryItemView(generic.BulkComponentCreateView):
|
||||
|
||||
class CableListView(generic.ObjectListView):
|
||||
queryset = Cable.objects.all()
|
||||
filterset = filters.CableFilterSet
|
||||
filterset = filtersets.CableFilterSet
|
||||
filterset_form = forms.CableFilterForm
|
||||
table = tables.CableTable
|
||||
action_buttons = ('import', 'export')
|
||||
@@ -2484,14 +2486,14 @@ class CableBulkImportView(generic.BulkImportView):
|
||||
|
||||
class CableBulkEditView(generic.BulkEditView):
|
||||
queryset = Cable.objects.prefetch_related('termination_a', 'termination_b')
|
||||
filterset = filters.CableFilterSet
|
||||
filterset = filtersets.CableFilterSet
|
||||
table = tables.CableTable
|
||||
form = forms.CableBulkEditForm
|
||||
|
||||
|
||||
class CableBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = Cable.objects.prefetch_related('termination_a', 'termination_b')
|
||||
filterset = filters.CableFilterSet
|
||||
filterset = filtersets.CableFilterSet
|
||||
table = tables.CableTable
|
||||
|
||||
|
||||
@@ -2501,7 +2503,7 @@ class CableBulkDeleteView(generic.BulkDeleteView):
|
||||
|
||||
class ConsoleConnectionsListView(generic.ObjectListView):
|
||||
queryset = ConsolePort.objects.filter(_path__isnull=False).order_by('device')
|
||||
filterset = filters.ConsoleConnectionFilterSet
|
||||
filterset = filtersets.ConsoleConnectionFilterSet
|
||||
filterset_form = forms.ConsoleConnectionFilterForm
|
||||
table = tables.ConsoleConnectionTable
|
||||
template_name = 'dcim/connections_list.html'
|
||||
@@ -2531,7 +2533,7 @@ class ConsoleConnectionsListView(generic.ObjectListView):
|
||||
|
||||
class PowerConnectionsListView(generic.ObjectListView):
|
||||
queryset = PowerPort.objects.filter(_path__isnull=False).order_by('device')
|
||||
filterset = filters.PowerConnectionFilterSet
|
||||
filterset = filtersets.PowerConnectionFilterSet
|
||||
filterset_form = forms.PowerConnectionFilterForm
|
||||
table = tables.PowerConnectionTable
|
||||
template_name = 'dcim/connections_list.html'
|
||||
@@ -2565,7 +2567,7 @@ class InterfaceConnectionsListView(generic.ObjectListView):
|
||||
_path__isnull=False,
|
||||
pk__lt=F('_path__destination_id')
|
||||
).order_by('device')
|
||||
filterset = filters.InterfaceConnectionFilterSet
|
||||
filterset = filtersets.InterfaceConnectionFilterSet
|
||||
filterset_form = forms.InterfaceConnectionFilterForm
|
||||
table = tables.InterfaceConnectionTable
|
||||
template_name = 'dcim/connections_list.html'
|
||||
@@ -2604,7 +2606,7 @@ class VirtualChassisListView(generic.ObjectListView):
|
||||
member_count=count_related(Device, 'virtual_chassis')
|
||||
)
|
||||
table = tables.VirtualChassisTable
|
||||
filterset = filters.VirtualChassisFilterSet
|
||||
filterset = filtersets.VirtualChassisFilterSet
|
||||
filterset_form = forms.VirtualChassisFilterForm
|
||||
|
||||
|
||||
@@ -2812,14 +2814,14 @@ class VirtualChassisBulkImportView(generic.BulkImportView):
|
||||
|
||||
class VirtualChassisBulkEditView(generic.BulkEditView):
|
||||
queryset = VirtualChassis.objects.all()
|
||||
filterset = filters.VirtualChassisFilterSet
|
||||
filterset = filtersets.VirtualChassisFilterSet
|
||||
table = tables.VirtualChassisTable
|
||||
form = forms.VirtualChassisBulkEditForm
|
||||
|
||||
|
||||
class VirtualChassisBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = VirtualChassis.objects.all()
|
||||
filterset = filters.VirtualChassisFilterSet
|
||||
filterset = filtersets.VirtualChassisFilterSet
|
||||
table = tables.VirtualChassisTable
|
||||
|
||||
|
||||
@@ -2833,7 +2835,7 @@ class PowerPanelListView(generic.ObjectListView):
|
||||
).annotate(
|
||||
powerfeed_count=count_related(PowerFeed, 'power_panel')
|
||||
)
|
||||
filterset = filters.PowerPanelFilterSet
|
||||
filterset = filtersets.PowerPanelFilterSet
|
||||
filterset_form = forms.PowerPanelFilterForm
|
||||
table = tables.PowerPanelTable
|
||||
|
||||
@@ -2873,7 +2875,7 @@ class PowerPanelBulkImportView(generic.BulkImportView):
|
||||
|
||||
class PowerPanelBulkEditView(generic.BulkEditView):
|
||||
queryset = PowerPanel.objects.prefetch_related('site', 'location')
|
||||
filterset = filters.PowerPanelFilterSet
|
||||
filterset = filtersets.PowerPanelFilterSet
|
||||
table = tables.PowerPanelTable
|
||||
form = forms.PowerPanelBulkEditForm
|
||||
|
||||
@@ -2884,7 +2886,7 @@ class PowerPanelBulkDeleteView(generic.BulkDeleteView):
|
||||
).annotate(
|
||||
powerfeed_count=count_related(PowerFeed, 'power_panel')
|
||||
)
|
||||
filterset = filters.PowerPanelFilterSet
|
||||
filterset = filtersets.PowerPanelFilterSet
|
||||
table = tables.PowerPanelTable
|
||||
|
||||
|
||||
@@ -2894,7 +2896,7 @@ class PowerPanelBulkDeleteView(generic.BulkDeleteView):
|
||||
|
||||
class PowerFeedListView(generic.ObjectListView):
|
||||
queryset = PowerFeed.objects.all()
|
||||
filterset = filters.PowerFeedFilterSet
|
||||
filterset = filtersets.PowerFeedFilterSet
|
||||
filterset_form = forms.PowerFeedFilterForm
|
||||
table = tables.PowerFeedTable
|
||||
|
||||
@@ -2920,7 +2922,7 @@ class PowerFeedBulkImportView(generic.BulkImportView):
|
||||
|
||||
class PowerFeedBulkEditView(generic.BulkEditView):
|
||||
queryset = PowerFeed.objects.prefetch_related('power_panel', 'rack')
|
||||
filterset = filters.PowerFeedFilterSet
|
||||
filterset = filtersets.PowerFeedFilterSet
|
||||
table = tables.PowerFeedTable
|
||||
form = forms.PowerFeedBulkEditForm
|
||||
|
||||
@@ -2931,5 +2933,5 @@ class PowerFeedBulkDisconnectView(BulkDisconnectView):
|
||||
|
||||
class PowerFeedBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = PowerFeed.objects.prefetch_related('power_panel', 'rack')
|
||||
filterset = filters.PowerFeedFilterSet
|
||||
filterset = filtersets.PowerFeedFilterSet
|
||||
table = tables.PowerFeedTable
|
||||
|
||||
@@ -9,7 +9,7 @@ from rest_framework.routers import APIRootView
|
||||
from rest_framework.viewsets import ReadOnlyModelViewSet, ViewSet
|
||||
from rq import Worker
|
||||
|
||||
from extras import filters
|
||||
from extras import filtersets
|
||||
from extras.choices import JobResultStatusChoices
|
||||
from extras.models import *
|
||||
from extras.models import CustomField
|
||||
@@ -61,7 +61,7 @@ class WebhookViewSet(ModelViewSet):
|
||||
metadata_class = ContentTypeMetadata
|
||||
queryset = Webhook.objects.all()
|
||||
serializer_class = serializers.WebhookSerializer
|
||||
filterset_class = filters.WebhookFilterSet
|
||||
filterset_class = filtersets.WebhookFilterSet
|
||||
|
||||
|
||||
#
|
||||
@@ -72,7 +72,7 @@ class CustomFieldViewSet(ModelViewSet):
|
||||
metadata_class = ContentTypeMetadata
|
||||
queryset = CustomField.objects.all()
|
||||
serializer_class = serializers.CustomFieldSerializer
|
||||
filterset_class = filters.CustomFieldFilterSet
|
||||
filterset_class = filtersets.CustomFieldFilterSet
|
||||
|
||||
|
||||
class CustomFieldModelViewSet(ModelViewSet):
|
||||
@@ -101,7 +101,7 @@ class CustomLinkViewSet(ModelViewSet):
|
||||
metadata_class = ContentTypeMetadata
|
||||
queryset = CustomLink.objects.all()
|
||||
serializer_class = serializers.CustomLinkSerializer
|
||||
filterset_class = filters.CustomLinkFilterSet
|
||||
filterset_class = filtersets.CustomLinkFilterSet
|
||||
|
||||
|
||||
#
|
||||
@@ -112,7 +112,7 @@ class ExportTemplateViewSet(ModelViewSet):
|
||||
metadata_class = ContentTypeMetadata
|
||||
queryset = ExportTemplate.objects.all()
|
||||
serializer_class = serializers.ExportTemplateSerializer
|
||||
filterset_class = filters.ExportTemplateFilterSet
|
||||
filterset_class = filtersets.ExportTemplateFilterSet
|
||||
|
||||
|
||||
#
|
||||
@@ -124,7 +124,7 @@ class TagViewSet(ModelViewSet):
|
||||
tagged_items=count_related(TaggedItem, 'tag')
|
||||
)
|
||||
serializer_class = serializers.TagSerializer
|
||||
filterset_class = filters.TagFilterSet
|
||||
filterset_class = filtersets.TagFilterSet
|
||||
|
||||
|
||||
#
|
||||
@@ -135,7 +135,7 @@ class ImageAttachmentViewSet(ModelViewSet):
|
||||
metadata_class = ContentTypeMetadata
|
||||
queryset = ImageAttachment.objects.all()
|
||||
serializer_class = serializers.ImageAttachmentSerializer
|
||||
filterset_class = filters.ImageAttachmentFilterSet
|
||||
filterset_class = filtersets.ImageAttachmentFilterSet
|
||||
|
||||
|
||||
#
|
||||
@@ -146,7 +146,7 @@ class JournalEntryViewSet(ModelViewSet):
|
||||
metadata_class = ContentTypeMetadata
|
||||
queryset = JournalEntry.objects.all()
|
||||
serializer_class = serializers.JournalEntrySerializer
|
||||
filterset_class = filters.JournalEntryFilterSet
|
||||
filterset_class = filtersets.JournalEntryFilterSet
|
||||
|
||||
|
||||
#
|
||||
@@ -158,7 +158,7 @@ class ConfigContextViewSet(ModelViewSet):
|
||||
'regions', 'site_groups', 'sites', 'roles', 'platforms', 'tenant_groups', 'tenants',
|
||||
)
|
||||
serializer_class = serializers.ConfigContextSerializer
|
||||
filterset_class = filters.ConfigContextFilterSet
|
||||
filterset_class = filtersets.ConfigContextFilterSet
|
||||
|
||||
|
||||
#
|
||||
@@ -358,7 +358,7 @@ class ObjectChangeViewSet(ReadOnlyModelViewSet):
|
||||
metadata_class = ContentTypeMetadata
|
||||
queryset = ObjectChange.objects.prefetch_related('user')
|
||||
serializer_class = serializers.ObjectChangeSerializer
|
||||
filterset_class = filters.ObjectChangeFilterSet
|
||||
filterset_class = filtersets.ObjectChangeFilterSet
|
||||
|
||||
|
||||
#
|
||||
@@ -371,7 +371,7 @@ class JobResultViewSet(ReadOnlyModelViewSet):
|
||||
"""
|
||||
queryset = JobResult.objects.prefetch_related('user')
|
||||
serializer_class = serializers.JobResultSerializer
|
||||
filterset_class = filters.JobResultFilterSet
|
||||
filterset_class = filtersets.JobResultFilterSet
|
||||
|
||||
|
||||
#
|
||||
@@ -384,4 +384,4 @@ class ContentTypeViewSet(ReadOnlyModelViewSet):
|
||||
"""
|
||||
queryset = ContentType.objects.order_by('app_label', 'model')
|
||||
serializer_class = serializers.ContentTypeSerializer
|
||||
filterset_class = filters.ContentTypeFilterSet
|
||||
filterset_class = filtersets.ContentTypeFilterSet
|
||||
|
||||
@@ -7,5 +7,6 @@ EXTRAS_FEATURES = [
|
||||
'custom_links',
|
||||
'export_templates',
|
||||
'job_results',
|
||||
'tags',
|
||||
'webhooks'
|
||||
]
|
||||
|
||||
@@ -1,31 +1,12 @@
|
||||
import django_filters
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db.models import Q
|
||||
from django.forms import DateField, IntegerField, NullBooleanField
|
||||
|
||||
from dcim.models import DeviceRole, DeviceType, Platform, Region, Site, SiteGroup
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from utilities.filters import BaseFilterSet, ContentTypeFilter
|
||||
from virtualization.models import Cluster, ClusterGroup
|
||||
from .models import Tag
|
||||
from .choices import *
|
||||
from .models import *
|
||||
|
||||
|
||||
__all__ = (
|
||||
'ConfigContextFilterSet',
|
||||
'ContentTypeFilterSet',
|
||||
'CreatedUpdatedFilterSet',
|
||||
'CustomFieldFilter',
|
||||
'CustomLinkFilterSet',
|
||||
'CustomFieldModelFilterSet',
|
||||
'ExportTemplateFilterSet',
|
||||
'ImageAttachmentFilterSet',
|
||||
'JournalEntryFilterSet',
|
||||
'LocalConfigContextFilterSet',
|
||||
'ObjectChangeFilterSet',
|
||||
'TagFilterSet',
|
||||
'WebhookFilterSet',
|
||||
'TagFilter',
|
||||
)
|
||||
|
||||
EXACT_FILTER_TYPES = (
|
||||
@@ -36,20 +17,6 @@ EXACT_FILTER_TYPES = (
|
||||
)
|
||||
|
||||
|
||||
class WebhookFilterSet(BaseFilterSet):
|
||||
content_types = ContentTypeFilter()
|
||||
http_method = django_filters.MultipleChoiceFilter(
|
||||
choices=WebhookHttpMethodChoices
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Webhook
|
||||
fields = [
|
||||
'id', 'content_types', 'name', 'type_create', 'type_update', 'type_delete', 'payload_url', 'enabled',
|
||||
'http_method', 'http_content_type', 'secret', 'ssl_verification', 'ca_file_path',
|
||||
]
|
||||
|
||||
|
||||
class CustomFieldFilter(django_filters.Filter):
|
||||
"""
|
||||
Filter objects by the presence of a CustomFieldValue. The filter's name is used as the CustomField name.
|
||||
@@ -73,331 +40,16 @@ class CustomFieldFilter(django_filters.Filter):
|
||||
self.lookup_expr = 'icontains'
|
||||
|
||||
|
||||
class CustomFieldModelFilterSet(django_filters.FilterSet):
|
||||
class TagFilter(django_filters.ModelMultipleChoiceFilter):
|
||||
"""
|
||||
Dynamically add a Filter for each CustomField applicable to the parent model.
|
||||
Match on one or more assigned tags. If multiple tags are specified (e.g. ?tag=foo&tag=bar), the queryset is filtered
|
||||
to objects matching all tags.
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
kwargs.setdefault('field_name', 'tags__slug')
|
||||
kwargs.setdefault('to_field_name', 'slug')
|
||||
kwargs.setdefault('conjoined', True)
|
||||
kwargs.setdefault('queryset', Tag.objects.all())
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
custom_fields = CustomField.objects.filter(
|
||||
content_types=ContentType.objects.get_for_model(self._meta.model)
|
||||
).exclude(
|
||||
filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED
|
||||
)
|
||||
for cf in custom_fields:
|
||||
self.filters['cf_{}'.format(cf.name)] = CustomFieldFilter(field_name=cf.name, custom_field=cf)
|
||||
|
||||
|
||||
class CustomFieldFilterSet(django_filters.FilterSet):
|
||||
content_types = ContentTypeFilter()
|
||||
|
||||
class Meta:
|
||||
model = CustomField
|
||||
fields = ['id', 'content_types', 'name', 'required', 'filter_logic', 'weight']
|
||||
|
||||
|
||||
class CustomLinkFilterSet(BaseFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = CustomLink
|
||||
fields = ['id', 'content_type', 'name', 'link_text', 'link_url', 'weight', 'group_name', 'new_window']
|
||||
|
||||
|
||||
class ExportTemplateFilterSet(BaseFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = ExportTemplate
|
||||
fields = ['id', 'content_type', 'name']
|
||||
|
||||
|
||||
class ImageAttachmentFilterSet(BaseFilterSet):
|
||||
content_type = ContentTypeFilter()
|
||||
|
||||
class Meta:
|
||||
model = ImageAttachment
|
||||
fields = ['id', 'content_type_id', 'object_id', 'name']
|
||||
|
||||
|
||||
class JournalEntryFilterSet(BaseFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
created = django_filters.DateTimeFromToRangeFilter()
|
||||
assigned_object_type = ContentTypeFilter()
|
||||
created_by_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=User.objects.all(),
|
||||
label='User (ID)',
|
||||
)
|
||||
created_by = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='created_by__username',
|
||||
queryset=User.objects.all(),
|
||||
to_field_name='username',
|
||||
label='User (name)',
|
||||
)
|
||||
kind = django_filters.MultipleChoiceFilter(
|
||||
choices=JournalEntryKindChoices
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = JournalEntry
|
||||
fields = ['id', 'assigned_object_type_id', 'assigned_object_id', 'created', 'kind']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(comments__icontains=value)
|
||||
|
||||
|
||||
class TagFilterSet(BaseFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Tag
|
||||
fields = ['id', 'name', 'slug', 'color']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(name__icontains=value) |
|
||||
Q(slug__icontains=value)
|
||||
)
|
||||
|
||||
|
||||
class ConfigContextFilterSet(BaseFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
region_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='regions',
|
||||
queryset=Region.objects.all(),
|
||||
label='Region',
|
||||
)
|
||||
region = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='regions__slug',
|
||||
queryset=Region.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Region (slug)',
|
||||
)
|
||||
site_group = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='site_groups__slug',
|
||||
queryset=SiteGroup.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Site group (slug)',
|
||||
)
|
||||
site_group_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='site_groups',
|
||||
queryset=SiteGroup.objects.all(),
|
||||
label='Site group',
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='sites',
|
||||
queryset=Site.objects.all(),
|
||||
label='Site',
|
||||
)
|
||||
site = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='sites__slug',
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Site (slug)',
|
||||
)
|
||||
device_type_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='device_types',
|
||||
queryset=DeviceType.objects.all(),
|
||||
label='Device type',
|
||||
)
|
||||
role_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='roles',
|
||||
queryset=DeviceRole.objects.all(),
|
||||
label='Role',
|
||||
)
|
||||
role = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='roles__slug',
|
||||
queryset=DeviceRole.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Role (slug)',
|
||||
)
|
||||
platform_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='platforms',
|
||||
queryset=Platform.objects.all(),
|
||||
label='Platform',
|
||||
)
|
||||
platform = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='platforms__slug',
|
||||
queryset=Platform.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Platform (slug)',
|
||||
)
|
||||
cluster_group_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='cluster_groups',
|
||||
queryset=ClusterGroup.objects.all(),
|
||||
label='Cluster group',
|
||||
)
|
||||
cluster_group = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='cluster_groups__slug',
|
||||
queryset=ClusterGroup.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Cluster group (slug)',
|
||||
)
|
||||
cluster_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='clusters',
|
||||
queryset=Cluster.objects.all(),
|
||||
label='Cluster',
|
||||
)
|
||||
tenant_group_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='tenant_groups',
|
||||
queryset=TenantGroup.objects.all(),
|
||||
label='Tenant group',
|
||||
)
|
||||
tenant_group = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='tenant_groups__slug',
|
||||
queryset=TenantGroup.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Tenant group (slug)',
|
||||
)
|
||||
tenant_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='tenants',
|
||||
queryset=Tenant.objects.all(),
|
||||
label='Tenant',
|
||||
)
|
||||
tenant = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='tenants__slug',
|
||||
queryset=Tenant.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Tenant (slug)',
|
||||
)
|
||||
tag = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='tags__slug',
|
||||
queryset=Tag.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Tag (slug)',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ConfigContext
|
||||
fields = ['id', 'name', 'is_active']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(name__icontains=value) |
|
||||
Q(description__icontains=value) |
|
||||
Q(data__icontains=value)
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Filter for Local Config Context Data
|
||||
#
|
||||
|
||||
class LocalConfigContextFilterSet(django_filters.FilterSet):
|
||||
local_context_data = django_filters.BooleanFilter(
|
||||
method='_local_context_data',
|
||||
label='Has local config context data',
|
||||
)
|
||||
|
||||
def _local_context_data(self, queryset, name, value):
|
||||
return queryset.exclude(local_context_data__isnull=value)
|
||||
|
||||
|
||||
class ObjectChangeFilterSet(BaseFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
time = django_filters.DateTimeFromToRangeFilter()
|
||||
changed_object_type = ContentTypeFilter()
|
||||
user_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=User.objects.all(),
|
||||
label='User (ID)',
|
||||
)
|
||||
user = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='user__username',
|
||||
queryset=User.objects.all(),
|
||||
to_field_name='username',
|
||||
label='User name',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ObjectChange
|
||||
fields = [
|
||||
'id', 'user', 'user_name', 'request_id', 'action', 'changed_object_type_id', 'changed_object_id',
|
||||
'object_repr',
|
||||
]
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(user_name__icontains=value) |
|
||||
Q(object_repr__icontains=value)
|
||||
)
|
||||
|
||||
|
||||
class CreatedUpdatedFilterSet(django_filters.FilterSet):
|
||||
created = django_filters.DateFilter()
|
||||
created__gte = django_filters.DateFilter(
|
||||
field_name='created',
|
||||
lookup_expr='gte'
|
||||
)
|
||||
created__lte = django_filters.DateFilter(
|
||||
field_name='created',
|
||||
lookup_expr='lte'
|
||||
)
|
||||
last_updated = django_filters.DateTimeFilter()
|
||||
last_updated__gte = django_filters.DateTimeFilter(
|
||||
field_name='last_updated',
|
||||
lookup_expr='gte'
|
||||
)
|
||||
last_updated__lte = django_filters.DateTimeFilter(
|
||||
field_name='last_updated',
|
||||
lookup_expr='lte'
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Job Results
|
||||
#
|
||||
|
||||
class JobResultFilterSet(BaseFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
created = django_filters.DateTimeFilter()
|
||||
completed = django_filters.DateTimeFilter()
|
||||
status = django_filters.MultipleChoiceFilter(
|
||||
choices=JobResultStatusChoices,
|
||||
null_value=None
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = JobResult
|
||||
fields = [
|
||||
'id', 'created', 'completed', 'status', 'user', 'obj_type', 'name'
|
||||
]
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(user__username__icontains=value)
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# ContentTypes
|
||||
#
|
||||
|
||||
class ContentTypeFilterSet(django_filters.FilterSet):
|
||||
|
||||
class Meta:
|
||||
model = ContentType
|
||||
fields = ['id', 'app_label', 'model']
|
||||
|
||||
373
netbox/extras/filtersets.py
Normal file
373
netbox/extras/filtersets.py
Normal file
@@ -0,0 +1,373 @@
|
||||
import django_filters
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db.models import Q
|
||||
|
||||
from dcim.models import DeviceRole, DeviceType, Platform, Region, Site, SiteGroup
|
||||
from netbox.filtersets import BaseFilterSet, ChangeLoggedModelFilterSet
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from utilities.filters import ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter
|
||||
from virtualization.models import Cluster, ClusterGroup
|
||||
from .choices import *
|
||||
from .models import *
|
||||
|
||||
|
||||
__all__ = (
|
||||
'ConfigContextFilterSet',
|
||||
'ContentTypeFilterSet',
|
||||
'CustomLinkFilterSet',
|
||||
'ExportTemplateFilterSet',
|
||||
'ImageAttachmentFilterSet',
|
||||
'JournalEntryFilterSet',
|
||||
'LocalConfigContextFilterSet',
|
||||
'ObjectChangeFilterSet',
|
||||
'TagFilterSet',
|
||||
'WebhookFilterSet',
|
||||
)
|
||||
|
||||
EXACT_FILTER_TYPES = (
|
||||
CustomFieldTypeChoices.TYPE_BOOLEAN,
|
||||
CustomFieldTypeChoices.TYPE_DATE,
|
||||
CustomFieldTypeChoices.TYPE_INTEGER,
|
||||
CustomFieldTypeChoices.TYPE_SELECT,
|
||||
)
|
||||
|
||||
|
||||
class WebhookFilterSet(BaseFilterSet):
|
||||
content_types = ContentTypeFilter()
|
||||
http_method = django_filters.MultipleChoiceFilter(
|
||||
choices=WebhookHttpMethodChoices
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Webhook
|
||||
fields = [
|
||||
'id', 'content_types', 'name', 'type_create', 'type_update', 'type_delete', 'payload_url', 'enabled',
|
||||
'http_method', 'http_content_type', 'secret', 'ssl_verification', 'ca_file_path',
|
||||
]
|
||||
|
||||
|
||||
class CustomFieldFilterSet(django_filters.FilterSet):
|
||||
content_types = ContentTypeFilter()
|
||||
|
||||
class Meta:
|
||||
model = CustomField
|
||||
fields = ['id', 'content_types', 'name', 'required', 'filter_logic', 'weight']
|
||||
|
||||
|
||||
class CustomLinkFilterSet(BaseFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = CustomLink
|
||||
fields = ['id', 'content_type', 'name', 'link_text', 'link_url', 'weight', 'group_name', 'new_window']
|
||||
|
||||
|
||||
class ExportTemplateFilterSet(BaseFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = ExportTemplate
|
||||
fields = ['id', 'content_type', 'name']
|
||||
|
||||
|
||||
class ImageAttachmentFilterSet(BaseFilterSet):
|
||||
created = django_filters.DateTimeFilter()
|
||||
content_type = ContentTypeFilter()
|
||||
|
||||
class Meta:
|
||||
model = ImageAttachment
|
||||
fields = ['id', 'content_type_id', 'object_id', 'name']
|
||||
|
||||
|
||||
class JournalEntryFilterSet(ChangeLoggedModelFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
created = django_filters.DateTimeFromToRangeFilter()
|
||||
assigned_object_type = ContentTypeFilter()
|
||||
created_by_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=User.objects.all(),
|
||||
label='User (ID)',
|
||||
)
|
||||
created_by = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='created_by__username',
|
||||
queryset=User.objects.all(),
|
||||
to_field_name='username',
|
||||
label='User (name)',
|
||||
)
|
||||
kind = django_filters.MultipleChoiceFilter(
|
||||
choices=JournalEntryKindChoices
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = JournalEntry
|
||||
fields = ['id', 'assigned_object_type_id', 'assigned_object_id', 'created', 'kind']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(comments__icontains=value)
|
||||
|
||||
|
||||
class TagFilterSet(ChangeLoggedModelFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
content_type = MultiValueCharFilter(
|
||||
method='_content_type'
|
||||
)
|
||||
content_type_id = MultiValueNumberFilter(
|
||||
method='_content_type_id'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Tag
|
||||
fields = ['id', 'name', 'slug', 'color']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(name__icontains=value) |
|
||||
Q(slug__icontains=value)
|
||||
)
|
||||
|
||||
def _content_type(self, queryset, name, values):
|
||||
ct_filter = Q()
|
||||
|
||||
# Compile list of app_label & model pairings
|
||||
for value in values:
|
||||
try:
|
||||
app_label, model = value.lower().split('.')
|
||||
ct_filter |= Q(
|
||||
app_label=app_label,
|
||||
model=model
|
||||
)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# Get ContentType instances
|
||||
content_types = ContentType.objects.filter(ct_filter)
|
||||
|
||||
return queryset.filter(extras_taggeditem_items__content_type__in=content_types).distinct()
|
||||
|
||||
def _content_type_id(self, queryset, name, values):
|
||||
|
||||
# Get ContentType instances
|
||||
content_types = ContentType.objects.filter(pk__in=values)
|
||||
|
||||
return queryset.filter(extras_taggeditem_items__content_type__in=content_types).distinct()
|
||||
|
||||
|
||||
class ConfigContextFilterSet(ChangeLoggedModelFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
region_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='regions',
|
||||
queryset=Region.objects.all(),
|
||||
label='Region',
|
||||
)
|
||||
region = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='regions__slug',
|
||||
queryset=Region.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Region (slug)',
|
||||
)
|
||||
site_group = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='site_groups__slug',
|
||||
queryset=SiteGroup.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Site group (slug)',
|
||||
)
|
||||
site_group_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='site_groups',
|
||||
queryset=SiteGroup.objects.all(),
|
||||
label='Site group',
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='sites',
|
||||
queryset=Site.objects.all(),
|
||||
label='Site',
|
||||
)
|
||||
site = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='sites__slug',
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Site (slug)',
|
||||
)
|
||||
device_type_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='device_types',
|
||||
queryset=DeviceType.objects.all(),
|
||||
label='Device type',
|
||||
)
|
||||
role_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='roles',
|
||||
queryset=DeviceRole.objects.all(),
|
||||
label='Role',
|
||||
)
|
||||
role = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='roles__slug',
|
||||
queryset=DeviceRole.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Role (slug)',
|
||||
)
|
||||
platform_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='platforms',
|
||||
queryset=Platform.objects.all(),
|
||||
label='Platform',
|
||||
)
|
||||
platform = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='platforms__slug',
|
||||
queryset=Platform.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Platform (slug)',
|
||||
)
|
||||
cluster_group_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='cluster_groups',
|
||||
queryset=ClusterGroup.objects.all(),
|
||||
label='Cluster group',
|
||||
)
|
||||
cluster_group = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='cluster_groups__slug',
|
||||
queryset=ClusterGroup.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Cluster group (slug)',
|
||||
)
|
||||
cluster_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='clusters',
|
||||
queryset=Cluster.objects.all(),
|
||||
label='Cluster',
|
||||
)
|
||||
tenant_group_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='tenant_groups',
|
||||
queryset=TenantGroup.objects.all(),
|
||||
label='Tenant group',
|
||||
)
|
||||
tenant_group = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='tenant_groups__slug',
|
||||
queryset=TenantGroup.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Tenant group (slug)',
|
||||
)
|
||||
tenant_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='tenants',
|
||||
queryset=Tenant.objects.all(),
|
||||
label='Tenant',
|
||||
)
|
||||
tenant = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='tenants__slug',
|
||||
queryset=Tenant.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Tenant (slug)',
|
||||
)
|
||||
tag = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='tags__slug',
|
||||
queryset=Tag.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Tag (slug)',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ConfigContext
|
||||
fields = ['id', 'name', 'is_active']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(name__icontains=value) |
|
||||
Q(description__icontains=value) |
|
||||
Q(data__icontains=value)
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Filter for Local Config Context Data
|
||||
#
|
||||
|
||||
class LocalConfigContextFilterSet(django_filters.FilterSet):
|
||||
local_context_data = django_filters.BooleanFilter(
|
||||
method='_local_context_data',
|
||||
label='Has local config context data',
|
||||
)
|
||||
|
||||
def _local_context_data(self, queryset, name, value):
|
||||
return queryset.exclude(local_context_data__isnull=value)
|
||||
|
||||
|
||||
class ObjectChangeFilterSet(BaseFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
time = django_filters.DateTimeFromToRangeFilter()
|
||||
changed_object_type = ContentTypeFilter()
|
||||
user_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=User.objects.all(),
|
||||
label='User (ID)',
|
||||
)
|
||||
user = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='user__username',
|
||||
queryset=User.objects.all(),
|
||||
to_field_name='username',
|
||||
label='User name',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ObjectChange
|
||||
fields = [
|
||||
'id', 'user', 'user_name', 'request_id', 'action', 'changed_object_type_id', 'changed_object_id',
|
||||
'object_repr',
|
||||
]
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(user_name__icontains=value) |
|
||||
Q(object_repr__icontains=value)
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Job Results
|
||||
#
|
||||
|
||||
class JobResultFilterSet(BaseFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
created = django_filters.DateTimeFilter()
|
||||
completed = django_filters.DateTimeFilter()
|
||||
status = django_filters.MultipleChoiceFilter(
|
||||
choices=JobResultStatusChoices,
|
||||
null_value=None
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = JobResult
|
||||
fields = [
|
||||
'id', 'created', 'completed', 'status', 'user', 'obj_type', 'name'
|
||||
]
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(user__username__icontains=value)
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# ContentTypes
|
||||
#
|
||||
|
||||
class ContentTypeFilterSet(django_filters.FilterSet):
|
||||
|
||||
class Meta:
|
||||
model = ContentType
|
||||
fields = ['id', 'app_label', 'model']
|
||||
@@ -8,12 +8,13 @@ from dcim.models import DeviceRole, DeviceType, Platform, Region, Site, SiteGrou
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from utilities.forms import (
|
||||
add_blank_choice, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ColorSelect,
|
||||
CommentField, CSVModelForm, DateTimePicker, DynamicModelMultipleChoiceField, JSONField, SlugField, StaticSelect2,
|
||||
BOOLEAN_WITH_BLANK_CHOICES,
|
||||
CommentField, ContentTypeMultipleChoiceField, CSVModelForm, DateTimePicker, DynamicModelMultipleChoiceField,
|
||||
JSONField, SlugField, StaticSelect2, BOOLEAN_WITH_BLANK_CHOICES,
|
||||
)
|
||||
from virtualization.models import Cluster, ClusterGroup
|
||||
from .choices import *
|
||||
from .models import ConfigContext, CustomField, ImageAttachment, JournalEntry, ObjectChange, Tag
|
||||
from .utils import FeatureQuery
|
||||
|
||||
|
||||
#
|
||||
@@ -180,6 +181,11 @@ class TagFilterForm(BootstrapMixin, forms.Form):
|
||||
required=False,
|
||||
label=_('Search')
|
||||
)
|
||||
content_type_id = ContentTypeMultipleChoiceField(
|
||||
queryset=ContentType.objects.filter(FeatureQuery('tags').get_query()),
|
||||
required=False,
|
||||
label=_('Tagged object type')
|
||||
)
|
||||
|
||||
|
||||
class TagBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
|
||||
@@ -431,7 +431,9 @@ class JournalEntry(ChangeLoggedModel):
|
||||
verbose_name_plural = 'journal entries'
|
||||
|
||||
def __str__(self):
|
||||
return f"{date_format(self.created)} - {time_format(self.created)} ({self.get_kind_display()})"
|
||||
created_date = timezone.localdate(self.created)
|
||||
created_time = timezone.localtime(self.created)
|
||||
return f"{date_format(created_date)} - {time_format(created_time)} ({self.get_kind_display()})"
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('extras:journalentry', args=[self.pk])
|
||||
|
||||
@@ -42,7 +42,7 @@ class InstalledPluginsAPIView(APIView):
|
||||
'author': plugin_app_config.author,
|
||||
'author_email': plugin_app_config.author_email,
|
||||
'description': plugin_app_config.description,
|
||||
'verison': plugin_app_config.version
|
||||
'version': plugin_app_config.version
|
||||
}
|
||||
|
||||
def get(self, request, format=None):
|
||||
|
||||
@@ -3,7 +3,7 @@ from django.core.exceptions import ValidationError
|
||||
from django.urls import reverse
|
||||
from rest_framework import status
|
||||
|
||||
from dcim.filters import SiteFilterSet
|
||||
from dcim.filtersets import SiteFilterSet
|
||||
from dcim.forms import SiteCSVForm
|
||||
from dcim.models import Site, Rack
|
||||
from extras.choices import *
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.test import TestCase
|
||||
|
||||
from circuits.models import Provider
|
||||
from dcim.models import DeviceRole, DeviceType, Manufacturer, Platform, Rack, Region, Site, SiteGroup
|
||||
from extras.choices import JournalEntryKindChoices, ObjectChangeActionChoices
|
||||
from extras.filters import *
|
||||
from extras.filtersets import *
|
||||
from extras.models import *
|
||||
from ipam.models import IPAddress
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from utilities.testing import BaseFilterSetTests, ChangeLoggedFilterSetTests
|
||||
from virtualization.models import Cluster, ClusterGroup, ClusterType
|
||||
|
||||
|
||||
class WebhookTestCase(TestCase):
|
||||
class WebhookTestCase(TestCase, BaseFilterSetTests):
|
||||
queryset = Webhook.objects.all()
|
||||
filterset = WebhookFilterSet
|
||||
|
||||
@@ -52,10 +55,6 @@ class WebhookTestCase(TestCase):
|
||||
webhooks[1].content_types.add(content_types[1])
|
||||
webhooks[2].content_types.add(content_types[2])
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Webhook 1', 'Webhook 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -89,7 +88,7 @@ class WebhookTestCase(TestCase):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
class CustomLinkTestCase(TestCase):
|
||||
class CustomLinkTestCase(TestCase, BaseFilterSetTests):
|
||||
queryset = CustomLink.objects.all()
|
||||
filterset = CustomLinkFilterSet
|
||||
|
||||
@@ -125,10 +124,6 @@ class CustomLinkTestCase(TestCase):
|
||||
)
|
||||
CustomLink.objects.bulk_create(custom_links)
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Custom Link 1', 'Custom Link 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -148,7 +143,7 @@ class CustomLinkTestCase(TestCase):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
|
||||
class ExportTemplateTestCase(TestCase):
|
||||
class ExportTemplateTestCase(TestCase, BaseFilterSetTests):
|
||||
queryset = ExportTemplate.objects.all()
|
||||
filterset = ExportTemplateFilterSet
|
||||
|
||||
@@ -164,10 +159,6 @@ class ExportTemplateTestCase(TestCase):
|
||||
)
|
||||
ExportTemplate.objects.bulk_create(export_templates)
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Export Template 1', 'Export Template 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -177,7 +168,7 @@ class ExportTemplateTestCase(TestCase):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
|
||||
class ImageAttachmentTestCase(TestCase):
|
||||
class ImageAttachmentTestCase(TestCase, BaseFilterSetTests):
|
||||
queryset = ImageAttachment.objects.all()
|
||||
filterset = ImageAttachmentFilterSet
|
||||
|
||||
@@ -235,10 +226,6 @@ class ImageAttachmentTestCase(TestCase):
|
||||
)
|
||||
ImageAttachment.objects.bulk_create(image_attachments)
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Image Attachment 1', 'Image Attachment 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -254,8 +241,14 @@ class ImageAttachmentTestCase(TestCase):
|
||||
}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
def test_created(self):
|
||||
pk_list = self.queryset.values_list('pk', flat=True)[:2]
|
||||
self.queryset.filter(pk__in=pk_list).update(created=datetime(2021, 1, 1, 0, 0, 0, tzinfo=timezone.utc))
|
||||
params = {'created': '2021-01-01T00:00:00'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
class JournalEntryTestCase(TestCase):
|
||||
|
||||
class JournalEntryTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = JournalEntry.objects.all()
|
||||
filterset = JournalEntryFilterSet
|
||||
|
||||
@@ -320,10 +313,6 @@ class JournalEntryTestCase(TestCase):
|
||||
)
|
||||
JournalEntry.objects.bulk_create(journal_entries)
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_created_by(self):
|
||||
users = User.objects.filter(username__in=['Alice', 'Bob'])
|
||||
params = {'created_by': [users[0].username, users[1].username]}
|
||||
@@ -348,8 +337,17 @@ class JournalEntryTestCase(TestCase):
|
||||
params = {'kind': [JournalEntryKindChoices.KIND_INFO, JournalEntryKindChoices.KIND_SUCCESS]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
def test_created(self):
|
||||
pk_list = self.queryset.values_list('pk', flat=True)[:2]
|
||||
self.queryset.filter(pk__in=pk_list).update(created=datetime(2021, 1, 1, 0, 0, 0, tzinfo=timezone.utc))
|
||||
params = {
|
||||
'created_after': '2020-12-31T00:00:00',
|
||||
'created_before': '2021-01-02T00:00:00',
|
||||
}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
class ConfigContextTestCase(TestCase):
|
||||
|
||||
class ConfigContextTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = ConfigContext.objects.all()
|
||||
filterset = ConfigContextFilterSet
|
||||
|
||||
@@ -449,10 +447,6 @@ class ConfigContextTestCase(TestCase):
|
||||
c.tenant_groups.set([tenant_groups[i]])
|
||||
c.tenants.set([tenants[i]])
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Config Context 1', 'Config Context 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -530,7 +524,7 @@ class ConfigContextTestCase(TestCase):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
class TagTestCase(TestCase):
|
||||
class TagTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = Tag.objects.all()
|
||||
filterset = TagFilterSet
|
||||
|
||||
@@ -544,9 +538,12 @@ class TagTestCase(TestCase):
|
||||
)
|
||||
Tag.objects.bulk_create(tags)
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
# Apply some tags so we can filter by content type
|
||||
site = Site.objects.create(name='Site 1', slug='site-1')
|
||||
provider = Provider.objects.create(name='Provider 1', slug='provider-1')
|
||||
|
||||
site.tags.set(tags[0])
|
||||
provider.tags.set(tags[1])
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Tag 1', 'Tag 2']}
|
||||
@@ -560,8 +557,16 @@ class TagTestCase(TestCase):
|
||||
params = {'color': ['ff0000', '00ff00']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_content_type(self):
|
||||
params = {'content_type': ['dcim.site', 'circuits.provider']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
site_ct = ContentType.objects.get_for_model(Site).pk
|
||||
provider_ct = ContentType.objects.get_for_model(Provider).pk
|
||||
params = {'content_type_id': [site_ct, provider_ct]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
class ObjectChangeTestCase(TestCase):
|
||||
|
||||
class ObjectChangeTestCase(TestCase, BaseFilterSetTests):
|
||||
queryset = ObjectChange.objects.all()
|
||||
filterset = ObjectChangeFilterSet
|
||||
|
||||
@@ -635,10 +640,6 @@ class ObjectChangeTestCase(TestCase):
|
||||
)
|
||||
ObjectChange.objects.bulk_create(object_changes)
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:3]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
|
||||
def test_user(self):
|
||||
params = {'user_id': User.objects.filter(username__in=['user1', 'user2']).values_list('pk', flat=True)}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
@@ -13,7 +13,7 @@ from utilities.forms import ConfirmationForm
|
||||
from utilities.tables import paginate_table
|
||||
from utilities.utils import copy_safe_request, count_related, shallow_compare_dict
|
||||
from utilities.views import ContentTypePermissionRequiredMixin
|
||||
from . import filters, forms, tables
|
||||
from . import filtersets, forms, tables
|
||||
from .choices import JobResultStatusChoices
|
||||
from .models import ConfigContext, ImageAttachment, JournalEntry, ObjectChange, JobResult, Tag, TaggedItem
|
||||
from .reports import get_report, get_reports, run_report
|
||||
@@ -28,7 +28,7 @@ class TagListView(generic.ObjectListView):
|
||||
queryset = Tag.objects.annotate(
|
||||
items=count_related(TaggedItem, 'tag')
|
||||
)
|
||||
filterset = filters.TagFilterSet
|
||||
filterset = filtersets.TagFilterSet
|
||||
filterset_form = forms.TagFilterForm
|
||||
table = tables.TagTable
|
||||
|
||||
@@ -94,7 +94,7 @@ class TagBulkDeleteView(generic.BulkDeleteView):
|
||||
|
||||
class ConfigContextListView(generic.ObjectListView):
|
||||
queryset = ConfigContext.objects.all()
|
||||
filterset = filters.ConfigContextFilterSet
|
||||
filterset = filtersets.ConfigContextFilterSet
|
||||
filterset_form = forms.ConfigContextFilterForm
|
||||
table = tables.ConfigContextTable
|
||||
action_buttons = ('add',)
|
||||
@@ -127,7 +127,7 @@ class ConfigContextEditView(generic.ObjectEditView):
|
||||
|
||||
class ConfigContextBulkEditView(generic.BulkEditView):
|
||||
queryset = ConfigContext.objects.all()
|
||||
filterset = filters.ConfigContextFilterSet
|
||||
filterset = filtersets.ConfigContextFilterSet
|
||||
table = tables.ConfigContextTable
|
||||
form = forms.ConfigContextBulkEditForm
|
||||
|
||||
@@ -173,7 +173,7 @@ class ObjectConfigContextView(generic.ObjectView):
|
||||
|
||||
class ObjectChangeListView(generic.ObjectListView):
|
||||
queryset = ObjectChange.objects.all()
|
||||
filterset = filters.ObjectChangeFilterSet
|
||||
filterset = filtersets.ObjectChangeFilterSet
|
||||
filterset_form = forms.ObjectChangeFilterForm
|
||||
table = tables.ObjectChangeTable
|
||||
template_name = 'extras/objectchange_list.html'
|
||||
@@ -300,7 +300,7 @@ class ImageAttachmentDeleteView(generic.ObjectDeleteView):
|
||||
|
||||
class JournalEntryListView(generic.ObjectListView):
|
||||
queryset = JournalEntry.objects.all()
|
||||
filterset = filters.JournalEntryFilterSet
|
||||
filterset = filtersets.JournalEntryFilterSet
|
||||
filterset_form = forms.JournalEntryFilterForm
|
||||
table = tables.JournalEntryTable
|
||||
action_buttons = ('export',)
|
||||
@@ -338,14 +338,14 @@ class JournalEntryDeleteView(generic.ObjectDeleteView):
|
||||
|
||||
class JournalEntryBulkEditView(generic.BulkEditView):
|
||||
queryset = JournalEntry.objects.prefetch_related('created_by')
|
||||
filterset = filters.JournalEntryFilterSet
|
||||
filterset = filtersets.JournalEntryFilterSet
|
||||
table = tables.JournalEntryTable
|
||||
form = forms.JournalEntryBulkEditForm
|
||||
|
||||
|
||||
class JournalEntryBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = JournalEntry.objects.prefetch_related('created_by')
|
||||
filterset = filters.JournalEntryFilterSet
|
||||
filterset = filtersets.JournalEntryFilterSet
|
||||
table = tables.JournalEntryTable
|
||||
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ from rest_framework.validators import UniqueTogetherValidator
|
||||
|
||||
from dcim.api.nested_serializers import NestedDeviceSerializer, NestedSiteSerializer
|
||||
from ipam.choices import *
|
||||
from ipam.constants import IPADDRESS_ASSIGNMENT_MODELS
|
||||
from ipam.constants import IPADDRESS_ASSIGNMENT_MODELS, VLANGROUP_SCOPE_TYPES
|
||||
from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, RouteTarget, Service, VLAN, VLANGroup, VRF
|
||||
from netbox.api import ChoiceField, ContentTypeField, SerializedPKRelatedField
|
||||
from netbox.api.serializers import OrganizationalModelSerializer
|
||||
@@ -116,8 +116,7 @@ class VLANGroupSerializer(OrganizationalModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlangroup-detail')
|
||||
scope_type = ContentTypeField(
|
||||
queryset=ContentType.objects.filter(
|
||||
app_label='dcim',
|
||||
model__in=['region', 'sitegroup', 'site', 'location', 'rack']
|
||||
model__in=VLANGROUP_SCOPE_TYPES
|
||||
),
|
||||
required=False
|
||||
)
|
||||
|
||||
@@ -10,7 +10,7 @@ from rest_framework.response import Response
|
||||
from rest_framework.routers import APIRootView
|
||||
|
||||
from extras.api.views import CustomFieldModelViewSet
|
||||
from ipam import filters
|
||||
from ipam import filtersets
|
||||
from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, RouteTarget, Service, VLAN, VLANGroup, VRF
|
||||
from netbox.api.views import ModelViewSet
|
||||
from utilities.constants import ADVISORY_LOCK_KEYS
|
||||
@@ -38,7 +38,7 @@ class VRFViewSet(CustomFieldModelViewSet):
|
||||
prefix_count=count_related(Prefix, 'vrf')
|
||||
)
|
||||
serializer_class = serializers.VRFSerializer
|
||||
filterset_class = filters.VRFFilterSet
|
||||
filterset_class = filtersets.VRFFilterSet
|
||||
|
||||
|
||||
#
|
||||
@@ -48,7 +48,7 @@ class VRFViewSet(CustomFieldModelViewSet):
|
||||
class RouteTargetViewSet(CustomFieldModelViewSet):
|
||||
queryset = RouteTarget.objects.prefetch_related('tenant').prefetch_related('tags')
|
||||
serializer_class = serializers.RouteTargetSerializer
|
||||
filterset_class = filters.RouteTargetFilterSet
|
||||
filterset_class = filtersets.RouteTargetFilterSet
|
||||
|
||||
|
||||
#
|
||||
@@ -60,7 +60,7 @@ class RIRViewSet(CustomFieldModelViewSet):
|
||||
aggregate_count=count_related(Aggregate, 'rir')
|
||||
)
|
||||
serializer_class = serializers.RIRSerializer
|
||||
filterset_class = filters.RIRFilterSet
|
||||
filterset_class = filtersets.RIRFilterSet
|
||||
|
||||
|
||||
#
|
||||
@@ -70,7 +70,7 @@ class RIRViewSet(CustomFieldModelViewSet):
|
||||
class AggregateViewSet(CustomFieldModelViewSet):
|
||||
queryset = Aggregate.objects.prefetch_related('rir').prefetch_related('tags')
|
||||
serializer_class = serializers.AggregateSerializer
|
||||
filterset_class = filters.AggregateFilterSet
|
||||
filterset_class = filtersets.AggregateFilterSet
|
||||
|
||||
|
||||
#
|
||||
@@ -83,7 +83,7 @@ class RoleViewSet(CustomFieldModelViewSet):
|
||||
vlan_count=count_related(VLAN, 'role')
|
||||
)
|
||||
serializer_class = serializers.RoleSerializer
|
||||
filterset_class = filters.RoleFilterSet
|
||||
filterset_class = filtersets.RoleFilterSet
|
||||
|
||||
|
||||
#
|
||||
@@ -95,7 +95,7 @@ class PrefixViewSet(CustomFieldModelViewSet):
|
||||
'site', 'vrf__tenant', 'tenant', 'vlan', 'role', 'tags'
|
||||
)
|
||||
serializer_class = serializers.PrefixSerializer
|
||||
filterset_class = filters.PrefixFilterSet
|
||||
filterset_class = filtersets.PrefixFilterSet
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.action == "available_prefixes" and self.request.method == "POST":
|
||||
@@ -275,7 +275,7 @@ class IPAddressViewSet(CustomFieldModelViewSet):
|
||||
'vrf__tenant', 'tenant', 'nat_inside', 'nat_outside', 'tags', 'assigned_object'
|
||||
)
|
||||
serializer_class = serializers.IPAddressSerializer
|
||||
filterset_class = filters.IPAddressFilterSet
|
||||
filterset_class = filtersets.IPAddressFilterSet
|
||||
|
||||
|
||||
#
|
||||
@@ -287,7 +287,7 @@ class VLANGroupViewSet(CustomFieldModelViewSet):
|
||||
vlan_count=count_related(VLAN, 'group')
|
||||
)
|
||||
serializer_class = serializers.VLANGroupSerializer
|
||||
filterset_class = filters.VLANGroupFilterSet
|
||||
filterset_class = filtersets.VLANGroupFilterSet
|
||||
|
||||
|
||||
#
|
||||
@@ -301,7 +301,7 @@ class VLANViewSet(CustomFieldModelViewSet):
|
||||
prefix_count=count_related(Prefix, 'vlan')
|
||||
)
|
||||
serializer_class = serializers.VLANSerializer
|
||||
filterset_class = filters.VLANFilterSet
|
||||
filterset_class = filtersets.VLANFilterSet
|
||||
|
||||
|
||||
#
|
||||
@@ -313,4 +313,4 @@ class ServiceViewSet(ModelViewSet):
|
||||
'device', 'virtual_machine', 'tags', 'ipaddresses'
|
||||
)
|
||||
serializer_class = serializers.ServiceSerializer
|
||||
filterset_class = filters.ServiceFilterSet
|
||||
filterset_class = filtersets.ServiceFilterSet
|
||||
|
||||
@@ -6,11 +6,11 @@ from django.db.models import Q
|
||||
from netaddr.core import AddrFormatError
|
||||
|
||||
from dcim.models import Device, Interface, Region, Site, SiteGroup
|
||||
from extras.filters import CustomFieldModelFilterSet, CreatedUpdatedFilterSet
|
||||
from tenancy.filters import TenancyFilterSet
|
||||
from extras.filters import TagFilter
|
||||
from netbox.filtersets import OrganizationalModelFilterSet, PrimaryModelFilterSet
|
||||
from tenancy.filtersets import TenancyFilterSet
|
||||
from utilities.filters import (
|
||||
BaseFilterSet, ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter, NameSlugSearchFilterSet,
|
||||
NumericArrayFilter, TagFilter, TreeNodeMultipleChoiceFilter,
|
||||
ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter, NumericArrayFilter, TreeNodeMultipleChoiceFilter,
|
||||
)
|
||||
from virtualization.models import VirtualMachine, VMInterface
|
||||
from .choices import *
|
||||
@@ -31,7 +31,7 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
class VRFFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
|
||||
class VRFFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
@@ -74,7 +74,7 @@ class VRFFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet, C
|
||||
fields = ['id', 'name', 'rd', 'enforce_unique']
|
||||
|
||||
|
||||
class RouteTargetFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
|
||||
class RouteTargetFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
@@ -116,14 +116,14 @@ class RouteTargetFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilt
|
||||
fields = ['id', 'name']
|
||||
|
||||
|
||||
class RIRFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
|
||||
class RIRFilterSet(OrganizationalModelFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = RIR
|
||||
fields = ['id', 'name', 'slug', 'is_private', 'description']
|
||||
|
||||
|
||||
class AggregateFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
|
||||
class AggregateFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
@@ -173,7 +173,7 @@ class AggregateFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilter
|
||||
return queryset.none()
|
||||
|
||||
|
||||
class RoleFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
|
||||
class RoleFilterSet(OrganizationalModelFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
@@ -184,7 +184,7 @@ class RoleFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
|
||||
fields = ['id', 'name', 'slug']
|
||||
|
||||
|
||||
class PrefixFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
|
||||
class PrefixFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
@@ -369,7 +369,7 @@ class PrefixFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet
|
||||
)
|
||||
|
||||
|
||||
class IPAddressFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
|
||||
class IPAddressFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
@@ -468,7 +468,7 @@ class IPAddressFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilter
|
||||
|
||||
class Meta:
|
||||
model = IPAddress
|
||||
fields = ['id', 'dns_name']
|
||||
fields = ['id', 'dns_name', 'description']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
@@ -515,7 +515,7 @@ class IPAddressFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilter
|
||||
return queryset.none()
|
||||
interface_ids = []
|
||||
for device in devices:
|
||||
interface_ids.extend(device.vc_interfaces.values_list('id', flat=True))
|
||||
interface_ids.extend(device.vc_interfaces().values_list('id', flat=True))
|
||||
return queryset.filter(
|
||||
interface__in=interface_ids
|
||||
)
|
||||
@@ -535,7 +535,11 @@ class IPAddressFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilter
|
||||
return queryset.exclude(assigned_object_id__isnull=value)
|
||||
|
||||
|
||||
class VLANGroupFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
|
||||
class VLANGroupFilterSet(OrganizationalModelFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
scope_type = ContentTypeFilter()
|
||||
region = django_filters.NumberFilter(
|
||||
method='filter_scope'
|
||||
@@ -563,6 +567,15 @@ class VLANGroupFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
|
||||
model = VLANGroup
|
||||
fields = ['id', 'name', 'slug', 'description', 'scope_id']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
qs_filter = (
|
||||
Q(name__icontains=value) |
|
||||
Q(description__icontains=value)
|
||||
)
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
def filter_scope(self, queryset, name, value):
|
||||
return queryset.filter(
|
||||
scope_type=ContentType.objects.get(model=name),
|
||||
@@ -570,7 +583,7 @@ class VLANGroupFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
|
||||
)
|
||||
|
||||
|
||||
class VLANFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
|
||||
class VLANFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
@@ -666,7 +679,7 @@ class VLANFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet,
|
||||
return queryset.get_for_virtualmachine(value)
|
||||
|
||||
|
||||
class ServiceFilterSet(BaseFilterSet, CreatedUpdatedFilterSet):
|
||||
class ServiceFilterSet(PrimaryModelFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
@@ -1270,6 +1270,10 @@ class VLANGroupBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
||||
|
||||
|
||||
class VLANGroupFilterForm(BootstrapMixin, forms.Form):
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
label=_('Search')
|
||||
)
|
||||
region = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
@@ -1561,7 +1565,7 @@ class ServiceForm(BootstrapMixin, CustomFieldModelForm):
|
||||
# Limit IP address choices to those assigned to interfaces of the parent device/VM
|
||||
if self.instance.device:
|
||||
self.fields['ipaddresses'].queryset = IPAddress.objects.filter(
|
||||
interface__in=self.instance.device.vc_interfaces.values_list('id', flat=True)
|
||||
interface__in=self.instance.device.vc_interfaces().values_list('id', flat=True)
|
||||
)
|
||||
elif self.instance.virtual_machine:
|
||||
self.fields['ipaddresses'].queryset = IPAddress.objects.filter(
|
||||
|
||||
@@ -29,7 +29,7 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'export_templates', 'webhooks')
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
class RIR(OrganizationalModel):
|
||||
"""
|
||||
A Regional Internet Registry (RIR) is responsible for the allocation of a large portion of the global IP address
|
||||
@@ -77,7 +77,7 @@ class RIR(OrganizationalModel):
|
||||
)
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||
class Aggregate(PrimaryModel):
|
||||
"""
|
||||
An aggregate exists at the root level of the IP address space hierarchy in NetBox. Aggregates are used to organize
|
||||
@@ -184,7 +184,7 @@ class Aggregate(PrimaryModel):
|
||||
return int(float(child_prefixes.size) / self.prefix.size * 100)
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'export_templates', 'webhooks')
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
class Role(OrganizationalModel):
|
||||
"""
|
||||
A Role represents the functional role of a Prefix or VLAN; for example, "Customer," "Infrastructure," or
|
||||
@@ -228,7 +228,7 @@ class Role(OrganizationalModel):
|
||||
)
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||
class Prefix(PrimaryModel):
|
||||
"""
|
||||
A Prefix represents an IPv4 or IPv6 network, including mask length. Prefixes can optionally be assigned to Sites and
|
||||
@@ -426,19 +426,11 @@ class Prefix(PrimaryModel):
|
||||
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:
|
||||
# IPv6, pool, or IPv4 /31 sets are fully usable
|
||||
if self.family == 6 or self.is_pool or self.prefix.prefixlen == 31:
|
||||
return available_ips
|
||||
|
||||
# All IP addresses within a point-to-point prefix (IPv4 /31 or IPv6 /127) are considered usable
|
||||
if (
|
||||
self.prefix.version == 4 and self.prefix.prefixlen == 31 # RFC 3021
|
||||
) or (
|
||||
self.prefix.version == 6 and self.prefix.prefixlen == 127 # RFC 6164
|
||||
):
|
||||
return available_ips
|
||||
|
||||
# Omit first and last IP address from the available set
|
||||
# For "normal" IPv4 prefixes, omit first and last addresses
|
||||
available_ips -= netaddr.IPSet([
|
||||
netaddr.IPAddress(self.prefix.first),
|
||||
netaddr.IPAddress(self.prefix.last),
|
||||
@@ -485,7 +477,7 @@ class Prefix(PrimaryModel):
|
||||
return int(float(child_count) / prefix_size * 100)
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||
class IPAddress(PrimaryModel):
|
||||
"""
|
||||
An IPAddress represents an individual IPv4 or IPv6 address and its mask. The mask length should match what is
|
||||
|
||||
@@ -17,7 +17,7 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||
class Service(PrimaryModel):
|
||||
"""
|
||||
A Service represents a layer-four service (e.g. HTTP or SSH) running on a Device or VirtualMachine. A Service may
|
||||
|
||||
@@ -21,7 +21,7 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'export_templates', 'webhooks')
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
class VLANGroup(OrganizationalModel):
|
||||
"""
|
||||
A VLAN group is an arbitrary collection of VLANs within which VLAN IDs and names must be unique.
|
||||
@@ -100,7 +100,7 @@ class VLANGroup(OrganizationalModel):
|
||||
return None
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||
class VLAN(PrimaryModel):
|
||||
"""
|
||||
A VLAN is a distinct layer two forwarding domain identified by a 12-bit integer (1-4094). Each VLAN must be assigned
|
||||
|
||||
@@ -13,7 +13,7 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||
class VRF(PrimaryModel):
|
||||
"""
|
||||
A virtual routing and forwarding (VRF) table represents a discrete layer three forwarding domain (e.g. a routing
|
||||
@@ -92,7 +92,7 @@ class VRF(PrimaryModel):
|
||||
return self.name
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||
class RouteTarget(PrimaryModel):
|
||||
"""
|
||||
A BGP extended community used to control the redistribution of routes among VRFs, as defined in RFC 4364.
|
||||
|
||||
@@ -64,6 +64,7 @@ class VLANQuerySet(RestrictedQuerySet):
|
||||
return self.filter(
|
||||
Q(group__in=VLANGroup.objects.filter(q)) |
|
||||
Q(site=device.site) |
|
||||
Q(group__scope_id__isnull=True, site__isnull=True) | # Global group VLANs
|
||||
Q(group__isnull=True, site__isnull=True) # Global VLANs
|
||||
)
|
||||
|
||||
@@ -104,6 +105,7 @@ class VLANQuerySet(RestrictedQuerySet):
|
||||
# Return all applicable VLANs
|
||||
q = (
|
||||
Q(group__in=vlan_groups) |
|
||||
Q(group__scope_id__isnull=True, site__isnull=True) | # Global group VLANs
|
||||
Q(group__isnull=True, site__isnull=True) # Global VLANs
|
||||
)
|
||||
if vm.cluster.site:
|
||||
|
||||
@@ -430,7 +430,8 @@ class VLANGroupTable(BaseTable):
|
||||
name = tables.Column(linkify=True)
|
||||
scope_type = ContentTypeColumn()
|
||||
scope = tables.Column(
|
||||
linkify=True
|
||||
linkify=True,
|
||||
orderable=False
|
||||
)
|
||||
vlan_count = LinkedCountColumn(
|
||||
viewname='ipam:vlan_list',
|
||||
|
||||
@@ -2,13 +2,14 @@ from django.test import TestCase
|
||||
|
||||
from dcim.models import Device, DeviceRole, DeviceType, Interface, Location, Manufacturer, Rack, Region, Site, SiteGroup
|
||||
from ipam.choices import *
|
||||
from ipam.filters import *
|
||||
from ipam.filtersets import *
|
||||
from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, RouteTarget, Service, VLAN, VLANGroup, VRF
|
||||
from utilities.testing import ChangeLoggedFilterSetTests
|
||||
from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
|
||||
|
||||
class VRFTestCase(TestCase):
|
||||
class VRFTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = VRF.objects.all()
|
||||
filterset = VRFFilterSet
|
||||
|
||||
@@ -53,10 +54,6 @@ class VRFTestCase(TestCase):
|
||||
vrfs[2].import_targets.add(route_targets[2])
|
||||
vrfs[2].export_targets.add(route_targets[2])
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['VRF 1', 'VRF 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -100,7 +97,7 @@ class VRFTestCase(TestCase):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
|
||||
class RouteTargetTestCase(TestCase):
|
||||
class RouteTargetTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = RouteTarget.objects.all()
|
||||
filterset = RouteTargetFilterSet
|
||||
|
||||
@@ -149,10 +146,6 @@ class RouteTargetTestCase(TestCase):
|
||||
vrfs[1].import_targets.add(route_targets[4], route_targets[5])
|
||||
vrfs[1].export_targets.add(route_targets[6], route_targets[7])
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['65000:1001', '65000:1002', '65000:1003']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
@@ -186,7 +179,7 @@ class RouteTargetTestCase(TestCase):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 8)
|
||||
|
||||
|
||||
class RIRTestCase(TestCase):
|
||||
class RIRTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = RIR.objects.all()
|
||||
filterset = RIRFilterSet
|
||||
|
||||
@@ -203,10 +196,6 @@ class RIRTestCase(TestCase):
|
||||
)
|
||||
RIR.objects.bulk_create(rirs)
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['RIR 1', 'RIR 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -226,7 +215,7 @@ class RIRTestCase(TestCase):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
|
||||
|
||||
class AggregateTestCase(TestCase):
|
||||
class AggregateTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = Aggregate.objects.all()
|
||||
filterset = AggregateFilterSet
|
||||
|
||||
@@ -265,10 +254,6 @@ class AggregateTestCase(TestCase):
|
||||
)
|
||||
Aggregate.objects.bulk_create(aggregates)
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_family(self):
|
||||
params = {'family': '4'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
@@ -304,7 +289,7 @@ class AggregateTestCase(TestCase):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
|
||||
class RoleTestCase(TestCase):
|
||||
class RoleTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = Role.objects.all()
|
||||
filterset = RoleFilterSet
|
||||
|
||||
@@ -318,10 +303,6 @@ class RoleTestCase(TestCase):
|
||||
)
|
||||
Role.objects.bulk_create(roles)
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Role 1', 'Role 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -331,7 +312,7 @@ class RoleTestCase(TestCase):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
class PrefixTestCase(TestCase):
|
||||
class PrefixTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = Prefix.objects.all()
|
||||
filterset = PrefixFilterSet
|
||||
|
||||
@@ -421,10 +402,6 @@ class PrefixTestCase(TestCase):
|
||||
)
|
||||
Prefix.objects.bulk_create(prefixes)
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_family(self):
|
||||
params = {'family': '6'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
|
||||
@@ -528,7 +505,7 @@ class PrefixTestCase(TestCase):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
|
||||
class IPAddressTestCase(TestCase):
|
||||
class IPAddressTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = IPAddress.objects.all()
|
||||
filterset = IPAddressFilterSet
|
||||
|
||||
@@ -594,12 +571,12 @@ class IPAddressTestCase(TestCase):
|
||||
Tenant.objects.bulk_create(tenants)
|
||||
|
||||
ipaddresses = (
|
||||
IPAddress(address='10.0.0.1/24', tenant=None, vrf=None, assigned_object=None, status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-a'),
|
||||
IPAddress(address='10.0.0.1/24', tenant=None, vrf=None, assigned_object=None, status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-a', description='foobar1'),
|
||||
IPAddress(address='10.0.0.2/24', tenant=tenants[0], vrf=vrfs[0], assigned_object=interfaces[0], status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-b'),
|
||||
IPAddress(address='10.0.0.3/24', tenant=tenants[1], vrf=vrfs[1], assigned_object=interfaces[1], status=IPAddressStatusChoices.STATUS_RESERVED, role=IPAddressRoleChoices.ROLE_VIP, dns_name='ipaddress-c'),
|
||||
IPAddress(address='10.0.0.4/24', tenant=tenants[2], vrf=vrfs[2], assigned_object=interfaces[2], status=IPAddressStatusChoices.STATUS_DEPRECATED, role=IPAddressRoleChoices.ROLE_SECONDARY, dns_name='ipaddress-d'),
|
||||
IPAddress(address='10.0.0.1/25', tenant=None, vrf=None, assigned_object=None, status=IPAddressStatusChoices.STATUS_ACTIVE),
|
||||
IPAddress(address='2001:db8::1/64', tenant=None, vrf=None, assigned_object=None, status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-a'),
|
||||
IPAddress(address='2001:db8::1/64', tenant=None, vrf=None, assigned_object=None, status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-a', description='foobar2'),
|
||||
IPAddress(address='2001:db8::2/64', tenant=tenants[0], vrf=vrfs[0], assigned_object=vminterfaces[0], status=IPAddressStatusChoices.STATUS_ACTIVE, dns_name='ipaddress-b'),
|
||||
IPAddress(address='2001:db8::3/64', tenant=tenants[1], vrf=vrfs[1], assigned_object=vminterfaces[1], status=IPAddressStatusChoices.STATUS_RESERVED, role=IPAddressRoleChoices.ROLE_VIP, dns_name='ipaddress-c'),
|
||||
IPAddress(address='2001:db8::4/64', tenant=tenants[2], vrf=vrfs[2], assigned_object=vminterfaces[2], status=IPAddressStatusChoices.STATUS_DEPRECATED, role=IPAddressRoleChoices.ROLE_SECONDARY, dns_name='ipaddress-d'),
|
||||
@@ -607,10 +584,6 @@ class IPAddressTestCase(TestCase):
|
||||
)
|
||||
IPAddress.objects.bulk_create(ipaddresses)
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_family(self):
|
||||
params = {'family': '6'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
|
||||
@@ -619,6 +592,10 @@ class IPAddressTestCase(TestCase):
|
||||
params = {'dns_name': ['ipaddress-a', 'ipaddress-b']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
def test_description(self):
|
||||
params = {'description': ['foobar1', 'foobar2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_parent(self):
|
||||
params = {'parent': '10.0.0.0/24'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
|
||||
@@ -708,7 +685,7 @@ class IPAddressTestCase(TestCase):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
|
||||
class VLANGroupTestCase(TestCase):
|
||||
class VLANGroupTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = VLANGroup.objects.all()
|
||||
filterset = VLANGroupFilterSet
|
||||
|
||||
@@ -751,10 +728,6 @@ class VLANGroupTestCase(TestCase):
|
||||
)
|
||||
VLANGroup.objects.bulk_create(vlan_groups)
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['VLAN Group 1', 'VLAN Group 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -796,7 +769,7 @@ class VLANGroupTestCase(TestCase):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
|
||||
class VLANTestCase(TestCase):
|
||||
class VLANTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = VLAN.objects.all()
|
||||
filterset = VLANFilterSet
|
||||
|
||||
@@ -965,10 +938,6 @@ class VLANTestCase(TestCase):
|
||||
)
|
||||
VLAN.objects.bulk_create(vlans)
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['VLAN 101', 'VLAN 102']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -1041,7 +1010,7 @@ class VLANTestCase(TestCase):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6) # 5 scoped + 1 global
|
||||
|
||||
|
||||
class ServiceTestCase(TestCase):
|
||||
class ServiceTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = Service.objects.all()
|
||||
filterset = ServiceFilterSet
|
||||
|
||||
@@ -1080,10 +1049,6 @@ class ServiceTestCase(TestCase):
|
||||
)
|
||||
Service.objects.bulk_create(services)
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:3]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Service 1', 'Service 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -7,7 +7,7 @@ from netbox.views import generic
|
||||
from utilities.tables import paginate_table
|
||||
from utilities.utils import count_related
|
||||
from virtualization.models import VirtualMachine, VMInterface
|
||||
from . import filters, forms, tables
|
||||
from . import filtersets, forms, tables
|
||||
from .constants import *
|
||||
from .models import Aggregate, IPAddress, Prefix, RIR, Role, RouteTarget, Service, VLAN, VLANGroup, VRF
|
||||
from .utils import add_available_ipaddresses, add_available_prefixes, add_available_vlans
|
||||
@@ -19,7 +19,7 @@ from .utils import add_available_ipaddresses, add_available_prefixes, add_availa
|
||||
|
||||
class VRFListView(generic.ObjectListView):
|
||||
queryset = VRF.objects.all()
|
||||
filterset = filters.VRFFilterSet
|
||||
filterset = filtersets.VRFFilterSet
|
||||
filterset_form = forms.VRFFilterForm
|
||||
table = tables.VRFTable
|
||||
|
||||
@@ -65,14 +65,14 @@ class VRFBulkImportView(generic.BulkImportView):
|
||||
|
||||
class VRFBulkEditView(generic.BulkEditView):
|
||||
queryset = VRF.objects.prefetch_related('tenant')
|
||||
filterset = filters.VRFFilterSet
|
||||
filterset = filtersets.VRFFilterSet
|
||||
table = tables.VRFTable
|
||||
form = forms.VRFBulkEditForm
|
||||
|
||||
|
||||
class VRFBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = VRF.objects.prefetch_related('tenant')
|
||||
filterset = filters.VRFFilterSet
|
||||
filterset = filtersets.VRFFilterSet
|
||||
table = tables.VRFTable
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@ class VRFBulkDeleteView(generic.BulkDeleteView):
|
||||
|
||||
class RouteTargetListView(generic.ObjectListView):
|
||||
queryset = RouteTarget.objects.all()
|
||||
filterset = filters.RouteTargetFilterSet
|
||||
filterset = filtersets.RouteTargetFilterSet
|
||||
filterset_form = forms.RouteTargetFilterForm
|
||||
table = tables.RouteTargetTable
|
||||
|
||||
@@ -123,14 +123,14 @@ class RouteTargetBulkImportView(generic.BulkImportView):
|
||||
|
||||
class RouteTargetBulkEditView(generic.BulkEditView):
|
||||
queryset = RouteTarget.objects.prefetch_related('tenant')
|
||||
filterset = filters.RouteTargetFilterSet
|
||||
filterset = filtersets.RouteTargetFilterSet
|
||||
table = tables.RouteTargetTable
|
||||
form = forms.RouteTargetBulkEditForm
|
||||
|
||||
|
||||
class RouteTargetBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = RouteTarget.objects.prefetch_related('tenant')
|
||||
filterset = filters.RouteTargetFilterSet
|
||||
filterset = filtersets.RouteTargetFilterSet
|
||||
table = tables.RouteTargetTable
|
||||
|
||||
|
||||
@@ -142,7 +142,7 @@ class RIRListView(generic.ObjectListView):
|
||||
queryset = RIR.objects.annotate(
|
||||
aggregate_count=count_related(Aggregate, 'rir')
|
||||
)
|
||||
filterset = filters.RIRFilterSet
|
||||
filterset = filtersets.RIRFilterSet
|
||||
filterset_form = forms.RIRFilterForm
|
||||
table = tables.RIRTable
|
||||
template_name = 'ipam/rir_list.html'
|
||||
@@ -184,7 +184,7 @@ class RIRBulkEditView(generic.BulkEditView):
|
||||
queryset = RIR.objects.annotate(
|
||||
aggregate_count=count_related(Aggregate, 'rir')
|
||||
)
|
||||
filterset = filters.RIRFilterSet
|
||||
filterset = filtersets.RIRFilterSet
|
||||
table = tables.RIRTable
|
||||
form = forms.RIRBulkEditForm
|
||||
|
||||
@@ -193,7 +193,7 @@ class RIRBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = RIR.objects.annotate(
|
||||
aggregate_count=count_related(Aggregate, 'rir')
|
||||
)
|
||||
filterset = filters.RIRFilterSet
|
||||
filterset = filtersets.RIRFilterSet
|
||||
table = tables.RIRTable
|
||||
|
||||
|
||||
@@ -205,7 +205,7 @@ class AggregateListView(generic.ObjectListView):
|
||||
queryset = Aggregate.objects.annotate(
|
||||
child_count=RawSQL('SELECT COUNT(*) FROM ipam_prefix WHERE ipam_prefix.prefix <<= ipam_aggregate.prefix', ())
|
||||
)
|
||||
filterset = filters.AggregateFilterSet
|
||||
filterset = filtersets.AggregateFilterSet
|
||||
filterset_form = forms.AggregateFilterForm
|
||||
table = tables.AggregateDetailTable
|
||||
template_name = 'ipam/aggregate_list.html'
|
||||
@@ -280,14 +280,14 @@ class AggregateBulkImportView(generic.BulkImportView):
|
||||
|
||||
class AggregateBulkEditView(generic.BulkEditView):
|
||||
queryset = Aggregate.objects.prefetch_related('rir')
|
||||
filterset = filters.AggregateFilterSet
|
||||
filterset = filtersets.AggregateFilterSet
|
||||
table = tables.AggregateTable
|
||||
form = forms.AggregateBulkEditForm
|
||||
|
||||
|
||||
class AggregateBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = Aggregate.objects.prefetch_related('rir')
|
||||
filterset = filters.AggregateFilterSet
|
||||
filterset = filtersets.AggregateFilterSet
|
||||
table = tables.AggregateTable
|
||||
|
||||
|
||||
@@ -337,7 +337,7 @@ class RoleBulkImportView(generic.BulkImportView):
|
||||
|
||||
class RoleBulkEditView(generic.BulkEditView):
|
||||
queryset = Role.objects.all()
|
||||
filterset = filters.RoleFilterSet
|
||||
filterset = filtersets.RoleFilterSet
|
||||
table = tables.RoleTable
|
||||
form = forms.RoleBulkEditForm
|
||||
|
||||
@@ -353,7 +353,7 @@ class RoleBulkDeleteView(generic.BulkDeleteView):
|
||||
|
||||
class PrefixListView(generic.ObjectListView):
|
||||
queryset = Prefix.objects.annotate_tree()
|
||||
filterset = filters.PrefixFilterSet
|
||||
filterset = filtersets.PrefixFilterSet
|
||||
filterset_form = forms.PrefixFilterForm
|
||||
table = tables.PrefixDetailTable
|
||||
template_name = 'ipam/prefix_list.html'
|
||||
@@ -493,14 +493,14 @@ class PrefixBulkImportView(generic.BulkImportView):
|
||||
|
||||
class PrefixBulkEditView(generic.BulkEditView):
|
||||
queryset = Prefix.objects.prefetch_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role')
|
||||
filterset = filters.PrefixFilterSet
|
||||
filterset = filtersets.PrefixFilterSet
|
||||
table = tables.PrefixTable
|
||||
form = forms.PrefixBulkEditForm
|
||||
|
||||
|
||||
class PrefixBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = Prefix.objects.prefetch_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role')
|
||||
filterset = filters.PrefixFilterSet
|
||||
filterset = filtersets.PrefixFilterSet
|
||||
table = tables.PrefixTable
|
||||
|
||||
|
||||
@@ -510,7 +510,7 @@ class PrefixBulkDeleteView(generic.BulkDeleteView):
|
||||
|
||||
class IPAddressListView(generic.ObjectListView):
|
||||
queryset = IPAddress.objects.all()
|
||||
filterset = filters.IPAddressFilterSet
|
||||
filterset = filtersets.IPAddressFilterSet
|
||||
filterset_form = forms.IPAddressFilterForm
|
||||
table = tables.IPAddressDetailTable
|
||||
|
||||
@@ -613,7 +613,7 @@ class IPAddressAssignView(generic.ObjectView):
|
||||
|
||||
addresses = self.queryset.prefetch_related('vrf', 'tenant')
|
||||
# Limit to 100 results
|
||||
addresses = filters.IPAddressFilterSet(request.POST, addresses).qs[:100]
|
||||
addresses = filtersets.IPAddressFilterSet(request.POST, addresses).qs[:100]
|
||||
table = tables.IPAddressAssignTable(addresses)
|
||||
|
||||
return render(request, 'ipam/ipaddress_assign.html', {
|
||||
@@ -643,14 +643,14 @@ class IPAddressBulkImportView(generic.BulkImportView):
|
||||
|
||||
class IPAddressBulkEditView(generic.BulkEditView):
|
||||
queryset = IPAddress.objects.prefetch_related('vrf__tenant', 'tenant')
|
||||
filterset = filters.IPAddressFilterSet
|
||||
filterset = filtersets.IPAddressFilterSet
|
||||
table = tables.IPAddressTable
|
||||
form = forms.IPAddressBulkEditForm
|
||||
|
||||
|
||||
class IPAddressBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = IPAddress.objects.prefetch_related('vrf__tenant', 'tenant')
|
||||
filterset = filters.IPAddressFilterSet
|
||||
filterset = filtersets.IPAddressFilterSet
|
||||
table = tables.IPAddressTable
|
||||
|
||||
|
||||
@@ -662,7 +662,7 @@ class VLANGroupListView(generic.ObjectListView):
|
||||
queryset = VLANGroup.objects.annotate(
|
||||
vlan_count=count_related(VLAN, 'group')
|
||||
)
|
||||
filterset = filters.VLANGroupFilterSet
|
||||
filterset = filtersets.VLANGroupFilterSet
|
||||
filterset_form = forms.VLANGroupFilterForm
|
||||
table = tables.VLANGroupTable
|
||||
|
||||
@@ -673,7 +673,7 @@ class VLANGroupView(generic.ObjectView):
|
||||
def get_extra_context(self, request, instance):
|
||||
vlans = VLAN.objects.restrict(request.user, 'view').filter(group=instance).prefetch_related(
|
||||
Prefetch('prefixes', queryset=Prefix.objects.restrict(request.user))
|
||||
)
|
||||
).order_by('vid')
|
||||
vlans_count = vlans.count()
|
||||
vlans = add_available_vlans(instance, vlans)
|
||||
|
||||
@@ -684,9 +684,17 @@ class VLANGroupView(generic.ObjectView):
|
||||
vlans_table.columns.hide('group')
|
||||
paginate_table(vlans_table, request)
|
||||
|
||||
# Compile permissions list for rendering the object table
|
||||
permissions = {
|
||||
'add': request.user.has_perm('ipam.add_vlan'),
|
||||
'change': request.user.has_perm('ipam.change_vlan'),
|
||||
'delete': request.user.has_perm('ipam.delete_vlan'),
|
||||
}
|
||||
|
||||
return {
|
||||
'vlans_count': vlans_count,
|
||||
'vlans_table': vlans_table,
|
||||
'permissions': permissions,
|
||||
}
|
||||
|
||||
|
||||
@@ -710,7 +718,7 @@ class VLANGroupBulkEditView(generic.BulkEditView):
|
||||
queryset = VLANGroup.objects.annotate(
|
||||
vlan_count=count_related(VLAN, 'group')
|
||||
)
|
||||
filterset = filters.VLANGroupFilterSet
|
||||
filterset = filtersets.VLANGroupFilterSet
|
||||
table = tables.VLANGroupTable
|
||||
form = forms.VLANGroupBulkEditForm
|
||||
|
||||
@@ -719,7 +727,7 @@ class VLANGroupBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = VLANGroup.objects.annotate(
|
||||
vlan_count=count_related(VLAN, 'group')
|
||||
)
|
||||
filterset = filters.VLANGroupFilterSet
|
||||
filterset = filtersets.VLANGroupFilterSet
|
||||
table = tables.VLANGroupTable
|
||||
|
||||
|
||||
@@ -729,7 +737,7 @@ class VLANGroupBulkDeleteView(generic.BulkDeleteView):
|
||||
|
||||
class VLANListView(generic.ObjectListView):
|
||||
queryset = VLAN.objects.all()
|
||||
filterset = filters.VLANFilterSet
|
||||
filterset = filtersets.VLANFilterSet
|
||||
filterset_form = forms.VLANFilterForm
|
||||
table = tables.VLANDetailTable
|
||||
|
||||
@@ -797,14 +805,14 @@ class VLANBulkImportView(generic.BulkImportView):
|
||||
|
||||
class VLANBulkEditView(generic.BulkEditView):
|
||||
queryset = VLAN.objects.prefetch_related('site', 'group', 'tenant', 'role')
|
||||
filterset = filters.VLANFilterSet
|
||||
filterset = filtersets.VLANFilterSet
|
||||
table = tables.VLANTable
|
||||
form = forms.VLANBulkEditForm
|
||||
|
||||
|
||||
class VLANBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = VLAN.objects.prefetch_related('site', 'group', 'tenant', 'role')
|
||||
filterset = filters.VLANFilterSet
|
||||
filterset = filtersets.VLANFilterSet
|
||||
table = tables.VLANTable
|
||||
|
||||
|
||||
@@ -814,7 +822,7 @@ class VLANBulkDeleteView(generic.BulkDeleteView):
|
||||
|
||||
class ServiceListView(generic.ObjectListView):
|
||||
queryset = Service.objects.all()
|
||||
filterset = filters.ServiceFilterSet
|
||||
filterset = filtersets.ServiceFilterSet
|
||||
filterset_form = forms.ServiceFilterForm
|
||||
table = tables.ServiceTable
|
||||
action_buttons = ('import', 'export')
|
||||
@@ -855,12 +863,12 @@ class ServiceDeleteView(generic.ObjectDeleteView):
|
||||
|
||||
class ServiceBulkEditView(generic.BulkEditView):
|
||||
queryset = Service.objects.prefetch_related('device', 'virtual_machine')
|
||||
filterset = filters.ServiceFilterSet
|
||||
filterset = filtersets.ServiceFilterSet
|
||||
table = tables.ServiceTable
|
||||
form = forms.ServiceBulkEditForm
|
||||
|
||||
|
||||
class ServiceBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = Service.objects.prefetch_related('device', 'virtual_machine')
|
||||
filterset = filters.ServiceFilterSet
|
||||
filterset = filtersets.ServiceFilterSet
|
||||
table = tables.ServiceTable
|
||||
|
||||
@@ -246,6 +246,9 @@ RQ_DEFAULT_TIMEOUT = 300
|
||||
# this setting is derived from the installed location.
|
||||
# SCRIPTS_ROOT = '/opt/netbox/netbox/scripts'
|
||||
|
||||
# The name to use for the session cookie.
|
||||
SESSION_COOKIE_NAME = 'sessionid'
|
||||
|
||||
# By default, NetBox will store session data in the database. Alternatively, a file path can be specified here to use
|
||||
# local file storage instead. (This can be useful for enabling authentication on a standby instance with read-only
|
||||
# database access.) Note that the user as which NetBox runs must have read and write permissions to this path.
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
from collections import OrderedDict
|
||||
|
||||
from circuits.filters import CircuitFilterSet, ProviderFilterSet, ProviderNetworkFilterSet
|
||||
from circuits.filtersets import CircuitFilterSet, ProviderFilterSet, ProviderNetworkFilterSet
|
||||
from circuits.models import Circuit, ProviderNetwork, Provider
|
||||
from circuits.tables import CircuitTable, ProviderNetworkTable, ProviderTable
|
||||
from dcim.filters import (
|
||||
from dcim.filtersets import (
|
||||
CableFilterSet, DeviceFilterSet, DeviceTypeFilterSet, PowerFeedFilterSet, RackFilterSet, LocationFilterSet,
|
||||
SiteFilterSet, VirtualChassisFilterSet,
|
||||
)
|
||||
@@ -12,17 +12,17 @@ from dcim.tables import (
|
||||
CableTable, DeviceTable, DeviceTypeTable, PowerFeedTable, RackTable, LocationTable, SiteTable,
|
||||
VirtualChassisTable,
|
||||
)
|
||||
from ipam.filters import AggregateFilterSet, IPAddressFilterSet, PrefixFilterSet, VLANFilterSet, VRFFilterSet
|
||||
from ipam.filtersets import AggregateFilterSet, IPAddressFilterSet, PrefixFilterSet, VLANFilterSet, VRFFilterSet
|
||||
from ipam.models import Aggregate, IPAddress, Prefix, VLAN, VRF
|
||||
from ipam.tables import AggregateTable, IPAddressTable, PrefixTable, VLANTable, VRFTable
|
||||
from secrets.filters import SecretFilterSet
|
||||
from secrets.filtersets import SecretFilterSet
|
||||
from secrets.models import Secret
|
||||
from secrets.tables import SecretTable
|
||||
from tenancy.filters import TenantFilterSet
|
||||
from tenancy.filtersets import TenantFilterSet
|
||||
from tenancy.models import Tenant
|
||||
from tenancy.tables import TenantTable
|
||||
from utilities.utils import count_related
|
||||
from virtualization.filters import ClusterFilterSet, VirtualMachineFilterSet
|
||||
from virtualization.filtersets import ClusterFilterSet, VirtualMachineFilterSet
|
||||
from virtualization.models import Cluster, VirtualMachine
|
||||
from virtualization.tables import ClusterTable, VirtualMachineDetailTable
|
||||
|
||||
|
||||
238
netbox/netbox/filtersets.py
Normal file
238
netbox/netbox/filtersets.py
Normal file
@@ -0,0 +1,238 @@
|
||||
import django_filters
|
||||
from copy import deepcopy
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db import models
|
||||
from django_filters.utils import get_model_field, resolve_field
|
||||
|
||||
from dcim.forms import MACAddressField
|
||||
from extras.choices import CustomFieldFilterLogicChoices
|
||||
from extras.filters import CustomFieldFilter, TagFilter
|
||||
from extras.models import CustomField
|
||||
from utilities.constants import (
|
||||
FILTER_CHAR_BASED_LOOKUP_MAP, FILTER_NEGATION_LOOKUP_MAP, FILTER_TREENODE_NEGATION_LOOKUP_MAP,
|
||||
FILTER_NUMERIC_BASED_LOOKUP_MAP
|
||||
)
|
||||
from utilities import filters
|
||||
|
||||
|
||||
__all__ = (
|
||||
'BaseFilterSet',
|
||||
'ChangeLoggedModelFilterSet',
|
||||
'OrganizationalModelFilterSet',
|
||||
'PrimaryModelFilterSet',
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# FilterSets
|
||||
#
|
||||
|
||||
class BaseFilterSet(django_filters.FilterSet):
|
||||
"""
|
||||
A base FilterSet which provides common functionality to all NetBox FilterSets
|
||||
"""
|
||||
FILTER_DEFAULTS = deepcopy(django_filters.filterset.FILTER_FOR_DBFIELD_DEFAULTS)
|
||||
FILTER_DEFAULTS.update({
|
||||
models.AutoField: {
|
||||
'filter_class': filters.MultiValueNumberFilter
|
||||
},
|
||||
models.CharField: {
|
||||
'filter_class': filters.MultiValueCharFilter
|
||||
},
|
||||
models.DateField: {
|
||||
'filter_class': filters.MultiValueDateFilter
|
||||
},
|
||||
models.DateTimeField: {
|
||||
'filter_class': filters.MultiValueDateTimeFilter
|
||||
},
|
||||
models.DecimalField: {
|
||||
'filter_class': filters.MultiValueNumberFilter
|
||||
},
|
||||
models.EmailField: {
|
||||
'filter_class': filters.MultiValueCharFilter
|
||||
},
|
||||
models.FloatField: {
|
||||
'filter_class': filters.MultiValueNumberFilter
|
||||
},
|
||||
models.IntegerField: {
|
||||
'filter_class': filters.MultiValueNumberFilter
|
||||
},
|
||||
models.PositiveIntegerField: {
|
||||
'filter_class': filters.MultiValueNumberFilter
|
||||
},
|
||||
models.PositiveSmallIntegerField: {
|
||||
'filter_class': filters.MultiValueNumberFilter
|
||||
},
|
||||
models.SlugField: {
|
||||
'filter_class': filters.MultiValueCharFilter
|
||||
},
|
||||
models.SmallIntegerField: {
|
||||
'filter_class': filters.MultiValueNumberFilter
|
||||
},
|
||||
models.TimeField: {
|
||||
'filter_class': filters.MultiValueTimeFilter
|
||||
},
|
||||
models.URLField: {
|
||||
'filter_class': filters.MultiValueCharFilter
|
||||
},
|
||||
MACAddressField: {
|
||||
'filter_class': filters.MultiValueMACAddressFilter
|
||||
},
|
||||
})
|
||||
|
||||
@staticmethod
|
||||
def _get_filter_lookup_dict(existing_filter):
|
||||
# Choose the lookup expression map based on the filter type
|
||||
if isinstance(existing_filter, (
|
||||
filters.MultiValueDateFilter,
|
||||
filters.MultiValueDateTimeFilter,
|
||||
filters.MultiValueNumberFilter,
|
||||
filters.MultiValueTimeFilter
|
||||
)):
|
||||
lookup_map = FILTER_NUMERIC_BASED_LOOKUP_MAP
|
||||
|
||||
elif isinstance(existing_filter, (
|
||||
filters.TreeNodeMultipleChoiceFilter,
|
||||
)):
|
||||
# TreeNodeMultipleChoiceFilter only support negation but must maintain the `in` lookup expression
|
||||
lookup_map = FILTER_TREENODE_NEGATION_LOOKUP_MAP
|
||||
|
||||
elif isinstance(existing_filter, (
|
||||
django_filters.ModelChoiceFilter,
|
||||
django_filters.ModelMultipleChoiceFilter,
|
||||
TagFilter
|
||||
)) or existing_filter.extra.get('choices'):
|
||||
# These filter types support only negation
|
||||
lookup_map = FILTER_NEGATION_LOOKUP_MAP
|
||||
|
||||
elif isinstance(existing_filter, (
|
||||
django_filters.filters.CharFilter,
|
||||
django_filters.MultipleChoiceFilter,
|
||||
filters.MultiValueCharFilter,
|
||||
filters.MultiValueMACAddressFilter
|
||||
)):
|
||||
lookup_map = FILTER_CHAR_BASED_LOOKUP_MAP
|
||||
|
||||
else:
|
||||
lookup_map = None
|
||||
|
||||
return lookup_map
|
||||
|
||||
@classmethod
|
||||
def get_filters(cls):
|
||||
"""
|
||||
Override filter generation to support dynamic lookup expressions for certain filter types.
|
||||
|
||||
For specific filter types, new filters are created based on defined lookup expressions in
|
||||
the form `<field_name>__<lookup_expr>`
|
||||
"""
|
||||
filters = super().get_filters()
|
||||
|
||||
new_filters = {}
|
||||
for existing_filter_name, existing_filter in filters.items():
|
||||
# Loop over existing filters to extract metadata by which to create new filters
|
||||
|
||||
# If the filter makes use of a custom filter method or lookup expression skip it
|
||||
# as we cannot sanely handle these cases in a generic mannor
|
||||
if existing_filter.method is not None or existing_filter.lookup_expr not in ['exact', 'in']:
|
||||
continue
|
||||
|
||||
# Choose the lookup expression map based on the filter type
|
||||
lookup_map = cls._get_filter_lookup_dict(existing_filter)
|
||||
if lookup_map is None:
|
||||
# Do not augment this filter type with more lookup expressions
|
||||
continue
|
||||
|
||||
# Get properties of the existing filter for later use
|
||||
field_name = existing_filter.field_name
|
||||
field = get_model_field(cls._meta.model, field_name)
|
||||
|
||||
# Create new filters for each lookup expression in the map
|
||||
for lookup_name, lookup_expr in lookup_map.items():
|
||||
new_filter_name = '{}__{}'.format(existing_filter_name, lookup_name)
|
||||
|
||||
try:
|
||||
if existing_filter_name in cls.declared_filters:
|
||||
# The filter field has been explicity defined on the filterset class so we must manually
|
||||
# create the new filter with the same type because there is no guarantee the defined type
|
||||
# is the same as the default type for the field
|
||||
resolve_field(field, lookup_expr) # Will raise FieldLookupError if the lookup is invalid
|
||||
new_filter = type(existing_filter)(
|
||||
field_name=field_name,
|
||||
lookup_expr=lookup_expr,
|
||||
label=existing_filter.label,
|
||||
exclude=existing_filter.exclude,
|
||||
distinct=existing_filter.distinct,
|
||||
**existing_filter.extra
|
||||
)
|
||||
else:
|
||||
# The filter field is listed in Meta.fields so we can safely rely on default behaviour
|
||||
# Will raise FieldLookupError if the lookup is invalid
|
||||
new_filter = cls.filter_for_field(field, field_name, lookup_expr)
|
||||
except django_filters.exceptions.FieldLookupError:
|
||||
# The filter could not be created because the lookup expression is not supported on the field
|
||||
continue
|
||||
|
||||
if lookup_name.startswith('n'):
|
||||
# This is a negation filter which requires a queryset.exclude() clause
|
||||
# Of course setting the negation of the existing filter's exclude attribute handles both cases
|
||||
new_filter.exclude = not existing_filter.exclude
|
||||
|
||||
new_filters[new_filter_name] = new_filter
|
||||
|
||||
filters.update(new_filters)
|
||||
return filters
|
||||
|
||||
|
||||
class ChangeLoggedModelFilterSet(BaseFilterSet):
|
||||
created = django_filters.DateFilter()
|
||||
created__gte = django_filters.DateFilter(
|
||||
field_name='created',
|
||||
lookup_expr='gte'
|
||||
)
|
||||
created__lte = django_filters.DateFilter(
|
||||
field_name='created',
|
||||
lookup_expr='lte'
|
||||
)
|
||||
last_updated = django_filters.DateTimeFilter()
|
||||
last_updated__gte = django_filters.DateTimeFilter(
|
||||
field_name='last_updated',
|
||||
lookup_expr='gte'
|
||||
)
|
||||
last_updated__lte = django_filters.DateTimeFilter(
|
||||
field_name='last_updated',
|
||||
lookup_expr='lte'
|
||||
)
|
||||
|
||||
|
||||
class PrimaryModelFilterSet(ChangeLoggedModelFilterSet):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Dynamically add a Filter for each CustomField applicable to the parent model
|
||||
custom_fields = CustomField.objects.filter(
|
||||
content_types=ContentType.objects.get_for_model(self._meta.model)
|
||||
).exclude(
|
||||
filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED
|
||||
)
|
||||
for cf in custom_fields:
|
||||
self.filters['cf_{}'.format(cf.name)] = CustomFieldFilter(field_name=cf.name, custom_field=cf)
|
||||
|
||||
|
||||
class OrganizationalModelFilterSet(PrimaryModelFilterSet):
|
||||
"""
|
||||
A base class for adding the search method to models which only expose the `name` and `slug` fields
|
||||
"""
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
models.Q(name__icontains=value) |
|
||||
models.Q(slug__icontains=value)
|
||||
)
|
||||
@@ -20,17 +20,20 @@ class LoginRequiredMiddleware(object):
|
||||
self.get_response = get_response
|
||||
|
||||
def __call__(self, request):
|
||||
# Redirect unauthenticated requests (except those exempted) to the login page if LOGIN_REQUIRED is true
|
||||
if settings.LOGIN_REQUIRED and not request.user.is_authenticated:
|
||||
# Redirect unauthenticated requests to the login page. API requests are exempt from redirection as the API
|
||||
# performs its own authentication. Also metrics can be read without login.
|
||||
api_path = reverse('api-root')
|
||||
if not request.path_info.startswith((api_path, '/metrics')) and request.path_info != settings.LOGIN_URL:
|
||||
return HttpResponseRedirect(
|
||||
'{}?next={}'.format(
|
||||
settings.LOGIN_URL,
|
||||
parse.quote(request.get_full_path_info())
|
||||
)
|
||||
)
|
||||
# Determine exempt paths
|
||||
exempt_paths = [
|
||||
reverse('api-root')
|
||||
]
|
||||
if settings.METRICS_ENABLED:
|
||||
exempt_paths.append(reverse('prometheus-django-metrics'))
|
||||
|
||||
# Redirect unauthenticated requests
|
||||
if not request.path_info.startswith(tuple(exempt_paths)) and request.path_info != settings.LOGIN_URL:
|
||||
login_url = f'{settings.LOGIN_URL}?next={parse.quote(request.get_full_path_info())}'
|
||||
return HttpResponseRedirect(login_url)
|
||||
|
||||
return self.get_response(request)
|
||||
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ from django.core.validators import URLValidator
|
||||
# Environment setup
|
||||
#
|
||||
|
||||
VERSION = '2.11.1'
|
||||
VERSION = '2.11.4'
|
||||
|
||||
# Hostname
|
||||
HOSTNAME = platform.node()
|
||||
@@ -120,6 +120,7 @@ REPORTS_ROOT = getattr(configuration, 'REPORTS_ROOT', os.path.join(BASE_DIR, 're
|
||||
RQ_DEFAULT_TIMEOUT = getattr(configuration, 'RQ_DEFAULT_TIMEOUT', 300)
|
||||
SCRIPTS_ROOT = getattr(configuration, 'SCRIPTS_ROOT', os.path.join(BASE_DIR, 'scripts')).rstrip('/')
|
||||
SESSION_FILE_PATH = getattr(configuration, 'SESSION_FILE_PATH', None)
|
||||
SESSION_COOKIE_NAME = getattr(configuration, 'SESSION_COOKIE_NAME', 'sessionid')
|
||||
SHORT_DATE_FORMAT = getattr(configuration, 'SHORT_DATE_FORMAT', 'Y-m-d')
|
||||
SHORT_DATETIME_FORMAT = getattr(configuration, 'SHORT_DATETIME_FORMAT', 'Y-m-d H:i')
|
||||
SHORT_TIME_FORMAT = getattr(configuration, 'SHORT_TIME_FORMAT', 'H:i:s')
|
||||
|
||||
@@ -2,7 +2,7 @@ $(document).ready(function() {
|
||||
|
||||
// Select or reset table columns
|
||||
$('#save_tableconfig').click(function(event) {
|
||||
$('select[name="columns"] option').attr("selected", "selected");
|
||||
$('select[name="columns"] option').prop("selected", "selected");
|
||||
});
|
||||
$('#reset_tableconfig').click(function(event) {
|
||||
$('select[name="columns"]').val([]);
|
||||
|
||||
@@ -10,7 +10,7 @@ from rest_framework.viewsets import ViewSet
|
||||
|
||||
from extras.api.views import CustomFieldModelViewSet
|
||||
from netbox.api.views import ModelViewSet
|
||||
from secrets import filters
|
||||
from secrets import filtersets
|
||||
from secrets.exceptions import InvalidKey
|
||||
from secrets.models import Secret, SecretRole, SessionKey, UserKey
|
||||
from utilities.utils import count_related
|
||||
@@ -39,7 +39,7 @@ class SecretRoleViewSet(CustomFieldModelViewSet):
|
||||
secret_count=count_related(Secret, 'role')
|
||||
)
|
||||
serializer_class = serializers.SecretRoleSerializer
|
||||
filterset_class = filters.SecretRoleFilterSet
|
||||
filterset_class = filtersets.SecretRoleFilterSet
|
||||
|
||||
|
||||
#
|
||||
@@ -49,7 +49,7 @@ class SecretRoleViewSet(CustomFieldModelViewSet):
|
||||
class SecretViewSet(ModelViewSet):
|
||||
queryset = Secret.objects.prefetch_related('role', 'tags')
|
||||
serializer_class = serializers.SecretSerializer
|
||||
filterset_class = filters.SecretFilterSet
|
||||
filterset_class = filtersets.SecretFilterSet
|
||||
|
||||
master_key = None
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ import django_filters
|
||||
from django.db.models import Q
|
||||
|
||||
from dcim.models import Device
|
||||
from extras.filters import CustomFieldModelFilterSet, CreatedUpdatedFilterSet
|
||||
from utilities.filters import BaseFilterSet, NameSlugSearchFilterSet, TagFilter
|
||||
from extras.filters import TagFilter
|
||||
from netbox.filtersets import OrganizationalModelFilterSet, PrimaryModelFilterSet
|
||||
from virtualization.models import VirtualMachine
|
||||
from .models import Secret, SecretRole
|
||||
|
||||
@@ -14,14 +14,14 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
class SecretRoleFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
|
||||
class SecretRoleFilterSet(OrganizationalModelFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = SecretRole
|
||||
fields = ['id', 'name', 'slug']
|
||||
|
||||
|
||||
class SecretFilterSet(BaseFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
|
||||
class SecretFilterSet(PrimaryModelFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
@@ -233,7 +233,7 @@ class SessionKey(BigIDModel):
|
||||
return session_key
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'export_templates', 'webhooks')
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
class SecretRole(OrganizationalModel):
|
||||
"""
|
||||
A SecretRole represents an arbitrary functional classification of Secrets. For example, a user might define roles
|
||||
@@ -273,7 +273,7 @@ class SecretRole(OrganizationalModel):
|
||||
)
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||
class Secret(PrimaryModel):
|
||||
"""
|
||||
A Secret stores an AES256-encrypted copy of sensitive data, such as passwords or secret keys. An irreversible
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
from django.test import TestCase
|
||||
|
||||
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
|
||||
from secrets.filters import *
|
||||
from secrets.filtersets import *
|
||||
from secrets.models import Secret, SecretRole
|
||||
from utilities.testing import ChangeLoggedFilterSetTests
|
||||
from virtualization.models import Cluster, ClusterType, VirtualMachine
|
||||
|
||||
|
||||
class SecretRoleTestCase(TestCase):
|
||||
class SecretRoleTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = SecretRole.objects.all()
|
||||
filterset = SecretRoleFilterSet
|
||||
|
||||
@@ -20,10 +21,6 @@ class SecretRoleTestCase(TestCase):
|
||||
)
|
||||
SecretRole.objects.bulk_create(roles)
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Secret Role 1', 'Secret Role 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -33,7 +30,7 @@ class SecretRoleTestCase(TestCase):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
class SecretTestCase(TestCase):
|
||||
class SecretTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = Secret.objects.all()
|
||||
filterset = SecretFilterSet
|
||||
|
||||
@@ -80,10 +77,6 @@ class SecretTestCase(TestCase):
|
||||
for s in secrets:
|
||||
s.save()
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Secret 1', 'Secret 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -2,14 +2,14 @@ import base64
|
||||
import logging
|
||||
|
||||
from django.contrib import messages
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.shortcuts import redirect, render
|
||||
from django.utils.html import escape
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
from netbox.views import generic
|
||||
from utilities.tables import paginate_table
|
||||
from utilities.utils import count_related
|
||||
from . import filters, forms, tables
|
||||
from . import filtersets, forms, tables
|
||||
from .models import SecretRole, Secret, SessionKey, UserKey
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ class SecretRoleBulkEditView(generic.BulkEditView):
|
||||
queryset = SecretRole.objects.annotate(
|
||||
secret_count=count_related(Secret, 'role')
|
||||
)
|
||||
filterset = filters.SecretRoleFilterSet
|
||||
filterset = filtersets.SecretRoleFilterSet
|
||||
table = tables.SecretRoleTable
|
||||
form = forms.SecretRoleBulkEditForm
|
||||
|
||||
@@ -86,17 +86,37 @@ class SecretRoleBulkDeleteView(generic.BulkDeleteView):
|
||||
# Secrets
|
||||
#
|
||||
|
||||
def inject_deprecation_warning(request):
|
||||
"""
|
||||
Inject a warning message notifying the user of the pending removal of secrets functionality.
|
||||
"""
|
||||
messages.warning(
|
||||
request,
|
||||
mark_safe('<i class="mdi mdi-alert"></i> The secrets functionality will be moved to a plugin in NetBox v2.12. '
|
||||
'Please see <a href="https://github.com/netbox-community/netbox/issues/5278">issue #5278</a> for '
|
||||
'more information.')
|
||||
)
|
||||
|
||||
|
||||
class SecretListView(generic.ObjectListView):
|
||||
queryset = Secret.objects.all()
|
||||
filterset = filters.SecretFilterSet
|
||||
filterset = filtersets.SecretFilterSet
|
||||
filterset_form = forms.SecretFilterForm
|
||||
table = tables.SecretTable
|
||||
action_buttons = ('import', 'export')
|
||||
|
||||
def get(self, request):
|
||||
inject_deprecation_warning(request)
|
||||
return super().get(request)
|
||||
|
||||
|
||||
class SecretView(generic.ObjectView):
|
||||
queryset = Secret.objects.all()
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
inject_deprecation_warning(request)
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
|
||||
class SecretEditView(generic.ObjectEditView):
|
||||
queryset = Secret.objects.all()
|
||||
@@ -220,12 +240,12 @@ class SecretBulkImportView(generic.BulkImportView):
|
||||
|
||||
class SecretBulkEditView(generic.BulkEditView):
|
||||
queryset = Secret.objects.prefetch_related('role')
|
||||
filterset = filters.SecretFilterSet
|
||||
filterset = filtersets.SecretFilterSet
|
||||
table = tables.SecretTable
|
||||
form = forms.SecretBulkEditForm
|
||||
|
||||
|
||||
class SecretBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = Secret.objects.prefetch_related('role')
|
||||
filterset = filters.SecretFilterSet
|
||||
filterset = filtersets.SecretFilterSet
|
||||
table = tables.SecretTable
|
||||
|
||||
@@ -80,8 +80,8 @@
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
{% include 'circuits/inc/circuit_termination.html' with termination=termination_a side='A' %}
|
||||
{% include 'circuits/inc/circuit_termination.html' with termination=termination_z side='Z' %}
|
||||
{% include 'circuits/inc/circuit_termination.html' with termination=object.termination_a side='A' %}
|
||||
{% include 'circuits/inc/circuit_termination.html' with termination=object.termination_z side='Z' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
<li role="presentation" {% if active_tab == 'device' %} class="active"{% endif %}>
|
||||
<a href="{% url 'dcim:device' pk=object.pk %}">Device</a>
|
||||
</li>
|
||||
{% with interface_count=object.vc_interfaces.count %}
|
||||
{% with interface_count=object.interfaces_count %}
|
||||
{% if interface_count %}
|
||||
<li role="presentation" {% if active_tab == 'interfaces' %} class="active"{% endif %}>
|
||||
<a href="{% url 'dcim:device_interfaces' pk=object.pk %}">Interfaces {% badge interface_count %}</a>
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
<strong><a href="{{ device.get_absolute_url }}">{{ device }}</a></strong><br />
|
||||
{{ device.device_type.manufacturer }} {{ device.device_type }}<br />
|
||||
<a href="{{ device.site.get_absolute_url }}">{{ device.site }}</a>
|
||||
{% if device.location %}
|
||||
/ <a href="{{ device.location.get_absolute_url }}">{{ device.location }}</a>
|
||||
{% endif %}
|
||||
{% if device.rack %}
|
||||
/ <a href="{{ device.rack.get_absolute_url }}">{{ device.rack }}</a>
|
||||
{% endif %}
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
{% extends 'ipam/prefix/base.html' %}
|
||||
|
||||
{% block buttons %}
|
||||
{% if perms.ipam.add_ipaddress and first_available_ip %}
|
||||
<a href="{% url 'ipam:ipaddress_add' %}?address={{ first_available_ip }}&vrf={{ object.vrf.pk }}&tenant_group={{ object.tenant.group.pk }}&tenant={{ object.tenant.pk }}" class="btn btn-success">
|
||||
<i class="mdi mdi-plus-thick" aria-hidden="true"></i>
|
||||
Add an IP Address
|
||||
</a>
|
||||
{% endif %}
|
||||
{{ block.super }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
|
||||
@@ -9,6 +9,11 @@
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu" aria-labelledby="max_length">
|
||||
{% if request.GET.mask_length__lte %}
|
||||
<li>
|
||||
<a href="{% url 'ipam:prefix_list' %}{% querystring request mask_length__lte=None page=1 %}">Clear</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% for i in "4,8,12,16,20,24,28,32,40,48,56,64"|split %}
|
||||
<li><a href="{% url 'ipam:prefix_list' %}{% querystring request mask_length__lte=i page=1 %}">
|
||||
{{ i }} {% if request.GET.mask_length__lte == i %}<i class="mdi mdi-check-bold"></i>{% endif %}
|
||||
|
||||
@@ -10,6 +10,15 @@
|
||||
<li>{{ object }}</li>
|
||||
{% endblock %}
|
||||
|
||||
{% block buttons %}
|
||||
{% if perms.ipam.add_vlan %}
|
||||
<a href="{% url 'ipam:vlan_add' %}?group={{ object.pk }}" class="btn btn-success">
|
||||
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add VLAN
|
||||
</a>
|
||||
{% endif %}
|
||||
{{ block.super }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
@@ -52,21 +61,8 @@
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>VLANs</strong>
|
||||
</div>
|
||||
{% include 'inc/table.html' with table=vlans_table %}
|
||||
{% if perms.ipam.add_vlan %}
|
||||
<div class="panel-footer text-right noprint">
|
||||
<a href="{% url 'ipam:vlan_add' %}?group={{ object.pk }}" class="btn btn-xs btn-primary">
|
||||
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add VLAN
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% include 'inc/paginator.html' with paginator=vlans_table.paginator page=vlans_table.page %}
|
||||
</div>
|
||||
{% include 'utilities/obj_table.html' with table=vlans_table table_template='panel_table.html' heading='VLANs' bulk_edit_url='ipam:vlan_bulk_edit' bulk_delete_url='ipam:vlan_bulk_delete' %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
@@ -76,6 +76,10 @@
|
||||
<h2><a href="{% url 'ipam:vrf_list' %}?tenant_id={{ object.pk }}" class="btn {% if stats.vrf_count %}btn-primary{% else %}btn-default{% endif %} btn-lg">{{ stats.vrf_count }}</a></h2>
|
||||
<p>VRFs</p>
|
||||
</div>
|
||||
<div class="col-md-4 text-center">
|
||||
<h2><a href="{% url 'ipam:aggregate_list' %}?tenant_id={{ object.pk }}" class="btn {% if stats.aggregate_count %}btn-primary{% else %}btn-default{% endif %} btn-lg">{{ stats.aggregate_count }}</a></h2>
|
||||
<p>Aggregates</p>
|
||||
</div>
|
||||
<div class="col-md-4 text-center">
|
||||
<h2><a href="{% url 'ipam:prefix_list' %}?tenant_id={{ object.pk }}" class="btn {% if stats.prefix_count %}btn-primary{% else %}btn-default{% endif %} btn-lg">{{ stats.prefix_count }}</a></h2>
|
||||
<p>Prefixes</p>
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
{% include 'inc/table.html' with table=clusters_table %}
|
||||
{% if perms.virtualization.add_cluster %}
|
||||
<div class="panel-footer text-right noprint">
|
||||
<a href="{% url 'virtualization:cluster_add' %}?type={{ object.pk }}" class="btn btn-xs btn-primary">
|
||||
<a href="{% url 'virtualization:cluster_add' %}?group={{ object.pk }}" class="btn btn-xs btn-primary">
|
||||
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add cluster
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,7 @@ from circuits.models import Circuit
|
||||
from dcim.models import Device, Rack, Site
|
||||
from extras.api.views import CustomFieldModelViewSet
|
||||
from ipam.models import IPAddress, Prefix, VLAN, VRF
|
||||
from tenancy import filters
|
||||
from tenancy import filtersets
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from utilities.utils import count_related
|
||||
from virtualization.models import VirtualMachine
|
||||
@@ -32,7 +32,7 @@ class TenantGroupViewSet(CustomFieldModelViewSet):
|
||||
cumulative=True
|
||||
)
|
||||
serializer_class = serializers.TenantGroupSerializer
|
||||
filterset_class = filters.TenantGroupFilterSet
|
||||
filterset_class = filtersets.TenantGroupFilterSet
|
||||
|
||||
|
||||
#
|
||||
@@ -54,4 +54,4 @@ class TenantViewSet(CustomFieldModelViewSet):
|
||||
vrf_count=count_related(VRF, 'tenant')
|
||||
)
|
||||
serializer_class = serializers.TenantSerializer
|
||||
filterset_class = filters.TenantFilterSet
|
||||
filterset_class = filtersets.TenantFilterSet
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import django_filters
|
||||
from django.db.models import Q
|
||||
|
||||
from extras.filters import CustomFieldModelFilterSet, CreatedUpdatedFilterSet
|
||||
from utilities.filters import BaseFilterSet, NameSlugSearchFilterSet, TagFilter, TreeNodeMultipleChoiceFilter
|
||||
from extras.filters import TagFilter
|
||||
from netbox.filtersets import OrganizationalModelFilterSet, PrimaryModelFilterSet
|
||||
from utilities.filters import TreeNodeMultipleChoiceFilter
|
||||
from .models import Tenant, TenantGroup
|
||||
|
||||
|
||||
@@ -13,7 +14,7 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
class TenantGroupFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
|
||||
class TenantGroupFilterSet(OrganizationalModelFilterSet):
|
||||
parent_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=TenantGroup.objects.all(),
|
||||
label='Tenant group (ID)',
|
||||
@@ -30,7 +31,7 @@ class TenantGroupFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
|
||||
fields = ['id', 'name', 'slug', 'description']
|
||||
|
||||
|
||||
class TenantFilterSet(BaseFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
|
||||
class TenantFilterSet(PrimaryModelFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
@@ -14,7 +14,7 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'export_templates', 'webhooks')
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
class TenantGroup(NestedGroupModel):
|
||||
"""
|
||||
An arbitrary collection of Tenants.
|
||||
@@ -57,7 +57,7 @@ class TenantGroup(NestedGroupModel):
|
||||
)
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||
class Tenant(PrimaryModel):
|
||||
"""
|
||||
A Tenant represents an organization served by the NetBox owner. This is typically a customer or an internal
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
from django.test import TestCase
|
||||
|
||||
from tenancy.filters import *
|
||||
from tenancy.filtersets import *
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from utilities.testing import ChangeLoggedFilterSetTests
|
||||
|
||||
|
||||
class TenantGroupTestCase(TestCase):
|
||||
class TenantGroupTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = TenantGroup.objects.all()
|
||||
filterset = TenantGroupFilterSet
|
||||
|
||||
@@ -27,10 +28,6 @@ class TenantGroupTestCase(TestCase):
|
||||
for tenantgroup in tenant_groups:
|
||||
tenantgroup.save()
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Tenant Group 1', 'Tenant Group 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -51,7 +48,7 @@ class TenantGroupTestCase(TestCase):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
class TenantTestCase(TestCase):
|
||||
class TenantTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = Tenant.objects.all()
|
||||
filterset = TenantFilterSet
|
||||
|
||||
@@ -73,10 +70,6 @@ class TenantTestCase(TestCase):
|
||||
)
|
||||
Tenant.objects.bulk_create(tenants)
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Tenant 1', 'Tenant 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -1,10 +1,10 @@
|
||||
from circuits.models import Circuit
|
||||
from dcim.models import Site, Rack, Device, RackReservation
|
||||
from ipam.models import IPAddress, Prefix, VLAN, VRF
|
||||
from ipam.models import Aggregate, IPAddress, Prefix, VLAN, VRF
|
||||
from netbox.views import generic
|
||||
from utilities.tables import paginate_table
|
||||
from virtualization.models import VirtualMachine, Cluster
|
||||
from . import filters, forms, tables
|
||||
from . import filtersets, forms, tables
|
||||
from .models import Tenant, TenantGroup
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ class TenantGroupBulkEditView(generic.BulkEditView):
|
||||
'tenant_count',
|
||||
cumulative=True
|
||||
)
|
||||
filterset = filters.TenantGroupFilterSet
|
||||
filterset = filtersets.TenantGroupFilterSet
|
||||
table = tables.TenantGroupTable
|
||||
form = forms.TenantGroupBulkEditForm
|
||||
|
||||
@@ -85,7 +85,7 @@ class TenantGroupBulkDeleteView(generic.BulkDeleteView):
|
||||
|
||||
class TenantListView(generic.ObjectListView):
|
||||
queryset = Tenant.objects.all()
|
||||
filterset = filters.TenantFilterSet
|
||||
filterset = filtersets.TenantFilterSet
|
||||
filterset_form = forms.TenantFilterForm
|
||||
table = tables.TenantTable
|
||||
|
||||
@@ -101,6 +101,7 @@ class TenantView(generic.ObjectView):
|
||||
'device_count': Device.objects.restrict(request.user, 'view').filter(tenant=instance).count(),
|
||||
'vrf_count': VRF.objects.restrict(request.user, 'view').filter(tenant=instance).count(),
|
||||
'prefix_count': Prefix.objects.restrict(request.user, 'view').filter(tenant=instance).count(),
|
||||
'aggregate_count': Aggregate.objects.restrict(request.user, 'view').filter(tenant=instance).count(),
|
||||
'ipaddress_count': IPAddress.objects.restrict(request.user, 'view').filter(tenant=instance).count(),
|
||||
'vlan_count': VLAN.objects.restrict(request.user, 'view').filter(tenant=instance).count(),
|
||||
'circuit_count': Circuit.objects.restrict(request.user, 'view').filter(tenant=instance).count(),
|
||||
@@ -130,12 +131,12 @@ class TenantBulkImportView(generic.BulkImportView):
|
||||
|
||||
class TenantBulkEditView(generic.BulkEditView):
|
||||
queryset = Tenant.objects.prefetch_related('group')
|
||||
filterset = filters.TenantFilterSet
|
||||
filterset = filtersets.TenantFilterSet
|
||||
table = tables.TenantTable
|
||||
form = forms.TenantBulkEditForm
|
||||
|
||||
|
||||
class TenantBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = Tenant.objects.prefetch_related('group')
|
||||
filterset = filters.TenantFilterSet
|
||||
filterset = filtersets.TenantFilterSet
|
||||
table = tables.TenantTable
|
||||
|
||||
@@ -89,6 +89,7 @@ class UserAdmin(UserAdmin_):
|
||||
('Important dates', {'fields': ('last_login', 'date_joined')}),
|
||||
)
|
||||
filter_horizontal = ('groups',)
|
||||
list_filter = ('is_active', 'is_staff', 'is_superuser', 'groups__name')
|
||||
|
||||
def get_inlines(self, request, obj):
|
||||
if obj is not None:
|
||||
|
||||
@@ -6,7 +6,7 @@ from rest_framework.routers import APIRootView
|
||||
from rest_framework.viewsets import ViewSet
|
||||
|
||||
from netbox.api.views import ModelViewSet
|
||||
from users import filters
|
||||
from users import filtersets
|
||||
from users.models import ObjectPermission, UserConfig
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
from utilities.utils import deepmerge
|
||||
@@ -28,13 +28,13 @@ class UsersRootView(APIRootView):
|
||||
class UserViewSet(ModelViewSet):
|
||||
queryset = RestrictedQuerySet(model=User).prefetch_related('groups').order_by('username')
|
||||
serializer_class = serializers.UserSerializer
|
||||
filterset_class = filters.UserFilterSet
|
||||
filterset_class = filtersets.UserFilterSet
|
||||
|
||||
|
||||
class GroupViewSet(ModelViewSet):
|
||||
queryset = RestrictedQuerySet(model=Group).annotate(user_count=Count('user')).order_by('name')
|
||||
serializer_class = serializers.GroupSerializer
|
||||
filterset_class = filters.GroupFilterSet
|
||||
filterset_class = filtersets.GroupFilterSet
|
||||
|
||||
|
||||
#
|
||||
@@ -44,7 +44,7 @@ class GroupViewSet(ModelViewSet):
|
||||
class ObjectPermissionViewSet(ModelViewSet):
|
||||
queryset = ObjectPermission.objects.prefetch_related('object_types', 'groups', 'users')
|
||||
serializer_class = serializers.ObjectPermissionSerializer
|
||||
filterset_class = filters.ObjectPermissionFilterSet
|
||||
filterset_class = filtersets.ObjectPermissionFilterSet
|
||||
|
||||
|
||||
#
|
||||
|
||||
@@ -2,8 +2,8 @@ import django_filters
|
||||
from django.contrib.auth.models import Group, User
|
||||
from django.db.models import Q
|
||||
|
||||
from netbox.filtersets import BaseFilterSet
|
||||
from users.models import ObjectPermission
|
||||
from utilities.filters import BaseFilterSet
|
||||
|
||||
__all__ = (
|
||||
'GroupFilterSet',
|
||||
@@ -2,11 +2,12 @@ from django.contrib.auth.models import Group, User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.test import TestCase
|
||||
|
||||
from users.filters import GroupFilterSet, ObjectPermissionFilterSet, UserFilterSet
|
||||
from users.filtersets import GroupFilterSet, ObjectPermissionFilterSet, UserFilterSet
|
||||
from users.models import ObjectPermission
|
||||
from utilities.testing import BaseFilterSetTests
|
||||
|
||||
|
||||
class UserTestCase(TestCase):
|
||||
class UserTestCase(TestCase, BaseFilterSetTests):
|
||||
queryset = User.objects.all()
|
||||
filterset = UserFilterSet
|
||||
|
||||
@@ -59,10 +60,6 @@ class UserTestCase(TestCase):
|
||||
users[1].groups.set([groups[1]])
|
||||
users[2].groups.set([groups[2]])
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_username(self):
|
||||
params = {'username': ['User1', 'User2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -95,7 +92,7 @@ class UserTestCase(TestCase):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
class GroupTestCase(TestCase):
|
||||
class GroupTestCase(TestCase, BaseFilterSetTests):
|
||||
queryset = Group.objects.all()
|
||||
filterset = GroupFilterSet
|
||||
|
||||
@@ -109,16 +106,12 @@ class GroupTestCase(TestCase):
|
||||
)
|
||||
Group.objects.bulk_create(groups)
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Group 1', 'Group 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
class ObjectPermissionTestCase(TestCase):
|
||||
class ObjectPermissionTestCase(TestCase, BaseFilterSetTests):
|
||||
queryset = ObjectPermission.objects.all()
|
||||
filterset = ObjectPermissionFilterSet
|
||||
|
||||
@@ -160,10 +153,6 @@ class ObjectPermissionTestCase(TestCase):
|
||||
permissions[i].users.set([users[i]])
|
||||
permissions[i].object_types.set([object_types[i]])
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Permission 1', 'Permission 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -130,22 +130,24 @@ class ColorChoices(ChoiceSet):
|
||||
|
||||
class ButtonColorChoices(ChoiceSet):
|
||||
"""
|
||||
Map standard button color choices to Bootstrap color classes
|
||||
Map standard button color choices to Bootstrap 3 button classes
|
||||
"""
|
||||
DEFAULT = 'default'
|
||||
BLUE = 'primary'
|
||||
GREY = 'secondary'
|
||||
CYAN = 'info'
|
||||
GREEN = 'success'
|
||||
RED = 'danger'
|
||||
YELLOW = 'warning'
|
||||
GREY = 'secondary'
|
||||
BLACK = 'dark'
|
||||
|
||||
CHOICES = (
|
||||
(DEFAULT, 'Default'),
|
||||
(BLUE, 'Blue'),
|
||||
(GREY, 'Grey'),
|
||||
(CYAN, 'Cyan'),
|
||||
(GREEN, 'Green'),
|
||||
(RED, 'Red'),
|
||||
(YELLOW, 'Yellow'),
|
||||
(GREY, 'Grey'),
|
||||
(BLACK, 'Black')
|
||||
)
|
||||
|
||||
@@ -1,17 +1,9 @@
|
||||
import django_filters
|
||||
from django_filters.constants import EMPTY_VALUES
|
||||
from copy import deepcopy
|
||||
from dcim.forms import MACAddressField
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
from django_filters.utils import get_model_field, resolve_field
|
||||
from django_filters.constants import EMPTY_VALUES
|
||||
|
||||
from extras.models import Tag
|
||||
from utilities.constants import (
|
||||
FILTER_CHAR_BASED_LOOKUP_MAP, FILTER_NEGATION_LOOKUP_MAP, FILTER_TREENODE_NEGATION_LOOKUP_MAP,
|
||||
FILTER_NUMERIC_BASED_LOOKUP_MAP
|
||||
)
|
||||
from dcim.forms import MACAddressField
|
||||
|
||||
|
||||
def multivalue_field_factory(field_class):
|
||||
@@ -91,21 +83,6 @@ class NullableCharFieldFilter(django_filters.CharFilter):
|
||||
return qs.distinct() if self.distinct else qs
|
||||
|
||||
|
||||
class TagFilter(django_filters.ModelMultipleChoiceFilter):
|
||||
"""
|
||||
Match on one or more assigned tags. If multiple tags are specified (e.g. ?tag=foo&tag=bar), the queryset is filtered
|
||||
to objects matching all tags.
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
kwargs.setdefault('field_name', 'tags__slug')
|
||||
kwargs.setdefault('to_field_name', 'slug')
|
||||
kwargs.setdefault('conjoined', True)
|
||||
kwargs.setdefault('queryset', Tag.objects.all())
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class NumericArrayFilter(django_filters.NumberFilter):
|
||||
"""
|
||||
Filter based on the presence of an integer within an ArrayField.
|
||||
@@ -134,182 +111,3 @@ class ContentTypeFilter(django_filters.CharFilter):
|
||||
f'{self.field_name}__model': model
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# FilterSets
|
||||
#
|
||||
|
||||
class BaseFilterSet(django_filters.FilterSet):
|
||||
"""
|
||||
A base filterset which provides common functionaly to all NetBox filtersets
|
||||
"""
|
||||
FILTER_DEFAULTS = deepcopy(django_filters.filterset.FILTER_FOR_DBFIELD_DEFAULTS)
|
||||
FILTER_DEFAULTS.update({
|
||||
models.AutoField: {
|
||||
'filter_class': MultiValueNumberFilter
|
||||
},
|
||||
models.CharField: {
|
||||
'filter_class': MultiValueCharFilter
|
||||
},
|
||||
models.DateField: {
|
||||
'filter_class': MultiValueDateFilter
|
||||
},
|
||||
models.DateTimeField: {
|
||||
'filter_class': MultiValueDateTimeFilter
|
||||
},
|
||||
models.DecimalField: {
|
||||
'filter_class': MultiValueNumberFilter
|
||||
},
|
||||
models.EmailField: {
|
||||
'filter_class': MultiValueCharFilter
|
||||
},
|
||||
models.FloatField: {
|
||||
'filter_class': MultiValueNumberFilter
|
||||
},
|
||||
models.IntegerField: {
|
||||
'filter_class': MultiValueNumberFilter
|
||||
},
|
||||
models.PositiveIntegerField: {
|
||||
'filter_class': MultiValueNumberFilter
|
||||
},
|
||||
models.PositiveSmallIntegerField: {
|
||||
'filter_class': MultiValueNumberFilter
|
||||
},
|
||||
models.SlugField: {
|
||||
'filter_class': MultiValueCharFilter
|
||||
},
|
||||
models.SmallIntegerField: {
|
||||
'filter_class': MultiValueNumberFilter
|
||||
},
|
||||
models.TimeField: {
|
||||
'filter_class': MultiValueTimeFilter
|
||||
},
|
||||
models.URLField: {
|
||||
'filter_class': MultiValueCharFilter
|
||||
},
|
||||
MACAddressField: {
|
||||
'filter_class': MultiValueMACAddressFilter
|
||||
},
|
||||
})
|
||||
|
||||
@staticmethod
|
||||
def _get_filter_lookup_dict(existing_filter):
|
||||
# Choose the lookup expression map based on the filter type
|
||||
if isinstance(existing_filter, (
|
||||
MultiValueDateFilter,
|
||||
MultiValueDateTimeFilter,
|
||||
MultiValueNumberFilter,
|
||||
MultiValueTimeFilter
|
||||
)):
|
||||
lookup_map = FILTER_NUMERIC_BASED_LOOKUP_MAP
|
||||
|
||||
elif isinstance(existing_filter, (
|
||||
TreeNodeMultipleChoiceFilter,
|
||||
)):
|
||||
# TreeNodeMultipleChoiceFilter only support negation but must maintain the `in` lookup expression
|
||||
lookup_map = FILTER_TREENODE_NEGATION_LOOKUP_MAP
|
||||
|
||||
elif isinstance(existing_filter, (
|
||||
django_filters.ModelChoiceFilter,
|
||||
django_filters.ModelMultipleChoiceFilter,
|
||||
TagFilter
|
||||
)) or existing_filter.extra.get('choices'):
|
||||
# These filter types support only negation
|
||||
lookup_map = FILTER_NEGATION_LOOKUP_MAP
|
||||
|
||||
elif isinstance(existing_filter, (
|
||||
django_filters.filters.CharFilter,
|
||||
django_filters.MultipleChoiceFilter,
|
||||
MultiValueCharFilter,
|
||||
MultiValueMACAddressFilter
|
||||
)):
|
||||
lookup_map = FILTER_CHAR_BASED_LOOKUP_MAP
|
||||
|
||||
else:
|
||||
lookup_map = None
|
||||
|
||||
return lookup_map
|
||||
|
||||
@classmethod
|
||||
def get_filters(cls):
|
||||
"""
|
||||
Override filter generation to support dynamic lookup expressions for certain filter types.
|
||||
|
||||
For specific filter types, new filters are created based on defined lookup expressions in
|
||||
the form `<field_name>__<lookup_expr>`
|
||||
"""
|
||||
filters = super().get_filters()
|
||||
|
||||
new_filters = {}
|
||||
for existing_filter_name, existing_filter in filters.items():
|
||||
# Loop over existing filters to extract metadata by which to create new filters
|
||||
|
||||
# If the filter makes use of a custom filter method or lookup expression skip it
|
||||
# as we cannot sanely handle these cases in a generic mannor
|
||||
if existing_filter.method is not None or existing_filter.lookup_expr not in ['exact', 'in']:
|
||||
continue
|
||||
|
||||
# Choose the lookup expression map based on the filter type
|
||||
lookup_map = cls._get_filter_lookup_dict(existing_filter)
|
||||
if lookup_map is None:
|
||||
# Do not augment this filter type with more lookup expressions
|
||||
continue
|
||||
|
||||
# Get properties of the existing filter for later use
|
||||
field_name = existing_filter.field_name
|
||||
field = get_model_field(cls._meta.model, field_name)
|
||||
|
||||
# Create new filters for each lookup expression in the map
|
||||
for lookup_name, lookup_expr in lookup_map.items():
|
||||
new_filter_name = '{}__{}'.format(existing_filter_name, lookup_name)
|
||||
|
||||
try:
|
||||
if existing_filter_name in cls.declared_filters:
|
||||
# The filter field has been explicity defined on the filterset class so we must manually
|
||||
# create the new filter with the same type because there is no guarantee the defined type
|
||||
# is the same as the default type for the field
|
||||
resolve_field(field, lookup_expr) # Will raise FieldLookupError if the lookup is invalid
|
||||
new_filter = type(existing_filter)(
|
||||
field_name=field_name,
|
||||
lookup_expr=lookup_expr,
|
||||
label=existing_filter.label,
|
||||
exclude=existing_filter.exclude,
|
||||
distinct=existing_filter.distinct,
|
||||
**existing_filter.extra
|
||||
)
|
||||
else:
|
||||
# The filter field is listed in Meta.fields so we can safely rely on default behaviour
|
||||
# Will raise FieldLookupError if the lookup is invalid
|
||||
new_filter = cls.filter_for_field(field, field_name, lookup_expr)
|
||||
except django_filters.exceptions.FieldLookupError:
|
||||
# The filter could not be created because the lookup expression is not supported on the field
|
||||
continue
|
||||
|
||||
if lookup_name.startswith('n'):
|
||||
# This is a negation filter which requires a queryset.exclude() clause
|
||||
# Of course setting the negation of the existing filter's exclude attribute handles both cases
|
||||
new_filter.exclude = not existing_filter.exclude
|
||||
|
||||
new_filters[new_filter_name] = new_filter
|
||||
|
||||
filters.update(new_filters)
|
||||
return filters
|
||||
|
||||
|
||||
class NameSlugSearchFilterSet(django_filters.FilterSet):
|
||||
"""
|
||||
A base class for adding the search method to models which only expose the `name` and `slug` fields
|
||||
"""
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
models.Q(name__icontains=value) |
|
||||
models.Q(slug__icontains=value)
|
||||
)
|
||||
|
||||
@@ -4,7 +4,9 @@ from django.core.paginator import Paginator, Page
|
||||
|
||||
class EnhancedPaginator(Paginator):
|
||||
|
||||
def __init__(self, object_list, per_page, **kwargs):
|
||||
def __init__(self, object_list, per_page, orphans=None, **kwargs):
|
||||
|
||||
# Determine the page size
|
||||
try:
|
||||
per_page = int(per_page)
|
||||
if per_page < 1:
|
||||
@@ -12,7 +14,13 @@ class EnhancedPaginator(Paginator):
|
||||
except ValueError:
|
||||
per_page = settings.PAGINATE_COUNT
|
||||
|
||||
super().__init__(object_list, per_page, **kwargs)
|
||||
# Set orphans count based on page size
|
||||
if orphans is None and per_page <= 50:
|
||||
orphans = 5
|
||||
elif orphans is None:
|
||||
orphans = 10
|
||||
|
||||
super().__init__(object_list, per_page, orphans=orphans, **kwargs)
|
||||
|
||||
def _get_page(self, *args, **kwargs):
|
||||
return EnhancedPage(*args, **kwargs)
|
||||
|
||||
@@ -5,7 +5,6 @@ from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import FieldDoesNotExist
|
||||
from django.db.models.fields.related import RelatedField
|
||||
from django.urls import reverse
|
||||
from django.utils.html import strip_tags
|
||||
from django.utils.safestring import mark_safe
|
||||
from django_tables2 import RequestConfig
|
||||
from django_tables2.data import TableQuerysetData
|
||||
@@ -15,19 +14,6 @@ from extras.models import CustomField
|
||||
from .paginator import EnhancedPaginator, get_paginate_count
|
||||
|
||||
|
||||
def stripped_value(self, **kwargs):
|
||||
"""
|
||||
Replaces TemplateColumn's value() method to both strip HTML tags and remove any leading/trailing whitespace.
|
||||
"""
|
||||
html = super(tables.TemplateColumn, self).value(**kwargs)
|
||||
return strip_tags(html).strip() if isinstance(html, str) else html
|
||||
|
||||
|
||||
# TODO: We're monkey-patching TemplateColumn here to strip leading/trailing whitespace. This will no longer
|
||||
# be necessary under django-tables2 v2.3.5+. (See #5926)
|
||||
tables.TemplateColumn.value = stripped_value
|
||||
|
||||
|
||||
class BaseTable(tables.Table):
|
||||
"""
|
||||
Default table for object lists
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from .api import *
|
||||
from .base import *
|
||||
from .filtersets import *
|
||||
from .utils import *
|
||||
from .views import *
|
||||
|
||||
35
netbox/utilities/testing/filtersets.py
Normal file
35
netbox/utilities/testing/filtersets.py
Normal file
@@ -0,0 +1,35 @@
|
||||
from datetime import date, datetime, timezone
|
||||
|
||||
|
||||
__all__ = (
|
||||
'BaseFilterSetTests',
|
||||
'ChangeLoggedFilterSetTests',
|
||||
)
|
||||
|
||||
|
||||
class BaseFilterSetTests:
|
||||
queryset = None
|
||||
filterset = None
|
||||
|
||||
def test_id(self):
|
||||
"""
|
||||
Test filtering for two PKs from a set of >2 objects.
|
||||
"""
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertGreater(self.queryset.count(), 2)
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
class ChangeLoggedFilterSetTests(BaseFilterSetTests):
|
||||
|
||||
def test_created(self):
|
||||
pk_list = self.queryset.values_list('pk', flat=True)[:2]
|
||||
self.queryset.filter(pk__in=pk_list).update(created=date(2021, 1, 1))
|
||||
params = {'created': '2021-01-01'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_last_updated(self):
|
||||
pk_list = self.queryset.values_list('pk', flat=True)[:2]
|
||||
self.queryset.filter(pk__in=pk_list).update(last_updated=datetime(2021, 1, 1, 0, 0, 0, tzinfo=timezone.utc))
|
||||
params = {'last_updated': '2021-01-01T00:00:00'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
@@ -7,14 +7,16 @@ from taggit.managers import TaggableManager
|
||||
|
||||
from dcim.choices import *
|
||||
from dcim.fields import MACAddressField
|
||||
from dcim.filters import DeviceFilterSet, SiteFilterSet
|
||||
from dcim.filtersets import DeviceFilterSet, SiteFilterSet
|
||||
from dcim.models import (
|
||||
Device, DeviceRole, DeviceType, Interface, Manufacturer, Platform, Rack, Region, Site
|
||||
)
|
||||
from extras.filters import TagFilter
|
||||
from extras.models import TaggedItem
|
||||
from netbox.filtersets import BaseFilterSet
|
||||
from utilities.filters import (
|
||||
BaseFilterSet, MACAddressFilter, MultiValueCharFilter, MultiValueDateFilter, MultiValueDateTimeFilter,
|
||||
MultiValueNumberFilter, MultiValueTimeFilter, TagFilter, TreeNodeMultipleChoiceFilter,
|
||||
MACAddressFilter, MultiValueCharFilter, MultiValueDateFilter, MultiValueDateTimeFilter, MultiValueNumberFilter,
|
||||
MultiValueTimeFilter, TreeNodeMultipleChoiceFilter,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ from rest_framework.routers import APIRootView
|
||||
from dcim.models import Device
|
||||
from extras.api.views import ConfigContextQuerySetMixin, CustomFieldModelViewSet, ModelViewSet
|
||||
from utilities.utils import count_related
|
||||
from virtualization import filters
|
||||
from virtualization import filtersets
|
||||
from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
|
||||
from . import serializers
|
||||
|
||||
@@ -25,7 +25,7 @@ class ClusterTypeViewSet(CustomFieldModelViewSet):
|
||||
cluster_count=count_related(Cluster, 'type')
|
||||
)
|
||||
serializer_class = serializers.ClusterTypeSerializer
|
||||
filterset_class = filters.ClusterTypeFilterSet
|
||||
filterset_class = filtersets.ClusterTypeFilterSet
|
||||
|
||||
|
||||
class ClusterGroupViewSet(CustomFieldModelViewSet):
|
||||
@@ -33,7 +33,7 @@ class ClusterGroupViewSet(CustomFieldModelViewSet):
|
||||
cluster_count=count_related(Cluster, 'group')
|
||||
)
|
||||
serializer_class = serializers.ClusterGroupSerializer
|
||||
filterset_class = filters.ClusterGroupFilterSet
|
||||
filterset_class = filtersets.ClusterGroupFilterSet
|
||||
|
||||
|
||||
class ClusterViewSet(CustomFieldModelViewSet):
|
||||
@@ -44,7 +44,7 @@ class ClusterViewSet(CustomFieldModelViewSet):
|
||||
virtualmachine_count=count_related(VirtualMachine, 'cluster')
|
||||
)
|
||||
serializer_class = serializers.ClusterSerializer
|
||||
filterset_class = filters.ClusterFilterSet
|
||||
filterset_class = filtersets.ClusterFilterSet
|
||||
|
||||
|
||||
#
|
||||
@@ -55,7 +55,7 @@ class VirtualMachineViewSet(ConfigContextQuerySetMixin, CustomFieldModelViewSet)
|
||||
queryset = VirtualMachine.objects.prefetch_related(
|
||||
'cluster__site', 'role', 'tenant', 'platform', 'primary_ip4', 'primary_ip6', 'tags'
|
||||
)
|
||||
filterset_class = filters.VirtualMachineFilterSet
|
||||
filterset_class = filtersets.VirtualMachineFilterSet
|
||||
|
||||
def get_serializer_class(self):
|
||||
"""
|
||||
@@ -83,5 +83,5 @@ class VMInterfaceViewSet(ModelViewSet):
|
||||
'virtual_machine', 'parent', 'tags', 'tagged_vlans', 'ip_addresses'
|
||||
)
|
||||
serializer_class = serializers.VMInterfaceSerializer
|
||||
filterset_class = filters.VMInterfaceFilterSet
|
||||
filterset_class = filtersets.VMInterfaceFilterSet
|
||||
brief_prefetch_fields = ['virtual_machine']
|
||||
|
||||
@@ -2,12 +2,11 @@ import django_filters
|
||||
from django.db.models import Q
|
||||
|
||||
from dcim.models import DeviceRole, Platform, Region, Site, SiteGroup
|
||||
from extras.filters import CustomFieldModelFilterSet, CreatedUpdatedFilterSet, LocalConfigContextFilterSet
|
||||
from tenancy.filters import TenancyFilterSet
|
||||
from utilities.filters import (
|
||||
BaseFilterSet, MultiValueMACAddressFilter, NameSlugSearchFilterSet, TagFilter,
|
||||
TreeNodeMultipleChoiceFilter,
|
||||
)
|
||||
from extras.filters import TagFilter
|
||||
from extras.filtersets import LocalConfigContextFilterSet
|
||||
from netbox.filtersets import OrganizationalModelFilterSet, PrimaryModelFilterSet
|
||||
from tenancy.filtersets import TenancyFilterSet
|
||||
from utilities.filters import MultiValueMACAddressFilter, TreeNodeMultipleChoiceFilter
|
||||
from .choices import *
|
||||
from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
|
||||
|
||||
@@ -20,21 +19,21 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
class ClusterTypeFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
|
||||
class ClusterTypeFilterSet(OrganizationalModelFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = ClusterType
|
||||
fields = ['id', 'name', 'slug', 'description']
|
||||
|
||||
|
||||
class ClusterGroupFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
|
||||
class ClusterGroupFilterSet(OrganizationalModelFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = ClusterGroup
|
||||
fields = ['id', 'name', 'slug', 'description']
|
||||
|
||||
|
||||
class ClusterFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
|
||||
class ClusterFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
@@ -110,13 +109,7 @@ class ClusterFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSe
|
||||
)
|
||||
|
||||
|
||||
class VirtualMachineFilterSet(
|
||||
BaseFilterSet,
|
||||
LocalConfigContextFilterSet,
|
||||
TenancyFilterSet,
|
||||
CustomFieldModelFilterSet,
|
||||
CreatedUpdatedFilterSet
|
||||
):
|
||||
class VirtualMachineFilterSet(PrimaryModelFilterSet, TenancyFilterSet, LocalConfigContextFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
@@ -237,7 +230,7 @@ class VirtualMachineFilterSet(
|
||||
return queryset.exclude(params)
|
||||
|
||||
|
||||
class VMInterfaceFilterSet(BaseFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
|
||||
class VMInterfaceFilterSet(PrimaryModelFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
@@ -646,7 +646,7 @@ class VMInterfaceForm(BootstrapMixin, InterfaceCommonForm, CustomFieldModelForm)
|
||||
vm_id = self.initial.get('virtual_machine') or self.data.get('virtual_machine')
|
||||
|
||||
# Restrict parent interface assignment by VM
|
||||
self.fields['parent'].widget.add_query_param('virtualmachine_id', vm_id)
|
||||
self.fields['parent'].widget.add_query_param('virtual_machine_id', vm_id)
|
||||
|
||||
# Limit VLAN choices by virtual machine
|
||||
self.fields['untagged_vlan'].widget.add_query_param('available_on_virtualmachine', vm_id)
|
||||
@@ -669,7 +669,7 @@ class VMInterfaceCreateForm(BootstrapMixin, InterfaceCommonForm):
|
||||
required=False,
|
||||
display_field='display_name',
|
||||
query_params={
|
||||
'virtualmachine_id': 'virtual_machine',
|
||||
'virtual_machine_id': '$virtual_machine',
|
||||
}
|
||||
)
|
||||
mtu = forms.IntegerField(
|
||||
@@ -683,7 +683,7 @@ class VMInterfaceCreateForm(BootstrapMixin, InterfaceCommonForm):
|
||||
label='MAC Address'
|
||||
)
|
||||
description = forms.CharField(
|
||||
max_length=100,
|
||||
max_length=200,
|
||||
required=False
|
||||
)
|
||||
mode = forms.ChoiceField(
|
||||
@@ -712,9 +712,6 @@ class VMInterfaceCreateForm(BootstrapMixin, InterfaceCommonForm):
|
||||
super().__init__(*args, **kwargs)
|
||||
vm_id = self.initial.get('virtual_machine') or self.data.get('virtual_machine')
|
||||
|
||||
# Restrict parent interface assignment by VM
|
||||
self.fields['parent'].widget.add_query_param('virtualmachine_id', vm_id)
|
||||
|
||||
# Limit VLAN choices by virtual machine
|
||||
self.fields['untagged_vlan'].widget.add_query_param('available_on_virtualmachine', vm_id)
|
||||
self.fields['tagged_vlans'].widget.add_query_param('available_on_virtualmachine', vm_id)
|
||||
@@ -798,7 +795,7 @@ class VMInterfaceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
|
||||
vm_id = self.initial.get('virtual_machine')
|
||||
|
||||
# Restrict parent interface assignment by VM
|
||||
self.fields['parent'].widget.add_query_param('virtualmachine_id', vm_id)
|
||||
self.fields['parent'].widget.add_query_param('virtual_machine_id', vm_id)
|
||||
|
||||
# Limit VLAN choices by virtual machine
|
||||
self.fields['untagged_vlan'].widget.add_query_param('available_on_virtualmachine', vm_id)
|
||||
|
||||
@@ -30,7 +30,7 @@ __all__ = (
|
||||
# Cluster types
|
||||
#
|
||||
|
||||
@extras_features('custom_fields', 'export_templates', 'webhooks')
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
class ClusterType(OrganizationalModel):
|
||||
"""
|
||||
A type of Cluster.
|
||||
@@ -73,7 +73,7 @@ class ClusterType(OrganizationalModel):
|
||||
# Cluster groups
|
||||
#
|
||||
|
||||
@extras_features('custom_fields', 'export_templates', 'webhooks')
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
class ClusterGroup(OrganizationalModel):
|
||||
"""
|
||||
An organizational group of Clusters.
|
||||
@@ -116,7 +116,7 @@ class ClusterGroup(OrganizationalModel):
|
||||
# Clusters
|
||||
#
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||
class Cluster(PrimaryModel):
|
||||
"""
|
||||
A cluster of VirtualMachines. Each Cluster may optionally be associated with one or more Devices.
|
||||
@@ -199,7 +199,7 @@ class Cluster(PrimaryModel):
|
||||
# Virtual machines
|
||||
#
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||
class VirtualMachine(PrimaryModel, ConfigContextModel):
|
||||
"""
|
||||
A virtual machine which runs inside a Cluster.
|
||||
@@ -380,7 +380,7 @@ class VirtualMachine(PrimaryModel, ConfigContextModel):
|
||||
# Interfaces
|
||||
#
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||
class VMInterface(PrimaryModel, BaseInterface):
|
||||
virtual_machine = models.ForeignKey(
|
||||
to='virtualization.VirtualMachine',
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user